Schemas and protocols
The two committed JSON Schemas, the versions they carry, and the three wire formats bingo speaks.
Two schemas, both generated
bingo commits two JSON Schema documents at the root of the repository. Both are generated by schemars from the sdk’s own types, so the schema is the code rather than a description of it.
| file | title | protocol | what it describes |
|---|---|---|---|
schema/rpc.json | bingo rpc | 1 | the JSON-RPC surface bingo serve speaks |
schema/plugin.json | bingo plugin | 5 | the wire a cross-process plugin speaks |
Each holds $defs for every wire type — 81 in rpc.json, 110 in plugin.json
— plus a methods table and a notifications table whose entries are $refs
into those definitions. plugin.json also carries a manifest reference and
a hostService block.
Two tests keep them honest. One regenerates the document and fails on any difference, naming the command that updates it:
BINGO_UPDATE_SCHEMA=1 cargo test -p bingo-surface-rpc
BINGO_UPDATE_SCHEMA=1 cargo test -p bingo-plugin-rpc
The other asserts that every property name is camelCase, which is what the journal, the RPC surface and the plugin wire all agree on.
Read the schema, not this page, when you are writing against either wire. What follows is orientation.
The RPC surface
JSON-RPC 2.0, one message per line, UTF-8, over stdin and stdout (ADR-0007).
The method table is the kernel’s host API one for one, plus a handshake.
initialize must come first — anything else is NOT_INITIALIZED.
| method | for |
|---|---|
initialize | the handshake: client identity and protocol in, name, version and capabilities out |
shutdown | end the server |
session/list | sessions matching a filter |
session/open | attach: returns the session and a snapshot |
session/close | detach; the session keeps running |
session/delete | delete it |
session/history | page a long transcript |
session/events | resync: frames after a since are re-sent |
session/submit | one input |
session/interrupt | stop a turn |
session/answer | answer an open interaction |
session/deliver | peer delivery into another session’s queue |
session/extend | publish durable plugin state |
session/signal | publish ephemeral plugin state |
catalog/read | one of models, providers, tools, commands, skills, plugins |
gateway/subscribe | gateway-level events |
Two notifications: event, carrying a Frame exactly as the sdk
serialises it, and gateway/event.
Three properties are worth knowing before writing a client:
- Events are verbatim. A client folds them with
SessionState::apply— the same reducer the kernel runs — and derives its view. Thesession/openresponse, a snapshot at seq N, is written before anyeventof that session with seq greater than N; frames of one session arrive in seq order; andLagged { from, to }means “callsession/eventswith your seq”. - Writes return
{}.submit,interruptandansweranswer as soon as the mailbox took them. The outcome arrives as theIntentAckevent whose intent is the ULID the client minted, which is also the idempotency key. - Errors are two-layered. JSON-RPC’s own codes for protocol faults —
-32700parse,-32600invalid request,-32601unknown method,-32602invalid params — and-32000for every kernel error, whose stable string travels inerror.data.codeand whose text iserror.message:
{"jsonrpc":"2.0","id":3,"error":{"code":-32000,"message":"no such session",
"data":{"code":"SESSION_NOT_FOUND"}}}
One stdio server serves one client. Two processes on one session are refused by the store’s lock. Concurrent clients of one server arrive with the WebSocket transport, which will carry the same bytes.
The plugin wire
Also JSON-RPC 2.0 over NDJSON, and also the sdk’s own types as JSON — the bridge adds envelopes, never shapes.
Nine methods: initialize, tool/call, command/run, command/complete,
context/contribute, compactor/compact, provider/stream, hook/decide,
and service/call — the one that travels in both directions. Five
notifications: tool/progress, tool/cancel, provider/delta,
provider/cancel, hook/observe.
The PROTOCOL version is 5, sent in the handshake and echoed back. A major
the host does not speak is refused with a notice rather than guessed at. The
method count is not a literal any more — it is a pin derived from the committed
schema, so opening a new capability means regenerating the document and bumping
the protocol.
hostService in the schema names the one service the bridge reserves,
bingo.host, and the two methods behind it. See
wire services.
The stream-json envelope
--print --output-format stream-json is a lossy compatibility encoder, not
a third protocol (ADR-0007 §8). It projects the same frames onto Claude Code’s
dialect so a host that already speaks it can drive bingo without a plugin. The
kernel does not know it exists.
Line types: system with subtype init first, then assistant and user
lines as the turn runs, and one result line at the end.
{"type":"system","subtype":"init","session_id":"…","cwd":"…","tools":[…],
"model":"…","permissionMode":"default","apiKeySource":"none"}
{"type":"result","subtype":"success","is_error":false,"duration_ms":8412,
"duration_api_ms":0,"num_turns":3,"result":"…","session_id":"…",
"total_cost_usd":0.0,"usage":{…}}
result exists only on the success arm; the error arms carry errors in its
place. Fields bingo cannot fill are left out rather than invented —
uuid, mcp_servers, slash_commands, modelUsage, permission_denials,
the result line’s stop_reason. What is written but constant is written
honestly: apiKeySource is none, total_cost_usd is 0.0 because nothing
here prices a turn, duration_api_ms is 0 because only the whole turn is
timed, and a message’s usage is zero because bingo counts tokens per round
and reports the total once, in the result line.
The matching input direction, --input-format stream-json, is documented in
running headless.
For anything more than compatibility, --output-format json gives you the
frames themselves.
The journal format
A session on disk is its journal written down (ADR-0005). Line 1 is a header:
{"format":"bingo-journal","version":1,"session":"<id>"}
Every following line is one durable Frame exactly as it serialises, in seq
order, appended and flushed per frame, never rewritten. Ephemeral frames are
never written.
Three rules follow. A torn last line — a crash mid-write — is dropped, and
replay ends at the last whole frame. An unreadable line anywhere else is a
STORAGE error naming the line: corruption is reported, not skipped. And a
header version newer than the reader’s is refused, because version 1 is never
edited in place. A format change is a new version and a migrator that re-folds.
The two files beside it are .lock, the only claim of ownership, and
summary.json, which is derived — a missing one is rebuilt from the journal,
and deleting every one loses nothing but the speed of list.