Skip to content

protocol

@alexkroman1/aai/protocol — the WebSocket wire contract both ends of a session derive.

A FACADE. The subpath resolves here rather than at protocol.ts, which buys two things the direct form could not. That module can be SPLIT as it grows without moving the published entry point — the path an implementation file happens to have is not a thing to promise anyone — and a name it gains next reaches the public surface only when a line is added below, rather than the moment it is written.

Named re-exports rather than export * for the second half of that: the wildcard form re-exports whatever arrives, and needs a noReExportAll suppression the escape-hatch ratchet only lets move down.

buildClientConfig(source): object

Build the GET /client-config response body from an agent-shaped config.

Every server that serves the endpoint (a self-hosted createRuntimeServer, the platform’s per-slug handler, the CLI dev server) goes through this, so a surface rule can’t drift between them.

string

string

"voice" | "static"

string

object

optional greeting?: string

optional name?: string

page: "voice" | "static"

optional sessionUrl?: string


buildReadyConfig(s2sConfig): object

Build the protocol-level session config (the config frame’s audio fields) from the session’s input/output sample rates — used by every session mode, pipeline and S2S alike.

number

number

object

audioFormat: "pcm16"

sampleRate: number

ttsSampleRate: number


lenientParse<T>(schema, json, knownTypes?): { data: T; ok: true; } | { error: string; malformed: boolean; ok: false; }

Two-phase message parse: tries the strict schema first, then falls back to the envelope to distinguish unknown-but-valid types (safe to ignore during rolling upgrades) from genuinely malformed messages.

Return value when ok: false:

  • malformed: true — message doesn’t have a { type: string } shape (likely corrupt data), OR its type is one of knownTypes but it still failed strict validation (e.g. a tool_result missing toolCallId); both should warn
  • malformed: false — has a valid type field whose value is unrecognised; safe to ignore (e.g. new message type from a newer server version)

Passing knownTypes is what separates “unknown newer-version type” from “known type that failed validation” — without it, an invalid known message is silently swallowed as if it were a forward-compat unknown type. When parsing client→server messages, pass SESSION_COMMAND_TYPES as knownTypes.

T

ZodType<T>

unknown

ReadonlySet<string>

{ data: T; ok: true; } | { error: string; malformed: boolean; ok: false; }

Typed interface for pushing session events to a connected client.

Events send JSON text frames; audio chunks (playAudioChunk) send raw PCM16 binary frames. There is no playAudioDone — the turn’s audio.completed is an ordinary event now, and the sink orders it behind held audio by type. That is what let it join the retained stream: a method on the sink was a frame no event log could see.

optional close(reason?): void

Close the underlying connection (best-effort, idempotent). Used when the server retires a session out from under a connected client — a resume takeover, or a sandbox teardown — so the client gets a real close to react to instead of a socket that silently stops answering.

string

void

event(event): void

Push a session event (JSON text frame) to the client.

Takes an ALREADY-STAMPED SessionEvent: the envelope is minted once, by the session’s emitter, which is also what appends the event to the retained stream. A sink that stamped its own would mint a second id for an event the stream had already recorded under another.

SessionEvent

void

playAudioChunk(chunk): void

Send a single PCM16 audio chunk (raw binary frame) to the client.

Uint8Array

void

readonly open: boolean

True when the underlying connection is open and will accept calls.

HostConfig = z.infer<typeof HostConfigSchema>

Host-provided agent configuration for a host-mode connection.


ReadyConfig = z.infer<typeof ReadyConfigSchema>

Protocol-level session config returned to the client on connect.


RestoredToolCall = z.infer<typeof RestoredToolCallSchema>

One tool call as a resume reports it.

Public for the reason its schema is: it rides the wire inside history.restored, so a custom client reads exactly this shape. It was tagged @internal while the schema it is inferred from was not, which went unseen until /protocol joined the contract system.


SessionCommand = z.infer<typeof SessionCommandSchema>

Client→server text messages (binary frames carry raw PCM16 audio).

