Skip to content

@ahpd/agent-pi ​

npmCIlicense MITnode >=22Agent Host Protocol 0.9.0

The pi coding agent as a backend for @ahpd/sdk, and a plugin for the @ahpd/server daemon. A pi session runs on one machine and is driven from VS Code, ahpc or any other AHP client, with more than one watching at once.

pi runs inside the daemon's process through pi's own SDK, not as a pi --mode rpc subprocess.

Part of ahpd. The source is in packages/agent-pi.

Install ​

bash
ahpd plugin install @ahpd/agent-pi
ahpd restart

Update, remove and one-run flags such as --plugin are in Plugins.

What it needs ​

  • At least one model provider set up in pi's own settings, or a record with its own baseUrl and apiKey. pi reads its model providers and credentials from its own settings, so nothing here needs configuring for those. Running pi by hand in the same directory is the quickest check.

Agents ​

This plugin registers the engine pi, and every pi agent is a record of that engine on the agents channel. The plugin's own options are the defaults each record starts from; a record names its own model, project trust, session directory and variables, and an edit of one applies at the session's next turn.

Options ​

The options below are the plugin's, in the configuration file:

json
{
  "plugins": [
    { "name": "@ahpd/agent-pi", "options": { "model": "anthropic/claude-sonnet-4-5", "projectTrust": "deny" } }
  ]
}

{ "plugins": ["@ahpd/agent-pi"] } is enough when pi's own settings already name a model.

Every option below is a key under options in the plugin's entry, and the daemon checks each value against the schema before apply runs. A record that names one wins over it.

OptionDefaultWhat it does
modelpi's own defaultThe model a record that names none runs on, as provider/modelId; a turn may choose another, and a resumed or forked session keeps its own
baseUrlpi's own providersThe OpenAI-compatible endpoint a record that names none runs on, such as https://api.example.com/v1
apiKeynoneThe key that endpoint's requests carry. A read answers <set>: the value stays in the configuration file
projectTrusttrustWhether a record that names none loads a project's own pi extensions, skills and prompts
sessionDirpi's ownWhere a record that names none keeps its pi sessions
hostToolstrueWhether the host's own tools reach each session. A record can turn them off for one agent

projectTrust is trust or deny. pi's third answer, ask, is not offered, because a daemon has nobody at a terminal to answer it. trust loads the project's pi resources, which runs its code.

The host's own answer about the folder is asked as well, and both have to say yes. A folder no window vouched for loads none of the project's extensions, skills or prompts, however projectTrust is set. See Trusted folders.

With sessionDir left alone, pi run by hand in the same directory lists the sessions started here, and the other way round.

provider, displayName and description are not options: they are the record's id, name and description.

A record ​

A record of the engine holds what one pi agent runs on, and the channel owns its id, name, description and model. A new record starts from the engine's one template, pi with nothing set, which runs on pi's own agent directory and credentials.

FieldWhat it does
modelThe model this agent runs on, as provider/modelId
baseUrlThe OpenAI-compatible endpoint this agent runs on. Its models are listed as <record id>/<model id>
apiKeyThe key that endpoint's requests carry. Kept in the vault, as a secret field
projectTrusttrust or deny, as above
sessionDirWhere this agent's pi sessions live. Host records only
hostToolsWhether the host's own tools reach this agent's sessions. Off drops them and keeps each client's tools
envVariables pi is given, by name

A variable in env is a key of this agent's own when pi takes a key from it, such as ANTHROPIC_API_KEY or DEEPSEEK_API_KEY: it is kept in the vault, a client is told it is set and never what it is, and a team's or a person's record may set it. Any other variable is the host's to set, because a free environment can run a program. The record's variables go on pi's own runtime each turn, over the daemon's environment and over auth.json, and they are never written to process.env where the daemon's other agents would see them.

The engine's key variables are the ones pi's own provider list reads - ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_OAUTH_TOKEN, DEEPSEEK_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, GEMINI_API_KEY and the rest of pi's list - so any owner may set one. Every other variable in env is host-only, and so is sessionDir.

A record that names its own provider key, with the model the channel owns:

json
{
  "id": "pi-deepseek",
  "engine": "pi",
  "owner": "host",
  "name": "pi on DeepSeek",
  "model": "deepseek/deepseek-flash",
  "config": { "env": { "DEEPSEEK_API_KEY": { "$secret": "host:deepseek" } } }
}

A client puts the key's value, and the channel moves it to the vault under host:agent/<id>/env.DEEPSEEK_API_KEY and keeps a { "$secret": "<name>" } in its place, so no client is shown the value again. The record above names host:deepseek, the form a record written by hand on disk uses; ahpd vault set host:deepseek keeps that entry from standard input.

A client makes one with x-ahpd/agents/put, and the daemon keeps it at agents/<id>.json in its configuration folder. A record whose config no longer fits, or whose $secret the vault cannot answer, is written and shown as invalid with one line naming it, and no value; a record whose engine no plugin registers any more is kept and shown as unavailable.

apiKey is a writeOnly option: it is checked as any other is, stays in the configuration file, and a read answers <set>. A record's own apiKey is a secret field, so the channel keeps it in the vault under the record and no client is shown the value again. A key for a machine is the @ahpd/computer profile's, as below.

A record with a baseUrl and no apiKey runs on a keyless endpoint: it lists that endpoint's models and runs turns on them, and its requests carry no Authorization header, as a cofold agent's do.

