Wire protocol
Introduction
Section titled “Introduction”@pilotoo/chat-protocol is the contract between a gateway and every client. Its Zod schemas are the single source of truth, and every transport and environment speaks it. You rarely write protocol messages by hand, because chat-core does it for you; read this page when you build your own client or debug a run.
The rule behind every schema: commands are strict, responses are permissive. A command rejects keys it does not know, because that is a client defect or an attack. A response tolerates extra fields, because a client embedded in a lagging system must not refuse a whole answer over a field it does not recognise.
Socket messages
Section titled “Socket messages”| Direction | Message | Payload |
|---|---|---|
| server to client | protocol:hello |
{ protocol, minClientMajor }, sent once per connection |
| client to server | run:start |
start command |
| client to server | run:resume |
resume command |
| client to server | run:cancel |
cancel command |
| client to server | run:detach |
detach command |
| client to server | thread:messages |
thread messages command; answered through the acknowledgement |
| server to client | run:event |
a stream envelope |
| server to client | task:status |
{ threadId, task } |
| server to client | protocol:error |
{ code, message } |
Commands
Section titled “Commands”| Command | Fields | Notes |
|---|---|---|
run:start |
threadId, messageId, input (at most 16,000 characters), attachments?, clientTools?, forkFromMessageId? |
Needs text or at least one attachment. With forkFromMessageId, the run restarts from the checkpoint just before that message, which is replaced by this run’s message. |
run:resume |
threadId, interruptId, answer, clientTools? |
answer is required. Answers one pending decision. |
run:cancel |
runId |
|
run:detach |
runId, toolCallId? |
Moves the run’s subagent task calls, or only toolCallId’s, to the background. |
thread:messages |
threadId |
Joins the thread’s room and returns history. |
Limits: at most 8 attachments per run (MAX_ATTACHMENTS_PER_RUN), each an image of image/png, image/jpeg, image/webp or image/gif up to 5 MiB (ATTACHMENT_MEDIA_TYPES, ATTACHMENT_MAX_BYTES), and at most 32 client tools per command.
Client tools
Section titled “Client tools”clientTools appears on run:start and run:resume only. Each declaration has a name (letters, digits, _ and -, up to 64 characters), a description, an arguments JSON Schema, and a kind:
blocking: the run pauses and the client’s answer becomes the tool result. May declare aresultschema.one-way: drawn or acted on while the run carries on; returns nothing.
Nothing about client tools is stored; the next run declares its own set. A blocking call arrives as an INTERRUPT whose payload is { clientTool, arguments } and is answered with run:resume. See Client tools.
Events
Section titled “Events”Every event travels inside an envelope. These are the event types, from agUiEventSchema:
| Group | Types |
|---|---|
| Run lifecycle | RUN_STARTED, RUN_FINISHED, RUN_CANCELLED, RUN_ERROR |
| Text | TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END |
| Tool calls | TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END, TOOL_CALL_RESULT |
| Human decisions | INTERRUPT, RUN_PAUSED |
| Reasoning | REASONING_START, REASONING_CONTENT, REASONING_END |
| Plans | TODOS_SNAPSHOT |
| Subagents | SUBAGENT_STARTED, SUBAGENT_FINISHED |
Points worth knowing:
RUN_CANCELLEDis separate fromRUN_ERROR: a deliberate stop is not a fault. Whatever had streamed stays in the transcript.INTERRUPTcarries aninterruptIdand apayload, and is always followed byRUN_PAUSED, which ends the segment. A graph that pauses on several decisions emits oneINTERRUPTeach, and each is answered by its ownrun:resume.RUN_PAUSEDis then followed byRUN_FINISHEDfor the same run id, so older clients still see the run settle.TODOS_SNAPSHOTis the whole list each time, so a reader who missed one is whole again at the next.SUBAGENT_STARTEDmay carrytaskIdandchildThreadId;SUBAGENT_FINISHEDcarriesstatus(finishedorerror) anddetached: truewhen the subagent goes on as a background task.RUN_ERRORcarries a freecodeand amessage.RUN_STARTEDmay carry themessagethat opened the run, so a window that joins mid-run shows the question.
Envelopes
Section titled “Envelopes”{ v: 1, // PROTOCOL_VERSION.major threadId: string, // on every envelope, so one is routable on its own runId: string, sequence: number, // positive integer, increasing within the run event: { type: ..., ... },}Naming the thread on every envelope is what lets a second window, or a reconnect, render a run it did not start. The gateway broadcasts to a room keyed on the thread, which a socket joins by asking for that thread’s history. RUN_STARTED is a run’s sequence 1.
An event type a client has no case for is admitted and ignored (counted, not refused), so the gateway can learn to say one more thing without breaking old clients. A known type that fails validation is still an error.
Thread history and replay
Section titled “Thread history and replay”thread:messages answers with ok: true and the thread’s history, or ok: false and an error:
| Field (ok) | Meaning |
|---|---|
messages |
The checkpointed messages, each with id, role, content, createdAt and optional toolCalls, reasoning, attachments, task. |
replay |
Present while a run is in flight: its envelopes as the gateway holds them, and truncated, which says the buffer dropped the beginning. |
pendingInterrupt |
The first decision the thread waits on, if any. |
todos |
The latest todo list, if the agent keeps one. |
task |
Present when the thread is a background task’s own conversation. |
The replay is the envelopes themselves, so a client folds them through the same path as the live stream, and the last of them is contiguous with the next envelope the room delivers. A reader who reloaded has no sequence to ask from, so the server offers what it holds rather than answering a bookmark.
Errors
Section titled “Errors”protocol:error and the ok: false branch carry { code, message }:
| Code | Meaning |
|---|---|
INVALID_COMMAND |
The command failed validation, or cannot be accepted now. |
UNKNOWN_RUN |
The run is not active, or the conversation is not waiting on that decision. |
DUPLICATE_MESSAGE |
A message with this id already exists in the thread. |
CONVERSATION_NOT_FOUND |
The conversation does not exist. |
FORK_POINT_NOT_FOUND |
The message to fork from is not in the thread. |
Run failures are a different channel: they arrive as the RUN_ERROR event.
HTTP surface
Section titled “HTTP surface”The same server also exposes REST routes (all authenticated unless stated): /health and /protocol (anonymous), /conversations, /agents, /agent-admin, /attachments, /presentations, /usage, /schedules and /model-gateway. GET /protocol returns the same object as protocol:hello.
Versioning
Section titled “Versioning”PROTOCOL_VERSION is { major, minor } (currently { major: 1, minor: 17 }). major is the envelope’s v; minor counts additions.
| Change | Effect |
|---|---|
| Add an event type, an optional response field or an optional command field | Increment minor. Every rule above already makes it tolerable in both directions. A client follows a gateway with a higher minor unchanged. |
| Change a field’s meaning, remove a required field, or change the sequencing contract | Increment major, and minor returns to 0. The gateway must serve both versions while any client remains that cannot rebuild. |
The gateway announces what it needs with protocol:hello { protocol, minClientMajor }, emitted to each socket as soon as the connection is identified, and with the same object at GET /protocol (anonymous). minClientMajor is the oldest major the gateway still serves; it rises only when the gateway stops serving a major, never with an addition.
chat-core sends nothing before the announcement. If minClientMajor exceeds its own major, or the gateway’s major differs from its own, the session stops with an error naming both versions and sends no further commands, not even after a reconnect. A gateway that announces nothing within protocolGraceMs (2 seconds) predates the announcement: it is followed as compatible, and the absence is logged with console.warn, as is an unreadable announcement.
Next steps
Section titled “Next steps”- The life of a run shows these messages in order.
- Connecting a client covers the transport and credentials.
- Client tools explains
clientToolsand blocking calls.