Note there is no history command any more. A reconnecting client used to push its own messages back because the server kept no record; the server is authoritative now and a resume reads the retained event stream by index. See “One durable session event stream” in the SDK guide.


SessionErrorCode = z.infer<typeof SessionErrorCodeSchema>

Error codes for categorizing session errors on the wire.

The field a client renders its error banner from (error.reported.code, and SessionError.code in @alexkroman1/aai-ui). Eight values, by where the failure came from:

  • stt — speech-to-text: the provider refused the connection, or its stream failed mid-utterance.
  • llm — the model call for a reply failed. In pipeline mode the caller also hears errorPhrase, so the turn is handed back rather than going silent.
  • tts — synthesis failed, which is the one the caller cannot hear.
  • tool — a tool threw and the failure could not be given to the model.
  • protocol — a frame that does not parse, or one sent in a state that has no answer for it.
  • connection — the session’s own link, or a provider’s, went away.
  • audio — the audio path: a rate the transport cannot honour, a decode.
  • internal — anything the runtime could not classify.

Severity is fatal, not the code, and the two are independent: any of these can arrive on a session that continues. fatal: false means surface the message and keep the session interactive. It is REQUIRED: a fatal frame is not a banner — aai-ui answers one by releasing the microphone and ending the call — so every emitter states which it means rather than inheriting a default that takes the whole session down.


SessionEventMeta = z.infer<typeof SessionEventMetaSchema>

The envelope every session event carries.

const CLIENT_CONFIG_METHODS: readonly string[]

The only method the endpoint answers — read by the host’s route dispatch, so this is the value and not a description of it.

Beside the path for the same reason the path is exported at all: the platform proxies this route, and aai-server’s GUEST_ROUTE_EXPOSURE has to name the verbs the guest answers. A hardcoded "GET" on that side would be a second source of truth for a one-word fact, which is the shape that rots — see WORKFLOW_API_METHODS on @alexkroman1/aai-runtime for the same rule on the workflow route, where it has already cost two incidents.


const CLIENT_CONFIG_PATH: "client-config" = "client-config"

Relative path of the client-config endpoint under an agent’s base URL.


const EVENT_ID_PREFIX: "evt_" = "evt_"

The prefix every session-event id carries, so an id names its own kind.

evt_ then a ULID — see SessionEventMeta and its id field for what the id is and is not good for. The link names the TYPE rather than the field because the type is z.inferred, so TypeDoc documents it as an anonymous object and has no anchor to point a member link at.


const HostConfigMessageSchema: z.ZodObject<{ audioFormat: z.ZodOptional<z.ZodEnum<{ pcm16: "pcm16"; }>>; host: z.ZodObject<{ audioLeadMs: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>; credentials: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>; greeting: z.ZodOptional<z.ZodString>; sttPrompt: z.ZodOptional<z.ZodString>; systemPrompt: z.ZodString; tools: z.ZodArray<z.ZodObject<{ description: z.ZodString; messages: z.ZodOptional<z.ZodObject<{ complete: z.ZodOptional<…>; delayed: z.ZodOptional<…>; failed: z.ZodOptional<…>; start: z.ZodOptional<…>; }, z.core.$strip>>; name: z.ZodString; parameters: z.ZodRecord<z.ZodString, z.ZodUnknown>; type: z.ZodLiteral<"function">; }, z.core.$strip>>; }, z.core.$strip>; sampleRate: z.ZodOptional<z.ZodNumber>; ttsSampleRate: z.ZodOptional<z.ZodNumber>; type: z.ZodLiteral<"config">; }, z.core.$strip>

The host-mode handshake frame: the first inbound message on a host-mode WebSocket connection, carrying the HostConfigSchema payload.

A host-mode client sends a single config frame that also carries the audio negotiation fields (audioFormat/sampleRate/ttsSampleRate) alongside host; they are captured here (optional) so the host-mode handshake can honor the client’s requested sample rates instead of discarding them.


