pinecallDocs
The wire / The wire

The wire

Every entry the gateway writes and every command an app sends, held to the runtime's code by its suite.

The seam between whoever does the real time and the application: every entry the gateway writes and every command an app sends. It is the runtime's: pinecall/wire/ holds each one as a pydantic model, and these pages describe them field by field. A rule of the runtime's suite (tests/rules/test_21_wire_pages.py) holds the two together: an event, a command or a shape the code has and a page does not, a field on one side only, a type or a required flag that differ, each fails the suite by name. The reference, page by page, is listed at the end.

One vocabulary, one seq#

The envelope of the log is the event. What the gateway streams to a subscriber and what it stores are the same bytes:

JSON
{ "seq": 29, "ts": 1786537584.38, "call": "CA_8f4a", "agent": "clinica-norte",
  "type": "confirm.granted", "ephemeral": false,
  "data": { "tool": "book_slot", "call_id": "toolu_02", "audience": "sha256:…", "said": "sí", "ttl_s": 120 } }
field meaning
seq the entry's place in its log, from 1, written before control returns. The cursor.
ts unix seconds, fractional
call the call id, or null for an entry about the agent itself
agent the agent's slug
type the event, dotted; the closed list is below
ephemeral a store may drop it and a slow reader may miss it: interim transcripts, VAD health, replay markers
data the event's payload, whose shape is the event's own, in Events

snake_case on the wire. A key name is touched in one place per language: in Python the one alias (from → from_), in the TypeScript SDK toCamel/toSnake for whoever wants camelCase, never under the app's own JSON (state, arguments, output, data, …).

A command is the frame the app sends: { "type", "agent", "call", "id"?, "data" }. The gateway answers with the events the command lands as, or with error naming the id.

Every turn carries every metric#

livekit-agents 1.8 measures a great deal per turn, and all of it travels under its own field names. turn.user and turn.agent carry the ChatMessage metrics report split by role; each typed block the session emits (LLMMetrics, STTMetrics, TTSMetrics, VADMetrics, EOUMetrics, EOTInferenceMetrics, InterruptionMetrics, RealtimeModelMetrics, AvatarMetrics) is its own metrics.<block> entry with every field, joined to the turn by speech_id; call.summary carries the per-model usage rows the session sums and the provider cost in US dollars, row by row. A text session fills what it can measure; an absent field is absent, never zero. The full table is at the end.

The join: livekit gives the caller's finished turn, the end-of-turn decision and the reply that follows one speech_id. So turn.user, metrics.eou, metrics.llm, metrics.tts and turn.agent for one exchange all carry the same id. metrics.stt, metrics.vad, metrics.eot and metrics.interruption have no speech_id in the library; they belong to the call. metrics.vad is ephemeral by default because it is a once-a-second health tick of the detector, not a measurement of a turn: a live reader sees it, a store may drop it, and nothing about the turn is lost with it.

The room and the outside environment#

A phone or web call lives in a LiveKit room, and the room's facts are entries like any other: room.opened, participant.joined (with livekit's participant attributes verbatim, the sip.* keys included — the caller's number is a fact of the room, not a field we invent), participant.left, participant.speaking (ephemeral), track.published, track.unpublished. The agent never touches LiveKit; it reads these, and acts on the room through commands: room.invite (a second SIP leg, the warm path), participant.mute, participant.remove, room.send (a payload to a browser, logged as room.sent with its size).

A fact from outside the conversation — the tenant's backend saying a slot was freed, a browser saying a form was submitted — is one event, event.received, told apart by source. It reaches the log only if the agent declared the name in its events with that source among the senders; the app's handler may then move the state, and state.changed says so with a cause of kind event. agent.reply makes the model speak to it. The page is events-room.md.

Writing a log#

A worker writes its call's entries in batches, POST /v1/calls/{id}/entries, and the answer is {entries}: each entry as the log numbered it, in the order it was sent. The body:

field meaning
after how many entries the log had taken from this worker on this call before this batch, from 0
entries 1 to 256 entries, each {type, data, ephemeral?, ts}: the protocol's own type, its data, ephemeral when the writer overrules the wire's default, and ts, the worker's clock when the event happened. The log keeps that ts, clamped to the gateway's clock and never earlier than the entry before it in the batch

The batch is taken whole or refused whole. Sent again with the same after, it is answered with the seqs and stamps it was given and nothing is written twice. POST /v1/calls/{id}/events takes one entry the same way and counts nothing: it is the endpoint of a worker of an older release, which retried it and could write an entry twice.

Reading a log#

Six endpoints, and two of them are the same URL twice — Accept decides:

endpoint gives
GET /v1/calls/{id}/events?after=<seq> the call's entries after the cursor as {entries, live, next} — next is the last seq the page READ, and null when the read came back empty; with Accept: text/event-stream, the same entries as SSE that stays open. Last-Event-ID is an alias of after, and a cursor at the end of a sealed log is 204 in both flavours
GET /v1/calls/{id}/state the whole log folded: {state, last_seq, live}, projected by what the caller is. last_seq is the cursor to open the stream at
GET /v1/agents/{slug}/calls?after=<seq> the agent's OWN log, in the same two flavours and the same {entries, live, next} page: agent.registered, agent.configured, error. Every entry of it has call: null — what happens inside a call is written into that call's log, so nothing on this stream names one. live is always true: an agent's log never ends
GET /v1/agents/{slug}/sessions?limit=<n> which calls that agent handled: {calls: [...]}, newest first, one row per call — call, live, last_seq, and the fields of the call's own reduced state that a list draws (status, channel, from, caller, started_at, ended_at, outcome, cost, …), through the same projection. It is the endpoint a list of calls is built from; the log above is not
GET /v1/agents which agents this key's fleet is holding right now: {agents: [{slug, channels}]}, in the order their sockets claimed them. An agent no socket holds answers no call, so it is not listed; its log is still readable by slug
POST /v1/calls/{id}/verbs one supervise verb onto a live call, with a supervise token or the org key. 202; its effect is read off the log above

A reader that only wants the app's state subscribes with types=state.changed: one entry per change instead of every transcript and every metric, on the same cursor, and GET .../state is the snapshot it starts from.

Reconnecting is the same URL with a fresher after. A reader that missed a stretch gets log.gap (with a snapshot of the state when the platform has one), then log.caught_up when the replay ends and what follows is live. A marker stands at the seq of the last entry it speaks for: the gap at to_seq, log.caught_up at the last seq sent. The cursor is the whole protocol. Which gateway wrote an entry, or which one a reader is connected to, changes none of it: every gateway hears every log something of its own reads, and hands each entry on in seq order.

Tokens#

token may
talk connect to ONE agent, once, for 60 s (web widget), and read its own call. Minted at POST /v1/tokens — LiveKit's standard token endpoint, in front of our organisation check: tokens.md. Its metadata is sealed by the tenant's server; the browser cannot forge it
chat talk with no microphone: the same room, and it SUBSCRIBES — LiveKit hands a text stream to subscribers only, so a token that could not would type into the room and never see the reply
observe hear the room, hidden and silent. It reads no log at all
supervise read the one call it was minted for, and send the verbs to POST /v1/calls/{id}/verbs
participate read its own call, through the public projection, and send pinecall.event

What each token sees is a projection, applied at the sink and never by a client: public for talk, chat and participate, tenant for observe and supervise. The contract is projections.md.

Verbs#

What a supervisor sends to POST /v1/calls/{id}/verbs, one per request. Each lands in the log as its own event.

verb does lands as
say {text} the agent says this, verbatim supervisor.said
whisper {text} the agent is told this; the caller never hears it supervisor.whispered
takeover the supervisor takes the line, the agent goes quiet supervisor.took_over
release the line goes back to the agent, history intact supervisor.released
transfer {to, mode} the caller is sent on, cold or warm supervisor.transferred, then call.transferred
end {reason?} hang up supervisor.ended, then call.ended

State#

State is what a log reduces to: fold every entry in seq order and that is what you hold. The runtime's reducer (pinecall/log/reduce.py) and the SDKs' must agree: TypeScript's and Ruby's are written by hand, Python's is a copy of this one. All of them agree, field for field, on the runtime's golden call log (tests/wire/golden/call-log.json) and the state it folds to (call-log.state.json); each SDK keeps a copy of both and a test that folds one into the other. The shape is in state.md.

The reference#

One page per domain. A change of the wire changes its page in the same commit, or the suite fails.

page holds
Events every event, one line each, with the page its data shape is on
Call events call.*, user.*, agent.state, agent.transcript, turn.*, metrics.*: the call and the conversation, field by field
App events tool.*, state.changed, prompt.changed, tools.changed, confirm.*, memory.ops, docs.sources, custom
Control events supervisor.*, log.gap, log.caught_up, error, pong, agent.registered, agent.configured
Room events room.*, participant.*, track.*, event.received: the room's facts and the outside environment's
Commands every command with what it lands as, and the supervise verbs
Metrics the metrics table: block · field · unit · required · measured by · meaning
State what a log reduces to
Shapes the envelope and the shared shapes
Projections what public and tenant keep, and the DataChannel topics
Operator API /v1/ops/*, the ops key, and the routes endpoints
Tokens POST /v1/tokens, LiveKit's token endpoint with the org check, the contact id and single use in front of it