Skip to main content

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 an id 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 ok reply to open is written before any frame for that session.
  • Frames for one session keep the crate’s sequence order; sessions interleave.
  • A session error is terminal. The running turn has already ended with TurnEnded { stop: Failed }; closed follows the error, after the engine’s last bookkeeping events.
  • closed is written exactly once per opened session.
  • EOF on stdin closes every open session, even one whose open was still in flight, writes their closed, and exits 0. Closing is “close stdin, wait”. A stdout that stops accepting writes ends serve with 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 by id or session_id, buffer events per session. These rules keep every wrapper honest, and the same eleven subprocess tests run in each.