const HostConfigSchema: z.ZodObject<{ audioLeadMs: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>; credentials: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>; greeting: z.ZodOptional<z.ZodString>; sttPrompt: z.ZodOptional<z.ZodString>; systemPrompt: z.ZodString; tools: z.ZodArray<z.ZodObject<{ description: z.ZodString; messages: z.ZodOptional<z.ZodObject<{ complete: z.ZodOptional<z.ZodArray<z.ZodObject<…, …>>>; delayed: z.ZodOptional<z.ZodArray<z.ZodObject<…, …>>>; failed: z.ZodOptional<z.ZodArray<z.ZodObject<…, …>>>; start: z.ZodOptional<z.ZodArray<z.ZodObject<…, …>>>; }, z.core.$strip>>; name: z.ZodString; parameters: z.ZodRecord<z.ZodString, z.ZodUnknown>; type: z.ZodLiteral<"function">; }, z.core.$strip>>; }, z.core.$strip>

Host-provided agent configuration for a host-mode connection: the caller (e.g. an external evaluation harness) supplies the system prompt, optional greeting, and tool schemas for a single session instead of using a deployed agent.

Validated standalone rather than as a SessionCommandSchema member — the host-mode handshake consumes this message before wireSessionSocket attaches, so it must never reach dispatchMessage/SessionCommandSchema.


const ReadyConfigSchema: z.ZodObject<{ audioFormat: z.ZodEnum<{ pcm16: "pcm16"; }>; sampleRate: z.ZodNumber; ttsSampleRate: z.ZodNumber; }, z.core.$strip>

Zod schema for ReadyConfig.


const RestoredToolCallSchema: z.ZodObject<{ afterMessageIndex: z.ZodNumber; args: z.ZodRecord<z.ZodString, z.ZodUnknown>; callId: z.ZodString; name: z.ZodString; result: z.ZodOptional<z.ZodString>; status: z.ZodEnum<{ done: "done"; pending: "pending"; }>; }, z.core.$strip>

One tool call as a RESUME reports it — see history.restored.

Its own schema because the host builds these (historyFromEvents) and the client reads them, so the shape wants one name on both sides. It is deliberately NOT tool.called plus tool.completed: those are two live events, and what a restore sends is their settled JOIN.


const SESSION_COMMAND_TYPES: ReadonlySet<string>

The set of recognised client→server command type values — pass to lenientParse so a known-but-invalid message warns instead of being silently dropped as an unknown forward-compat type.


const SESSION_EVENT_TYPES: ReadonlySet<string>

Every event name, as a set — for lenientParse’s known-types argument.


const SessionCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ type: z.ZodLiteral<"audio_ready">; }, z.core.$strip>, z.ZodObject<{ type: z.ZodLiteral<"cancel">; }, z.core.$strip>, z.ZodObject<{ type: z.ZodLiteral<"reset">; }, z.core.$strip>, z.ZodObject<{ type: z.ZodLiteral<"user_turn_start">; }, z.core.$strip>, z.ZodObject<{ type: z.ZodLiteral<"user_turn_commit">; }, z.core.$strip>, z.ZodObject<{ type: z.ZodLiteral<"user_turn_clear">; }, z.core.$strip>, z.ZodObject<{ bufferedMs: z.ZodNumber; type: z.ZodLiteral<"playback_progress">; }, z.core.$strip>, z.ZodObject<{ error: z.ZodOptional<z.ZodString>; result: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>; toolCallId: z.ZodString; type: z.ZodLiteral<"tool_result">; }, z.core.$strip>], "type">

Zod schema for SessionCommand.


const SessionErrorCodeSchema: z.ZodEnum<{ audio: "audio"; connection: "connection"; internal: "internal"; llm: "llm"; protocol: "protocol"; stt: "stt"; tool: "tool"; tts: "tts"; }>

Zod schema for session error codes.


const SessionEventMetaSchema: z.ZodObject<{ at: z.ZodNumber; id: z.ZodString; }, z.core.$strip>

Zod schema for SessionEventMeta.

Re-exports ClientConfigResponse


Re-exports ClientConfigResponseSchema