pi
v0.1.3Pi coding agent as an iii worker — pi::* functions run headless Pi turns, mirror raw events onto pi::events, and stream AgentEvent frames onto agent::events.
exact versions are immutable; binary and bundle artifacts are digest-pinned.
full markdown
/workers/pi.md?version=0.1.3. paste it into an llm prompt or pipe it through curl from a worker.install
configuration
- defaults:
agent_dir:
cwd:
model:
thinking_level: medium
tools:
engine_url: ws://127.0.0.1:49134
events_stream: agent::events
iii_context: true
raw_events_stream: pi::eventsdependencies
readme
pi
Pi coding agent as an iii worker: the Pi API exposed as functions and streams on the iii bus, nothing else. The worker runs the same in-process agent loop Pi runs in the terminal, with the same tools (read, bash, edit, write) against any host directory. pi::run executes one headless turn and returns the result; the raw Pi events mirror verbatim onto the pi::events stream, and a translated AgentEvent view lands on agent::events, so the iii console, the acp worker, and any sibling worker observe a Pi run exactly like a native harness turn. The worker also registers run::start_and_wait, the same entrypoint the console and the acp worker drive, so both run Pi with no changes.
Install
iii worker add piPi runs the loop in-process (no CLI subprocess), so the worker environment needs model credentials — ANTHROPIC_API_KEY (or the provider key Pi is configured for) or an existing Pi login.
Skills
Install the pi agent skill for Claude Code, Cursor, and 30+ other agents:
npx skills add iii-hq/workers --skill piQuickstart
From zero to a Pi turn over the bus:
curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
iii worker add pi
iii # starts the engine + workerThen talk to it like any other function: from the console chat, from iii trigger pi::run, or from any SDK:
import { registerWorker } from 'iii-sdk';
const iii = registerWorker('ws://127.0.0.1:49134', { workerName: 'demo' });
const res = await iii.trigger({
function_id: 'pi::run',
payload: {
prompt: 'Add a /health endpoint to server.ts and run the tests',
cwd: '/path/to/repo',
},
timeout_ms: 600_000,
});
// { session_id, pi_session_id, result, stop_reason, usage, total_cost_usd }Or straight from the terminal with the iii trigger CLI:
# one full turn (raise the timeout; the default 30s is too short for agent turns)
iii trigger pi::run --timeout-ms 600000 \
--json '{"prompt":"add a /health endpoint and run the tests","cwd":"/path/to/repo"}'
# quick reads use key=value syntax
iii trigger pi::sessions::list
iii trigger pi::status session_id=<session_id>
# background turn + control
iii trigger pi::start --json '{"prompt":"...","cwd":"/path/to/repo"}'
iii trigger pi::stop session_id=<session_id>
# ask the running engine for a function's description
iii trigger pi::run --helpA turn from the CLI returns the result with token usage and cost:

iii trigger pi::run --help prints the published request schema as a parameter table:

Call pi::run again with the returned session_id to continue the same conversation: the worker maps iii session ids to Pi session files in engine state and resumes automatically.

Two ids come back from every run. session_id is the iii session id: the key for pi::status, pi::stop, pi::steer, resume, and the stream group. pi_session_id is Pi's internal session id — returned for reference, not a lookup key.
Long turns: use pi::start to return immediately, then watch agent::events (group_id = your session_id) for message_complete, function_execution_start/end, and turn_end frames. pi::stop interrupts a live run, pi::status reads a point-in-time view, pi::sessions::list enumerates past sessions.
Functions
| Function | Purpose |
|---|---|
pi::run |
Run one turn, wait, return the final result |
pi::start |
Fire-and-forget turn; progress arrives on agent::events |
pi::steer |
Inject a steering instruction into a live run |
pi::follow_up |
Queue a follow-up message for a live run |
pi::stop |
Interrupt a live run |
pi::status |
Session state, live flag, usage, cost |
pi::sessions::list |
All sessions this worker has run |
run::start_and_wait |
Alias for pi::run under the entrypoint the console and acp worker drive |
pi::run accepts either a bare prompt string or a messages array ([{ role: 'user', content: [{ type: 'text', text }] }]), plus model, cwd, thinking_level, tools, and iii_context overrides.
Raw events
Every event Pi emits (agent_start/end, turn_start/end, message_start/update/end, tool_execution_start/update/end, and the session events queue_update, compaction_start/end) is mirrored verbatim onto the pi::events stream, group_id = session_id. Consumers that want the exact Pi event format read pi::events; consumers that want harness-shaped frames read agent::events. Same turn, two views.
Steering a live run
A turn started with pi::start keeps its session reachable while it streams. Two functions push instructions into it:
# start a long run
iii trigger pi::start --json '{"prompt":"refactor the auth module","cwd":"/path/to/repo","session_id":"s1"}'
# redirect it mid-flight — applied after the current tool calls finish
iii trigger pi::steer --json '{"session_id":"s1","prompt":"stop, keep the public API unchanged"}'
# queue work for after it would otherwise stop
iii trigger pi::follow_up --json '{"session_id":"s1","prompt":"then add tests for the new paths"}'pi::steer maps onto Pi's steering queue (interrupt-style), pi::follow_up onto its follow-up queue (wait-style). Both no-op with {steered:false} / {queued:false} when no run is live for the session.
The agent on the bus
By default every turn's prompt carries the iii runtime context: the same engine-grounded rules as the harness identity prompts, retargeted to the iii CLI the agent reaches through its shell. The agent discovers capabilities from the live engine instead of memory — iii trigger engine::functions::list to find function ids, iii trigger as the contract before every first call, the registry flow (directory::registry::workers::list/info, worker::add) when nothing registered fits — plus the calling rules and error-handling discipline that go with them. Local file edits stay on Pi's native tools; backend actions go through registered functions.
# the agent answers this by querying the live engine itself
iii trigger pi::run --timeout-ms 300000 \
--json '{"prompt":"List every worker connected to this engine and what each one does.","cwd":"/tmp"}'Pi answers by querying the live engine itself, grouping every connected worker by role:

The context is prepended on a fresh session and skipped on resume (it is already in the conversation history). Turn it off entirely with "iii_context": false per call or globally in config.yaml.
Thinking and tools
thinking_level maps straight onto Pi's reasoning levels, per turn:
| Level | Behavior |
|---|---|
off |
No extended reasoning |
minimal / low |
Short reasoning budget |
medium |
The worker default |
high / xhigh |
Deep reasoning (xhigh on supported model families) |
tools is an allowlist: leave it empty for Pi's defaults (read, bash, edit, write), or pass a subset to narrow what the turn can do — e.g. {"tools":["read","bash"]} for a read-and-run turn that cannot edit or write files.
Configuration
engine_url: ws://127.0.0.1:49134
defaults:
model: "" # empty = Pi settings default; else "provider/modelId"
thinking_level: medium # off | minimal | low | medium | high | xhigh
cwd: "" # default working directory for runs
tools: [] # empty = Pi defaults (read, bash, edit, write)
agent_dir: "" # Pi global config dir; empty = ~/.pi/agent
events_stream: agent::events # translated AgentEvent frames
raw_events_stream: pi::events # verbatim Pi events
iii_context: true # prepend the iii runtime context on fresh sessionsconfig.yaml is the seed: on first boot the worker registers it with the built-in configuration worker as the initial value, then reads the live value back and hot-reloads on every configuration:updated. engine_url is excluded from the managed schema — it is bootstrap, so it stays on the local seed / --url.
Observability
Every pi::run is an ordinary traced invocation on the engine: the trace carries the full input payload (prompt, cwd, caller worker id) and the output (result, stop reason, token usage, cost) as span events, with per-function p50/p95/p99 in the console's trace explorer — no extra instrumentation in the worker.
How it maps
| Pi | iii |
|---|---|
AgentSession.prompt() turn |
pi::run invocation |
| every AgentSession event, verbatim | pi::events stream frame |
assistant message_end |
message_complete frame on agent::events |
tool_execution_start / tool_execution_end |
function_execution_start / function_execution_end frames |
| final result | turn_end + agent_end frames, function return value |
steer() / followUp() |
pi::steer / pi::follow_up |
| session resume | engine state scope pi_sessions, keyed by iii session_id |
| extra capability | another iii worker on the bus (shell, database, storage, ...) |
api reference (json)
{
"functions": [
{
"description": "Queue a follow-up message for a live Pi run; processed after the agent would otherwise stop.",
"metadata": {},
"name": "pi::follow_up",
"request_schema": {
"additionalProperties": false,
"properties": {
"prompt": {
"description": "Instruction to inject into the live run",
"type": "string"
},
"session_id": {
"description": "iii session id of a live run",
"type": "string"
}
},
"required": [
"session_id",
"prompt"
],
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"queued": {
"type": "boolean"
},
"reason": {
"type": "string"
},
"session_id": {
"type": "string"
}
},
"required": [
"session_id",
"queued"
],
"type": "object"
}
},
{
"description": "Internal: reload pi configuration when it changes.",
"metadata": {},
"name": "pi::on-config-change",
"request_schema": {
"properties": {},
"type": "object"
},
"response_schema": {
"type": "null"
}
},
{
"description": "Run one Pi coding-agent turn and wait for the result. Accepts `prompt` or a `messages` array; streams raw Pi events onto pi::events, AgentEvent frames onto agent::events, and returns {session_id, result, usage, total_cost_usd}.",
"metadata": {},
"name": "pi::run",
"request_schema": {
"additionalProperties": false,
"properties": {
"cwd": {
"description": "Working directory the turn runs in",
"type": "string"
},
"iii_context": {
"description": "Prepend the iii runtime discovery context (engine catalog via the iii CLI)",
"type": "boolean"
},
"messages": {
"description": "Alternative to prompt: role/content messages; the last user entry becomes the prompt",
"items": {
"additionalProperties": false,
"properties": {
"content": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"additionalProperties": {},
"properties": {
"type": {
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
"type": "array"
}
]
},
"role": {
"type": "string"
}
},
"required": [
"role",
"content"
],
"type": "object"
},
"type": "array"
},
"model": {
"description": "Model as \"provider/modelId\"; empty = Pi settings default",
"type": "string"
},
"prompt": {
"description": "The user prompt for this turn",
"type": "string"
},
"session_id": {
"description": "iii session id; reuse to resume the same Pi conversation",
"type": "string"
},
"thinking_level": {
"description": "Reasoning depth for this turn",
"enum": [
"off",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string"
},
"timeout_ms": {
"description": "Reserved for callers; not forwarded",
"exclusiveMinimum": 0,
"maximum": 9007199254740991,
"type": "integer"
},
"tools": {
"description": "Tool allowlist; empty = Pi defaults (read, bash, edit, write)",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"busy": {
"type": "boolean"
},
"is_error": {
"type": "boolean"
},
"num_turns": {
"type": "number"
},
"pi_session_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"reason": {
"type": "string"
},
"result": {
"type": "string"
},
"session_id": {
"type": "string"
},
"stop_reason": {
"type": "string"
},
"total_cost_usd": {
"type": "number"
},
"usage": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cache_read_tokens": {
"type": "number"
},
"cache_write_tokens": {
"type": "number"
},
"input_tokens": {
"type": "number"
},
"output_tokens": {
"type": "number"
}
},
"required": [
"input_tokens",
"output_tokens"
],
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"session_id"
],
"type": "object"
}
},
{
"description": "List every Pi session this worker has run.",
"metadata": {},
"name": "pi::sessions::list",
"request_schema": {
"properties": {},
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"sessions": {
"items": {
"additionalProperties": false,
"properties": {
"cwd": {
"type": "string"
},
"model": {
"type": "string"
},
"pi_session_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"session_file": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"session_id": {
"type": "string"
},
"status": {
"enum": [
"working",
"done",
"error"
],
"type": "string"
},
"total_cost_usd": {
"type": "number"
},
"turns": {
"type": "number"
},
"updated_at_ms": {
"type": "number"
},
"usage": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cache_read_tokens": {
"type": "number"
},
"cache_write_tokens": {
"type": "number"
},
"input_tokens": {
"type": "number"
},
"output_tokens": {
"type": "number"
}
},
"required": [
"input_tokens",
"output_tokens"
],
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"session_id",
"pi_session_id",
"session_file",
"cwd",
"model",
"status",
"turns",
"total_cost_usd",
"usage",
"updated_at_ms"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"sessions"
],
"type": "object"
}
},
{
"description": "Start a Pi turn and return immediately; watch agent::events (group_id = session_id) for progress and turn_end.",
"metadata": {},
"name": "pi::start",
"request_schema": {
"additionalProperties": false,
"properties": {
"cwd": {
"description": "Working directory the turn runs in",
"type": "string"
},
"iii_context": {
"description": "Prepend the iii runtime discovery context (engine catalog via the iii CLI)",
"type": "boolean"
},
"messages": {
"description": "Alternative to prompt: role/content messages; the last user entry becomes the prompt",
"items": {
"additionalProperties": false,
"properties": {
"content": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"additionalProperties": {},
"properties": {
"type": {
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
"type": "array"
}
]
},
"role": {
"type": "string"
}
},
"required": [
"role",
"content"
],
"type": "object"
},
"type": "array"
},
"model": {
"description": "Model as \"provider/modelId\"; empty = Pi settings default",
"type": "string"
},
"prompt": {
"description": "The user prompt for this turn",
"type": "string"
},
"session_id": {
"description": "iii session id; reuse to resume the same Pi conversation",
"type": "string"
},
"thinking_level": {
"description": "Reasoning depth for this turn",
"enum": [
"off",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string"
},
"timeout_ms": {
"description": "Reserved for callers; not forwarded",
"exclusiveMinimum": 0,
"maximum": 9007199254740991,
"type": "integer"
},
"tools": {
"description": "Tool allowlist; empty = Pi defaults (read, bash, edit, write)",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"busy": {
"type": "boolean"
},
"reason": {
"type": "string"
},
"session_id": {
"type": "string"
},
"started": {
"type": "boolean"
}
},
"required": [
"session_id",
"started"
],
"type": "object"
}
},
{
"description": "Point-in-time status of a Pi session.",
"metadata": {},
"name": "pi::status",
"request_schema": {
"additionalProperties": false,
"properties": {
"session_id": {
"description": "iii session id returned by pi::run / pi::start",
"type": "string"
}
},
"required": [
"session_id"
],
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"live": {
"type": "boolean"
},
"record": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cwd": {
"type": "string"
},
"model": {
"type": "string"
},
"pi_session_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"session_file": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"session_id": {
"type": "string"
},
"status": {
"enum": [
"working",
"done",
"error"
],
"type": "string"
},
"total_cost_usd": {
"type": "number"
},
"turns": {
"type": "number"
},
"updated_at_ms": {
"type": "number"
},
"usage": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cache_read_tokens": {
"type": "number"
},
"cache_write_tokens": {
"type": "number"
},
"input_tokens": {
"type": "number"
},
"output_tokens": {
"type": "number"
}
},
"required": [
"input_tokens",
"output_tokens"
],
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"session_id",
"pi_session_id",
"session_file",
"cwd",
"model",
"status",
"turns",
"total_cost_usd",
"usage",
"updated_at_ms"
],
"type": "object"
},
{
"type": "null"
}
]
},
"session_id": {
"type": "string"
}
},
"required": [
"session_id",
"live",
"record"
],
"type": "object"
}
},
{
"description": "Inject a steering instruction into a live Pi run; applied after the current tool calls finish.",
"metadata": {},
"name": "pi::steer",
"request_schema": {
"additionalProperties": false,
"properties": {
"prompt": {
"description": "Instruction to inject into the live run",
"type": "string"
},
"session_id": {
"description": "iii session id of a live run",
"type": "string"
}
},
"required": [
"session_id",
"prompt"
],
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"reason": {
"type": "string"
},
"session_id": {
"type": "string"
},
"steered": {
"type": "boolean"
}
},
"required": [
"session_id",
"steered"
],
"type": "object"
}
},
{
"description": "Interrupt a live Pi run for a session.",
"metadata": {},
"name": "pi::stop",
"request_schema": {
"additionalProperties": false,
"properties": {
"session_id": {
"description": "iii session id returned by pi::run / pi::start",
"type": "string"
}
},
"required": [
"session_id"
],
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"reason": {
"type": "string"
},
"session_id": {
"type": "string"
},
"stopped": {
"type": "boolean"
}
},
"required": [
"session_id",
"stopped"
],
"type": "object"
}
},
{
"description": "Alias for pi::run under the shared agent entrypoint: run a turn for {session_id, messages} and return when it ends.",
"metadata": {},
"name": "run::start_and_wait",
"request_schema": {
"additionalProperties": false,
"properties": {
"cwd": {
"description": "Working directory the turn runs in",
"type": "string"
},
"iii_context": {
"description": "Prepend the iii runtime discovery context (engine catalog via the iii CLI)",
"type": "boolean"
},
"messages": {
"description": "Alternative to prompt: role/content messages; the last user entry becomes the prompt",
"items": {
"additionalProperties": false,
"properties": {
"content": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"additionalProperties": {},
"properties": {
"type": {
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
"type": "array"
}
]
},
"role": {
"type": "string"
}
},
"required": [
"role",
"content"
],
"type": "object"
},
"type": "array"
},
"model": {
"description": "Model as \"provider/modelId\"; empty = Pi settings default",
"type": "string"
},
"prompt": {
"description": "The user prompt for this turn",
"type": "string"
},
"session_id": {
"description": "iii session id; reuse to resume the same Pi conversation",
"type": "string"
},
"thinking_level": {
"description": "Reasoning depth for this turn",
"enum": [
"off",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string"
},
"timeout_ms": {
"description": "Reserved for callers; not forwarded",
"exclusiveMinimum": 0,
"maximum": 9007199254740991,
"type": "integer"
},
"tools": {
"description": "Tool allowlist; empty = Pi defaults (read, bash, edit, write)",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"response_schema": {
"additionalProperties": false,
"properties": {
"busy": {
"type": "boolean"
},
"is_error": {
"type": "boolean"
},
"num_turns": {
"type": "number"
},
"pi_session_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"reason": {
"type": "string"
},
"result": {
"type": "string"
},
"session_id": {
"type": "string"
},
"stop_reason": {
"type": "string"
},
"total_cost_usd": {
"type": "number"
},
"usage": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"cache_read_tokens": {
"type": "number"
},
"cache_write_tokens": {
"type": "number"
},
"input_tokens": {
"type": "number"
},
"output_tokens": {
"type": "number"
}
},
"required": [
"input_tokens",
"output_tokens"
],
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"session_id"
],
"type": "object"
}
}
],
"triggers": []
}