Appearance
@ahpd/agent-pi
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 restartUpdate, 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
baseUrlandapiKey. pi reads its model providers and credentials from its own settings, so nothing here needs configuring for those. Runningpiby 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.
| Option | Default | What it does |
|---|---|---|
model | pi's own default | The 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 |
baseUrl | pi's own providers | The OpenAI-compatible endpoint a record that names none runs on, such as https://api.example.com/v1 |
apiKey | none | The key that endpoint's requests carry. A read answers <set>: the value stays in the configuration file |
projectTrust | trust | Whether a record that names none loads a project's own pi extensions, skills and prompts |
sessionDir | pi's own | Where a record that names none keeps its pi sessions |
hostTools | true | Whether 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.
| Field | What it does |
|---|---|
model | The model this agent runs on, as provider/modelId |
baseUrl | The OpenAI-compatible endpoint this agent runs on. Its models are listed as <record id>/<model id> |
apiKey | The key that endpoint's requests carry. Kept in the vault, as a secret field |
projectTrust | trust or deny, as above |
sessionDir | Where this agent's pi sessions live. Host records only |
hostTools | Whether the host's own tools reach this agent's sessions. Off drops them and keeps each client's tools |
env | Variables 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/sdkts
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 whatchat/truncatedasks 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!commandturn and one whosepromptthrew 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 inModelSelection.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 abaseUrlis listed from its endpoint's/modelsas well, read with the record's key, or with noAuthorizationheader 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
permissionModesays to ask about: the call is shownpending-confirmationwith what it asks to do, for the host to decide, the session isInputNeeded, 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, withdefaultas 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 apromptand an extension ahook, 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
AgentSessionhas a singlecwd, 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.md | Loading a plugin into the daemon |
| AGENT.md | The Agent and Session contracts this implements |
| pi | The coding agent this runs |
License
MIT © Softov