pi-daemon1.3

Reference

Building an application on Pi Daemon

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:

  1. 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.
  2. A protocol client (AHP, or /chord from 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:

  • beforeTool hooks, for example a pause gate that blocks tools while the application has the agent paused;
  • lifecycle hooks such as afterResponse or 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: /name slash commands that run in the worker instead of a turn, with small per-session durable state and notify text 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:

  1. 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).
  2. Create or reuse a session. resolveSessionConfig with the profile as provider returns the config schema and defaults. createSession with 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 a title in 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's systemPrompt or appendSystemPrompt, env, tools, its own mcpServers (full definitions, as Claude's --mcp-config takes them) and chosen resources in its config, and the session keeps them (see A session's own configuration). Check for pi-daemon/sessionProfile in the initialize result's _meta first, and fail closed on a host without it.
  3. Send a turn. Subscribe to the chat ahp-chat:/<id>/1 and dispatch chat/turnStarted with a user message. Use the application's own message id as the turn id. The daemon submits it to Durable with the request id ahp:<clientId>:<turnId>, so sending the same turn again (after a reconnect or an application restart) never runs it twice; the duplicate comes back with a rejectionReason. Keep a stable AHP client id across restarts for this to hold.
  4. Follow it. The answer streams as chat/* actions and ends with chat/turnComplete, chat/turnCancelled or chat/error. That end state is the application's delivery status.
  5. Answer questions. An agent's ask_user question appears as an input request in the chat's active turn and in the session's inputNeeded; answer it by dispatching chat/inputCompleted.
  6. Delete. disposeSession deletes 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.