In your own host ​

bash
pnpm add @ahpd/agent-pi @ahpd/sdk
ts
import { createHost, listen } from '@ahpd/sdk';
import { piAgent } from '@ahpd/agent-pi';

const path = process.cwd();
const host = createHost({ path, agents: [piAgent({}, [path])] });
await listen({ port: 9187 }, (peer) => host.accept(peer));

piAgent(options, directories) takes the options above and the directories the backend may work in.

In a machine ​

A pi session whose computer setting names a machine runs nested: pi is a library in the daemon's process, so an ahpd from the ahpd part, which carries this plugin, is started inside the machine and runs the session there. Any glibc image runs it with nothing installed.

pi's agent directory inside is /ahpd/pi, set as PI_CODING_AGENT_DIR: a state volume seeded with this host's settings.json and models.json, or this host's directory mounted read-write in a profile with state: "host". auth.json is never seeded, so a key reaches the machine as a need the profile fills, one per variable pi's provider list reads, named <provider>.<VARIABLE>:

json
{ "plugins": [{ "name": "@ahpd/computer", "options": {
  "profiles": { "pi": { "agents": ["pi"], "needs": { "pi.ANTHROPIC_API_KEY": { "$secret": "user:ada/anthropic" } } } }
} }] }

Such a key reaches pi's nested host and no other agent in the machine. A variable only pi's own models.json names is not declared, and does not reach a machine. See pi in a machine.

What maps, and what does not ​

Some of pi lands on the protocol without adaptation:

  • Steering is pi's own steer(), so a message sent into a running turn is the thing the protocol means rather than a queued message pretending to be one. The message is said back as the chat's steering message as it is taken, which answers the client that sent it, and taken out once pi's queue no longer holds it: pi says its queue on every change, and reads its steering messages in the order it was handed them.
  • Truncation is navigateTree. pi's sessions are append-only trees: the leaf moves back and the abandoned path stops being context, which is exactly what chat/truncated asks for. A turn read back from pi's file ends at the entry the file says it did, so it can be truncated like one this session watched. A !command turn and one whose prompt threw leave no point to cut at, and the host refuses to truncate them.
  • The thinking level rides in each model's own configSchema, the protocol's escape hatch for a model that needs an answer beside its name. A client draws it as a form beside the model and sends the values back in ModelSelection.config. It is VS Code's "Effort Level" property over the levels pi gives the model, one level included, labelled and described in VS Code's words, its default the level pi's settings start the model on.
  • Models are identified as provider/modelId, the way pi's own configuration spells one, so two providers serving a model of the same name stay apart. The probe lists the models pi's own runtime has for its agent directory, so a client's picker has them before any session opens and after a restart. A record with a baseUrl is listed from its endpoint's /models as well, read with the record's key, or with no Authorization header when it has none, on each probe, so an edit of the endpoint lists the new models. A provider only a project's extension registers arrives when a session in that project opens.
  • Resuming a failed turn is pi's agent.continue(). The reply that failed leaves the model's context and pi continues from the call before it, so the tools the turn already ran are not run again. Only a failed model call is resumable, the latest turn, and after a restart too. A turn that failed before pi had its prompt is not.
  • Forking a turn is SessionManager.createBranchedSession. The entry a turn settled at is what a fork cuts at, and branching there writes the conversation through that turn, answer included, under a new id, leaving the source file as it was.
  • The session file is a real path, so the reference client's "open session state file" opens the conversation pi actually wrote.
  • Host and client tools. The tools the host contributes to a session are offered to pi's model, and one of the host's own runs in the host. A tool a connected client provides is offered too, and a call to it is reported against that client and waits for its answer. A client's tools take effect from the next turn, because pi fixes its custom tools when the session starts and the session is restarted on the same file with the new set.
  • Tool confirmation. A session asks before a call its permissionMode says to ask about: the call is shown pending-confirmation with what it asks to do, for the host to decide, the session is InputNeeded, and the host's answer runs the call or blocks it with a reason the model reads. A tool that declares no effects runs, a read outside the working directory asks, and the six modes carry the meanings Claude and cofold advertise, with default as pi's default.
  • Customizations. The skills, prompt templates and extensions pi loaded for a session are its customizations: a directory per folder pi loads a kind from, holding that kind, and an installed pi package a plugin holding its kinds. A skill is a skill, a prompt template a prompt and an extension a hook, pi's extensions being its hooks and custom tools in one, and each is named by its URI. A switch takes effect from the next turn, as a tool change does: pi is built again on the same file, its loader's own overrides leaving out what is off.

Some of it does not, and the backend does not advertise it:

  • MCP servers. MCP in pi is an extension's job, so a start or a stop answers false.
  • Several directories per session. pi's AgentSession has a single cwd, which its tools, project resources and session store all use.

The catalogue ​

Sessions are listed from two places at once: every file pi's store holds, read with pi's own listAll, so a conversation somebody had from the pi command in any folder has a row here, and the sessions this process is running. A row that came off disk opens with its turns: the entries on the file's current branch are raised as the events pi raises live and read through the same mapping a running turn uses, so a rebuilt turn has the same text, reasoning and tool calls a watched one has. A session this process is running answers from what it watched, after the turns it resumed from the file.

Documentation ​

PLUGINS.mdLoading a plugin into the daemon
AGENT.mdThe Agent and Session contracts this implements
piThe coding agent this runs

License ​

MIT © Softov