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.1.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] aprompt/steer/follow_upack reports on pi ≥ 0.99.0). Unmodeled commands pass through viaRpcCommand::Raw.--mode json— the one-shot event stream:PiEventwith typed lifecycle/tool events and anUnknownfallback that preserves the payload, so newer CLIs degrade soft.- Messages —
system/user/assistant/toolResult/bashExecutionroles with content blocks (text, thinking, toolCall, image) and usage accounting. Thesystemrole (pi ≥ 0.86.0) carries the promptsectionsandtoolsAdded/toolsRemoveddeclarations.
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,steerorfollow_upresponse carriesdata.disposition:started(a run began;promptonly),queued(held during a run) orhandled(an extension consumed it — no run started, so don't wait foragent_settled). Read it withRpcResponse::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. acodemodescript) emitstool_execution_*events carryingparentToolCallId, 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-inbashresults add astructuredContenttwin (output,exit_code,truncated) beside the model-facingcontent. Pinned by the 0.99.1 corpus. -
Every turn opens with a
systemmessage (0.86.0). It streams asmessage_start/message_endbefore the user message and leadsagent_end'smessages, carrying the full prompt as namedsectionsplustoolsAddeddeclarations; later prompt/tool changes arrive as furthersystemmessages 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
--yoloequivalent because nothing needs bypassing.--approve/--no-approveonly control trusting project-local config files (extensions, AGENTS.md). -
tool_execution_endcarries noargsfield in RPC mode, though the documentedAgentEventtype includes it. Pinned by the corpus; the decoder tolerates both. -
--mode jsonhangs headless: with no TTY it produced no output and never exited in our probes (with and without--print). Use--mode rpcfor headless work — that is whatPiRpcClientdrives. -
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 inresult.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.