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 catalog agent at an exact path is {"id": "claude", "path": "/opt/claude"}; a custom ACP agent is {"acp": {"name": "..", "path": "..", "args": [..]}}. answer is one of {"Permission": "AllowOnce"}, {"Question": [..]}, {"Deny": {"message": ".."}} (deny once with a reason), or "Cancel" (withdraw the request). 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. probe and plan_usage take open’s option fields too; resume or fork on them is InvalidConfiguration. plan_usage uses only env, args and config_home of them.

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, throws from the for try await in Swift, is yielded once by Events in Go); rt.generate(agent, { dir }, prompt) is one-shot text; rt.probe(agent, { dir, env }) and rt.planUsage(agent, { config_home }) take the launch options. 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 (Swift: macOS and Linux); the TypeScript package also runs ten live on claude and codex. packages/README.md lays the four packages side by side.

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.