# pi-codes
Typed Rust SDK for the [pi coding agent](https://github.com/earendil-works/pi)
(npm `@earendil-works/pi-coding-agent`): serde models of the
`pi --mode json` JSONL event stream and the `pi --mode rpc` stdin/stdout
command protocol, plus an async (Tokio) RPC client.
Tested against pi 1.4.0. The crate version may carry a patch offset
above the CLI release for crate-side additions.
## What's covered
- **`--mode rpc`** — the headless command protocol: typed
[`RpcCommand`]s (prompt/steer/follow-up, state, model, thinking,
compaction, bash, session), the `{"type":"response"}` envelope with id
correlation, and typed views of the important payloads
([`AgentState`], [`BashResult`], messages, and the
[`InputDisposition`] a `prompt`/`steer`/`follow_up` ack reports on
pi ≥ 0.99.0). Unmodeled commands pass through via `RpcCommand::Raw`.
- **`--mode json`** — the one-shot event stream: `PiEvent` with typed
lifecycle/tool events and an `Unknown` fallback that preserves the
payload, so newer CLIs degrade soft.
- **Messages** — `system` / `user` / `assistant` / `toolResult` /
`bashExecution` roles with content blocks (text, thinking, toolCall,
image) and usage accounting. The `system` role (pi ≥ 0.86.0) carries
the prompt `sections` and `toolsAdded` / `toolsRemoved` declarations.
## Live testing
The integration tier (`cargo test -p pi-codes --features
integration-tests`) drives a real `pi --mode rpc` process and is
**credential-free**: state, model catalog, id correlation, the `bash`
command (a real shell round trip with typed results landing in
`get_messages`), and clean failure envelopes for unknown commands.
Model-turn coverage additionally needs a configured provider
(`pi auth check --provider <p>`).
Note: pi requires **Node 22+** (`fs.globSync`).
The model tier (gated on `OPENAI_API_KEY`) adds a streamed model turn
plus a conformance trio mirroring wirecheck's cross-harness checks:
the model reads a planted nonce file, writes a requested nonce to a
requested path, and runs a shell command with a disk-visible side
effect — write/bash verified on disk, never from the transcript.
## Measured wire notes (pi 0.84.4 → 0.99.1)
- **Input acks report a disposition (0.99.0).** A successful `prompt`,
`steer` or `follow_up` response carries `data.disposition`:
`started` (a run began; `prompt` only), `queued` (held during a run)
or `handled` (an extension consumed it — no run started, so don't
wait for `agent_settled`). Read it with `RpcResponse::disposition()`.
Earlier CLIs sent a bare success envelope. Pinned by the 0.99.1
corpus.
- **Nested tool calls are events only (0.99.0).** A tool that runs
other tools (`ctx.executeTool()`, e.g. a `codemode` script) emits
`tool_execution_*` events carrying `parentToolCallId`, with a
`<parent id>/<n>` `toolCallId`. They never appear as tool calls or
tool results in the transcript.
- **Assistant messages carry `thinkingLevel` (0.99.0)**, and built-in
`bash` results add a `structuredContent` twin (`output`, `exit_code`,
`truncated`) beside the model-facing `content`. Pinned by the 0.99.1
corpus.
- **Every turn opens with a `system` message (0.86.0).** It streams as
`message_start` / `message_end` before the user message and leads
`agent_end`'s `messages`, carrying the full prompt as named
`sections` plus `toolsAdded` declarations; later prompt/tool changes
arrive as further `system` messages that patch by name. Pinned by
the 0.86.0 corpus; 0.84.4 never emitted one.
- **pi has no tool-approval gate.** read/bash/edit/write run without
prompting; there is no `--yolo` equivalent because nothing needs
bypassing. `--approve`/`--no-approve` only control trusting
project-local config files (extensions, AGENTS.md).
- **`tool_execution_end` carries no `args` field** in RPC mode, though
the documented `AgentEvent` type includes it. Pinned by the corpus;
the decoder tolerates both.
- **`--mode json` hangs headless**: with no TTY it produced no output
and never exited in our probes (with and without `--print`). Use
`--mode rpc` for headless work — that is what `PiRpcClient` drives.
- The model may issue parallel tool calls in one assistant message; a
racing read can fail with ENOENT and recover next turn. Tool errors
arrive as `tool_execution_end { isError: true }` with the error text
in `result.content`, not as stream errors.
## Framing contract
RPC mode is strict JSONL: split records on `\n` only and tolerate a
trailing `\r`. U+2028/U+2029 are valid inside JSON strings and must not
split records — the Tokio line reader used here complies.