Pi Daemon is a generic agent host. An application (a coordinator, a bot, a fleet manager) uses it in two ways and needs nothing else from the host:
- A profile that the operator configures, carrying the application's own Durable extension module and MCP servers. This is how application behaviour runs inside the agent.
- A protocol client (AHP, or
/chordfrom JavaScript) that creates sessions and sends turns. This is how the application drives agents.
The host has no application endpoints and no application semantics: no wake, tickets, reconcile, schedules, nudges, heartbeats, deadlines, generations or delivery receipts. The application owns those and expresses them as ordinary turns, cancels and session lifecycle.
1. Give the application a profile
profiles:
myapp:
cwdRoots: [~/work/myapp-agents]
model: anthropic/claude-sonnet-4-5
tools: [read, write, edit, bash]
extensions: [~/.config/myapp/pi-daemon-extension.mjs]
mcpServers:
myapp: { command: myapp, args: [mcp, stdio] }
envFiles: [~/.config/myapp/agent.env] # credentials the MCP server and tools need
sessionEnv: [MYAPP_AGENT_ID] # nonsecret per-session values
sessionEnvFiles: ["{cwd}/../env.sh"] # each agent's own credentials, beside its cwd
concurrency: 8 # this profile's turns at once
The profile's sessions run in their own worker process, so the application's extension, MCP servers and load cannot take down or starve another profile's sessions. See Profiles for every field.
Extension module
The module is a trusted Durable extension loaded into the profile's worker. It is where agent-side behaviour lives:
beforeToolhooks, for example a pause gate that blocks tools while the application has the agent paused;- lifecycle hooks such as
afterResponseor compaction hooks, and tools and system-prompt sections of its own; - an optional read-only
observe(event, { sessionId, conversationId, state })for side effects such as narration or metrics, which cannot steer the agent; - optional
commands:/nameslash commands that run in the worker instead of a turn, with small per-session durablestateandnotifytext for the session's clients (for example/tts on).
Editing the file hot-reloads it for the next model request. See Extension modules for the exports and reload rules.
MCP servers
MCP servers are the main way an application gives agents capabilities: its commands become tools the agent can call. They run inside the profile's worker with the session's environment. Clients cannot add MCP servers; they come only from the profile.
2. Drive sessions over AHP
AHP is the protocol for applications in any language (official
TypeScript reducers in @microsoft/agent-host-protocol, Rust
ahp crates). Connect a WebSocket to /ahp with
Authorization: Bearer <token>, then:
initialize, offering the protocol versions the client speaks, most preferred first (["1.0.0", "0.9.0", "0.8.0"]). The daemon picks 1.0 or 0.9 when offered, else 0.8.0, and sends a 0.8 client 0.8 shapes (AHP 1.0 with a 0.8 fallback).- Create or reuse a session.
resolveSessionConfigwith the profile asproviderreturns the config schema and defaults.createSessionwith a session URI the application chooses (ahp-session:/<id>), the profile and a cwd under its roots creates the session. Retrying with the same profile and cwd succeeds without creating anything, so "ensure this agent's session" is one call. Put atitlein the config (an agent id, say) to name the session; otherwise it is titled from its first prompt. Agents with different roles can share one profile, which is then the catalog they compose from: put each agent'ssystemPromptorappendSystemPrompt,env,tools, its ownmcpServers(full definitions, as Claude's--mcp-configtakes them) and chosenresourcesin its config, and the session keeps them (see A session's own configuration). Check forpi-daemon/sessionProfilein theinitializeresult's_metafirst, and fail closed on a host without it. - Send a turn. Subscribe to the chat
ahp-chat:/<id>/1and dispatchchat/turnStartedwith a user message. Use the application's own message id as the turn id. The daemon submits it to Durable with the request idahp:<clientId>:<turnId>, so sending the same turn again (after a reconnect or an application restart) never runs it twice; the duplicate comes back with arejectionReason. Keep a stable AHP client id across restarts for this to hold. - Follow it. The answer streams as
chat/*actions and ends withchat/turnComplete,chat/turnCancelledorchat/error. That end state is the application's delivery status. - Answer questions. An agent's
ask_userquestion appears as an input request in the chat's active turn and in the session'sinputNeeded; answer it by dispatchingchat/inputCompleted. - Delete.
disposeSessiondeletes the session's store and catalog entry.
Other clients (Culture, an editor) see the same sessions and turns live, so an operator can watch or join an application's agent.
Timing belongs to the application
| Application need | How to do it |
|---|---|
| Deliver a message to an agent | A turn whose turn id is the message id |
| Heartbeats, nudges, batching | The application's own timers sending ordinary turns |
| Schedules | The application's scheduler sending turns |
| Deadlines | The application's timer cancelling the turn |
| Pause | The application's extension module blocking tools in
beforeTool |
| Delivery status | The turn's end state over AHP |
JavaScript clients:
/chord
A Node application can use Durable's own views instead of AHP.
ChordClient from this package connects to
/chord with the bearer:
import { ChordClient } from "@harryaskham/pi-daemon";
const chord = await ChordClient.connect({ url: "http://127.0.0.1:7463", token });
const done = await chord.call("conversation.submit", {
sessionId, requestId: messageId, content: "Summarize today's changes", wait: true,
});
A requestId reused on retry is accepted once.
conversation.watchEvents streams the conversation's Durable
agent events, and conversation.watch keeps a replica of its
view. The methods are listed in Operations. Sessions can be
created and disposed over /chord too
(sessions.create, sessions.dispose), and it
serves what a terminal client needs: models, / and
@ completions, ! commands and usage.
pi-daemon's own terminal client, pi-client, is built on
exactly these methods (see the README's pi-client section);
ChordClient.connect also accepts the full URL of a
/chord endpoint behind a proxy, for example
wss://caco.example/api/v1/agents/a1/chord.