The binary
list and chat are for a terminal. serve is for an app: it speaks the
same Runtime and Session calls as one JSON object per line, so a
TypeScript, Python, Swift, or Go app spawns the binary and talks to it.
The crate stays the only place with logic.
Frames
Six shapes. Every command carries anid you choose; its reply carries
the same id back, so several commands can be in flight.
Events are the crate’s own serialization, unchanged: a variant with fields is
{"kind": {"TextDelta": {"message_id": "m1", "text": "hi"}}}, and a unit
variant is a bare string, {"kind": "ContextCompacted"}.
What the sidecar guarantees:
- The
okreply toopenis written before any frame for that session. - Frames for one session keep the crate’s
sequenceorder; sessions interleave. - A session error is terminal. The running turn has already ended with
TurnEnded { stop: Failed };closedfollows the error, after the engine’s last bookkeeping events. closedis written exactly once per opened session.- EOF on stdin closes every open session, even one whose
openwas still in flight, writes theirclosed, and exits 0. Closing is “close stdin, wait”. A stdout that stops accepting writes endsservewith that error. - A line that is not a command gets
{"id": null, "error": {"kind": "BadFrame", ..}}and the loop continues. - The reader must keep up. A session whose events are not read within 1024 of production is closed by the crate, the same rule a Rust caller lives with.
Commands
One command per public call.agent is a catalog id such as "claude";
a custom ACP agent is {"acp": {"name": "..", "path": "..", "args": [..]}}.
An error body is
kind, message, and the variant’s own fields: agent
for NotInstalled, login for AuthRequired, status and stderr for
ProcessExited, detail for the rest. Two kinds are the sidecar’s own:
BadFrame and UnknownSession (with session). A command on a closed
session gets the crate’s SessionClosed.
Types for other languages
packages/schema.json is a JSON schema (draft 7) of every wire type,
generated from the Rust types with just schema and checked by
tests/schema.rs. The command type is Frame; every output line is a
Line; events are Event and EventKind; errors are ErrorBody. Feed it
to a generator:
Testing an app without agents
A build with--features mock accepts serve --mock <script.json>: the
real engine over a scripted agent. The scripts every wrapper’s tests use
are in packages/mock-scripts/, with a README mapping each test case to
its file.
Packages
Linux builds need glibc 2.28 or newer: Ubuntu 20.04, Debian 10, RHEL 8 and up.
session.info and session.status stay current; a session error throws
from the for await (raises from the async for in Python);
rt.generate(agent, { dir }, prompt) is one-shot text.
Runtime.start({ bin, mock }) runs the package over a mock script.
Every package passes the same eleven tests over the mock scripts on macOS,
Linux, and Windows in CI; the TypeScript package also runs ten live on
claude and codex.
Writing a wrapper
The wrapper is a pipe: spawn, write lines, route lines byid or
session_id, buffer events per session. These rules keep every wrapper
honest, and the same eleven subprocess tests run in each.