pi-codes 1.0.0

Typed Rust SDK for the pi coding agent (earendil-works/pi): serde models of the `pi --mode json` event stream and the `pi --mode rpc` stdin/stdout command protocol, plus an async (Tokio) RPC client.
Documentation

pi-codes

Typed Rust SDK for the pi coding agent (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.0.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.