smooth-operator

Reference

Protocol

One WebSocket, 9 client actions, 17 server events. Every frame and field on this page is read out of the JSON Schemas that the clients generate from, so this page cannot describe a message the spec does not define.

Derived from sourceHand-written noteCaveat / known gap

Every teal entry on this page is generated from spec/{actions,events,domain}/*.schema.json at 158c9b4c, not from prose about it. Regenerate with pnpm --filter @smooai/web gen:operator-reference.

The envelope

Every frame is a JSON object. Client frames carry an action; server frames carry a type. You choose requestId, and the server echoes it on every frame belonging to that request — that correlation is what lets one socket carry several turns at once.

frames on the wire
// Client → server. `action` selects the handler; `requestId` is yours,
// and every server frame for this request echoes it back.
{ "action": "send_message", "requestId": "01J...", "sessionId": "…", "message": "…" }

// Server → client. `type` identifies the frame; `status` is HTTP-like:
// 202 accepted, 200 final, 4xx/5xx error.
{ "type": "immediate_response", "requestId": "01J...", "status": 202 }
{ "type": "stream_token",       "requestId": "01J...", "token": "Ret" }
{ "type": "eventual_response",  "requestId": "01J...", "status": 200, "data": { … } }

A turn is: immediate_response 202, then zero or more streaming frames, then exactly one terminal frame — eventual_response 200, cancelled, or error. Anything that needs the person — a write approval, an OTP, a structured question — arrives in between and parks the turn until you answer it.

The envelope enum is behind the spec

The envelope schema repeats the action and type lists by hand, alongside the per-message schema files that define them. Those two disagree today, and this section is computed from the difference rather than written down — it disappears on its own when the enums are fixed.

4 messages have a schema file but no entry in envelope.schema.json:

cancelcancelledstream_preamblestream_reasoning

Read the per-message schemas, which are what this page lists. A code generator or validator pointed at the envelope’s enums will reject frames that real servers send and accept.

Actions

Client to server. Required fields are teal and marked with an asterisk.

Client-initiated cancellation of the connection's in-flight agent turn (the "Stop button"). Aborts the running send_message turn: the server drops the turn future at its next await point, abandoning the in-flight LLM/tool call, and replies with a terminal cancelled event. Correlation convention: send the SAME requestId as the send_message being cancelled, so the cancelled event echoes it. A connection runs at most ONE turn at a time; a cancel with no active turn is a harmless no-op that emits nothing.

Dispatched by the Rust server, and present as its own schema file — but missing from the envelope schema’s hand-maintained action enum. Generators that read only the envelope will not know this verb exists.

FieldTypeMeaning
action*"cancel"Action discriminator.
requestIdstringThe requestId of the in-flight send_message turn to cancel. Echoed back on the cancelled event so the client correlates the reset.
sessionIdstringOptional, advisory. The server cancels the connection's single active turn; a per-connection socket carries one turn at a time.

Sent in response to a write_confirmation_required event. Resumes the paused agent workflow with the user's approval or rejection decision. Approved calls proceed; rejected calls are skipped and the agent receives the rejection as context.

FieldTypeMeaning
action*"confirm_tool_action"Action discriminator.
requestId*stringMust match the requestId from the write_confirmation_required event being responded to. This is how the server correlates the confirmation back to the paused workflow.
sessionId*stringSession ID of the paused session.
approved*booleanTrue to allow the tool call to proceed; false to reject it. On rejection the agent workflow resumes with an informational context message.

Starts a new AI conversation session for the given agent. On success the server creates a conversation, two participants (user + agent), and a session record, then replies with an immediate_response event whose data matches the Response schema below.

FieldTypeMeaning
action*"create_conversation_session"Action discriminator.
requestIdstringClient-generated correlation ID echoed back on all related events.
agentId*stringUUID of the agent to start a session with.
userNamestringOptional display name for the user participant.
userEmailstringOptional email address for the user participant.
browserFingerprintstringBrowser fingerprint string (e.g. from ThumbmarkJS) used for anonymous user correlation across sessions.
supportsstring[]Client render capabilities for this session — a per-kind list gating the Rich Interactions the server may emit mid-turn (interaction_required). Each interaction kind declares the capability that gates it (e.g. kind identity_intake → capability identity_form, kind choices → capability choice_chips); future kinds add their own values (date_picker, file_upload, …). Text-only channels (SMS, voice) declare [] and the server degrades each kind to its conversational fallback. Unknown values are ignored (forward-compatible). Durability: the declared list is persisted on the CONVERSATION, so a reconnect that resumes an existing conversationId and OMITS this key inherits the set the conversation last declared — a reconnect is not a downgrade to text-only. Any list the frame does declare (including []) replaces the inherited one, so a text-only client resuming a rich conversation opts out explicitly.
metadataobjectArbitrary key/value metadata to attach to the session.
authContextobjectPre-auth context for HMAC-based identity verification. When provided, the server can skip OTP flows by verifying the HMAC signature instead.

get_conversation_messages

actions/get-messages.schema.json

Retrieves a page of messages from the conversation attached to the given session. Results are ordered newest-first. Page by feeding a response's nextCursor back as the next request's cursor. The server replies with an immediate_response event whose data matches the Response schema below.

FieldTypeMeaning
action*"get_conversation_messages"Action discriminator.
requestIdstringClient-generated correlation ID echoed back on all related events.
sessionId*stringSession ID whose conversation messages to fetch.
limitinteger
default 50
Maximum number of messages to return per page. Must be 1–100; defaults to 50.
cursorstringOpaque pagination cursor from a prior response's nextCursor. Returns only messages older than the one the cursor names. Treat it as opaque — its encoding is storage-defined (a message id today) and may change. Omit to start from the most recent message. Deliberately NOT a timestamp: two messages can share a timestamp at any precision the wire format keeps, so a created_at < cursor filter either drops or repeats the messages that collide. An id cursor identifies exactly one message and cannot.

Fetches a snapshot of an existing session by ID. The server replies with an immediate_response event whose data contains the session details. Useful for reconnects — the client can restore its local state without creating a new session.

FieldTypeMeaning
action*"get_session"Action discriminator.
requestIdstringClient-generated correlation ID echoed back on all related events.
sessionId*stringID of the session to retrieve.

Client-initiated keepalive. The server replies with a pong event carrying the server's current Unix timestamp in milliseconds. Clients should send a ping every 30 seconds (or at the configured heartbeatInterval) to prevent AWS API Gateway's 10-minute idle connection timeout.

FieldTypeMeaning
action*"ping"Action discriminator.
requestIdstringClient-generated correlation ID echoed back in the pong event.

Submits a user message to the active session. The server immediately acknowledges with immediate_response (status 202), then emits zero or more stream_chunk / stream_token events as the agent workflow runs, and finally emits eventual_response (status 200) with the terminal output. If the agent requires human confirmation mid-stream, a write_confirmation_required event is sent and processing pauses until the client sends confirm_tool_action.

The one action that runs an agent. Everything else on this page either sets up the session it needs or answers a question it raised mid-turn.

FieldTypeMeaning
action*"send_message"Action discriminator.
requestIdstringClient-generated correlation ID echoed back on all related events.
sessionId*stringSession ID returned by create_conversation_session.
message*stringThe user's message text. Between 1 and 10 000 characters.
streamboolean
default true
Whether to receive incremental stream_chunk and stream_token events. Defaults to true. Set to false to receive only the final eventual_response.
modelstringOptional gateway model id to run THIS turn on (e.g. a /smooth-mode preset). Absent → the server's configured default model.
skillstringOptional name of a skill (a reusable recipe) to run THIS turn under. The SERVER resolves the name to the skill's markdown body and composes it into the turn's system prompt, so the wire carries the intent ("use skill X") rather than the client prepending the skill's prose to message — and the persisted user message stays exactly what the user typed. Absent → an ordinary turn (byte-identical to before this field existed). Fail-closed, unlike images: a skill the server cannot resolve returns a SKILL_NOT_FOUND error and the turn does NOT run, since silently answering without the requested recipe is indistinguishable from answering with it.
imagesobject[]Optional image attachments for a multimodal turn. Each item is a data: or https image URL with an optional OpenAI vision detail hint. Absent/empty → a text-only turn (byte-identical to before this field existed). Fail-soft: a malformed entry is ignored rather than rejecting the turn.
filesobject[]Optional non-image file attachments for this turn. Unlike images (which are sent to the model as vision content parts), each file is surfaced to the host on the tool-provider context so the host can persist it into the agent's workspace, where ordinary tools (read_file, bash, …) can then read it. The protocol layer does NOT send file bytes to the model. Absent/empty → no files (byte-identical to before this field existed). Fail-soft: a malformed entry is ignored rather than rejecting the turn.

The single Rich Interactions resume verb: sent in response to an interaction_required event with the visitor's values for that interaction kind — or with declined: true when the visitor refuses (the agent handles the decline gracefully). The server routes values to the kind's validator; invalid values emit interaction_invalid and the turn stays parked for a resubmit. Valid values (or a decline) resume the turn: the agent's raise tool returns the kind's canonical validated payload. Because this one verb serves every interaction kind, adding a new kind requires no new protocol action and no client-library release.

FieldTypeMeaning
action*"submit_interaction"Action discriminator.
requestId*stringMust match the requestId from the interaction_required event being responded to.
sessionId*stringSession ID of the parked session.
interactionId*stringMust match the interactionId from the interaction_required event, so a stale submit can never resolve a newer park.
kindstringOptional interaction kind, for cross-checking; the server already knows the parked interaction's kind. When present and mismatched, the submit is rejected.
valuesobjectKind-specific submitted values. Required unless declined is true. Shape per interactions/<kind>.schema.json#/$defs/Values (e.g. identity_intake's { name?, email?, phone? }). Validated server-side by the kind's validator.
declinedbooleanTrue when the visitor refused the interaction. The turn resumes with a declined payload so the agent can proceed gracefully. When true, values is ignored.

Sent after the user receives an OTP code (delivered via a channel indicated in otp_verification_required) and enters it. The server validates the code and either resumes the agent workflow (success → otp_verified event + resumed stream) or rejects it (otp_invalid event with remaining attempt count). On max attempts exhausted the session is locked and a new otp_sent / otp_verification_required flow must be started.

FieldTypeMeaning
action*"verify_otp"Action discriminator.
requestId*stringMust match the requestId from the otp_verification_required event being responded to.
sessionId*stringSession ID of the paused session.
code*stringThe one-time password code entered by the user.

Events

Server to client. Every one of these can arrive unsolicited on a socket that has a turn in flight.

The terminal event of a turn the client aborted with a cancel action — emitted IN PLACE OF the eventual_response a completed turn would send. Echoes the cancelled send_message's requestId so the client correlates it to the in-flight turn and resets its UI (drop the streaming indicator, re-enable input). Status 499 ("client closed request") marks a terminal, non-200 outcome distinct from a server error. There is NO answer payload: a cancelled turn produced no assistant message — the streamed tokens were ephemeral and are NOT persisted, while the user's message stays persisted (so the conversation carries the user turn with no reply). Only emitted when a live turn was actually aborted; a cancel with no active turn emits nothing.

FieldTypeMeaning
type*"cancelled"Event type discriminator.
requestIdstringEchoes the requestId of the cancelled send_message turn (falls back to the cancel frame's own requestId). Absent only if neither carried one.
status*"499"Terminal cancellation status ("client closed request"). Distinct from 200 (eventual_response) and from error codes.
timestampintegerServer-side Unix epoch milliseconds when the event was emitted.
dataobjectCancellation payload (mirrors requestId + status for clients that only inspect data).

Emitted when an unrecoverable error occurs during request processing. The nested error object shape ({ code, message }) is preserved for wire compatibility with clients that destructure message.error.code. details carries additional structured context when available.

FieldTypeMeaning
type*"error"Event type discriminator.
requestIdstringEchoes the requestId from the originating action, if applicable. Absent for server-initiated errors with no associated request.
errorobjectTop-level error object (duplicate of data.error; kept for clients that pattern-match on the envelope-level error field).
data*objectFull error payload.
timestampintegerUnix epoch milliseconds when the event was emitted.

The terminal event of a streaming turn. Emitted after the agent workflow completes and its output has been persisted. Clients should treat this as the authoritative final state for the turn and may discard intermediate stream_chunk / stream_token data. Status is always 200 on success.

The authoritative end of a turn. A client that trusts accumulated tokens instead of this frame will disagree with the database.

FieldTypeMeaning
type*"eventual_response"Event type discriminator.
requestIdstringEchoes the requestId from the originating send_message action.
statusintegerHTTP-like status. Always 200 for a successful eventual response.
data*objectThe terminal response payload.
timestampintegerUnix epoch milliseconds when the event was emitted.

Sent by the server synchronously upon receiving any action, to acknowledge that the request was accepted and processing has begun. For streaming actions (send_message) this always precedes stream_chunk / stream_token events and the final eventual_response. For non-streaming actions (e.g. get_session, create_conversation_session) this also carries the complete response payload in data.

FieldTypeMeaning
type*"immediate_response"Event type discriminator.
requestIdstringEchoes the requestId from the originating action.
statusintegerHTTP-like status. 202 = accepted and processing; 200 = synchronous success (non-streaming responses).
messagestringHuman-readable status description (e.g. Processing your request...).
data*objectAction-specific response payload. For create_conversation_session and get_session, this is the session descriptor. For get_conversation_messages, this is the message page. For streaming send_message, this is typically empty or contains only a minimal ack.
timestampintegerUnix epoch milliseconds when the event was emitted.

Emitted when a submit_interaction action carried values that failed the kind's server-side validation. The turn REMAINS parked — the client should re-render the interaction card with the per-field errors and let the visitor resubmit. Mirrors otp_invalid: invalid input is a retryable state, never a terminal error event.

FieldTypeMeaning
type*"interaction_invalid"Event type discriminator.
requestIdstringEchoes the requestId of the parked turn (same correlation as the interaction_required event).
data*objectValidation failure details.
timestampintegerUnix epoch milliseconds when the event was emitted.

The Rich Interactions envelope: emitted mid-turn when the agent requests a structured interaction (identity intake, a date picker, choice chips, …) and the session declared the interaction kind's render capability in supports at create_conversation_session. The turn is parked until the client replies with a submit_interaction action carrying the same requestId + interactionId (values or declined: true). Sessions without the capability never receive this event — the server degrades that kind to its conversational fallback instead. kind selects the client card and the server validator; spec is the kind-specific payload whose shape is defined by interactions/<kind>.schema.json (e.g. interactions/identity-intake.schema.json#/$defs/Spec).

The generic mid-turn question. One kind is registered per structured question shape; the client answers with submit_interaction.

FieldTypeMeaning
type*"interaction_required"Event type discriminator.
requestIdstringEchoes the requestId from the originating send_message action. Must be included in the submit_interaction reply.
data*objectInteraction prompt details.
timestampintegerUnix epoch milliseconds when the event was emitted.

Sent periodically by the server during long-running agent turns (typically every 30 seconds) to prevent AWS API Gateway's 10-minute idle connection timeout from closing the WebSocket while the backend is still computing. Clients should acknowledge receipt by updating their last-seen timestamp, but no reply action is needed. Distinct from ping/pong which are client-initiated.

Specified, and a generated type exists in the Go client — but no server implementation in the repository emits it. Written against the spec, not against a running server. Do not build a liveness assumption on it; use ping / pong.

FieldTypeMeaning
type*"keepalive"Event type discriminator.
requestIdstringThe requestId of the in-flight request this keepalive is associated with.
data*objectKeepalive payload.
timestampintegerUnix epoch milliseconds when the keepalive was sent.

Emitted when the caller's OTP attempt is rejected — wrong code, expired, max attempts reached, or record not found. When attemptsRemaining is 0 the session is locked; the client must restart the OTP flow. When greater than 0 the client may prompt the user to try again.

FieldTypeMeaning
type*"otp_invalid"Event type discriminator.
requestIdstringEchoes the requestId from the originating verify_otp action.
data*objectFailure details.
timestampintegerUnix epoch milliseconds when the event was emitted.

Acknowledgement that an OTP code has been dispatched to the user via the chosen delivery channel. The client should update the UI to prompt the user to enter the code they received.

FieldTypeMeaning
type*"otp_sent"Event type discriminator.
requestIdstringEchoes the requestId from the originating action.
data*objectOTP send acknowledgement details.
timestampintegerUnix epoch milliseconds when the event was emitted.

Emitted when the agent workflow pauses because it needs the caller to complete OTP verification before proceeding with an authenticated action. Corresponds to smooth-operator's AgentEvent::HumanInputRequired { Input } for auth gates. The client should surface a channel-selection and OTP input UI. After the user selects a channel, the client may trigger OTP delivery via a separate flow; the verify_otp action submits the received code.

FieldTypeMeaning
type*"otp_verification_required"Event type discriminator.
requestIdstringEchoes the requestId from the originating send_message action. Must be included in the verify_otp reply.
data*objectVerification prompt details.
timestampintegerUnix epoch milliseconds when the event was emitted.

Emitted when the caller's OTP attempt succeeds, or when a pre-auth HMAC makes OTP unnecessary. The session is now authenticated at the required level and the paused agent workflow resumes. A streaming sequence (stream_chunk / stream_tokeneventual_response) follows.

FieldTypeMeaning
type*"otp_verified"Event type discriminator.
requestIdstringEchoes the requestId from the originating verify_otp action.
data*objectVerification success details.
timestampintegerUnix epoch milliseconds when the event was emitted.

The server's reply to a ping action. Carries the server's current Unix epoch timestamp in milliseconds. Clients use the round-trip time to detect zombie connections.

FieldTypeMeaning
type*"pong"Event type discriminator.
requestIdstringEchoes the requestId from the originating ping action.
timestampintegerServer-side Unix epoch milliseconds when the pong was emitted.
dataobjectPong payload (mirrors top-level timestamp for clients that only inspect data).

Emitted each time a node in the smooth-operator workflow completes. Carries the node name and a filtered state snapshot. Clients use this to show per-node progress (e.g. knowledge_search completed, tool activity) in an agent team view. Distinct from stream_token which carries raw token deltas; a stream_chunk typically fires once per node while multiple stream_token events may fire within a single node's LLM call.

FieldTypeMeaning
type*"stream_chunk"Event type discriminator.
requestIdstringEchoes the requestId from the originating send_message action.
nodestringName of the workflow node that just completed and produced this chunk.
data*objectThe per-node state snapshot.
timestampintegerUnix epoch milliseconds when the event was emitted.

A single token of a short, present-tense "what I'm about to do" sentence, generated by a small fast model IN PARALLEL with the main turn to cover the reasoning model's time-to-first-token. Emitted only when the server is configured with SMOOTH_AGENT_PREAMBLE_MODEL. Shaped identically to stream_token so clients can reuse the render path, but on a distinct type so it is shown as an EPHEMERAL status line that the real answer replaces — it is NEVER folded into the answer, and the final response (carried by eventual_response) never includes it. The server suppresses it if the real answer has already begun streaming. Clients that do not recognize this event MUST ignore it — the answer still streams via stream_token, so the preamble simply isn't shown.

Off unless SMOOTH_AGENT_PREAMBLE_MODEL names a fast model. It runs in parallel with the real turn purely to cover time-to-first-token, and it is ephemeral — never persist it as the answer.

FieldTypeMeaning
type*"stream_preamble"Event type discriminator.
requestIdstringEchoes the requestId from the originating send_message action.
tokenstringThe raw preamble token text. Also present inside data.token for consumers that only inspect data.
data*objectPreamble token event payload.
timestampintegerUnix epoch milliseconds when the event was emitted.

A single *reasoning* token from a reasoning-model's separate thinking channel (reasoning_content/reasoning deltas — e.g. DeepSeek, gpt-oss/harmony, MiniMax, GLM), forwarded to the client in real time. Corresponds to smooth-operator's AgentEvent::ReasoningDelta. Shaped identically to stream_token so clients can render it the same way, but on a distinct type so reasoning is shown as collapsible "thinking" and is NEVER folded into the answer. The final response (carried by eventual_response) already excludes reasoning. Clients that do not recognize this event MUST ignore it — the answer still streams via stream_token, so reasoning simply isn't shown.

Only from reasoning models. Separate from stream_token so a UI can style or hide thinking.

FieldTypeMeaning
type*"stream_reasoning"Event type discriminator.
requestIdstringEchoes the requestId from the originating send_message action.
tokenstringThe raw reasoning token text. Also present inside data.token for consumers that only inspect data.
data*objectReasoning token event payload.
timestampintegerUnix epoch milliseconds when the event was emitted.

A single LLM output token forwarded to the client in real time. Corresponds to smooth-operator's AgentEvent::TokenDelta. Clients accumulate tokens to display a live typing animation. After the node finishes, a stream_chunk event carries the complete state snapshot for that node.

FieldTypeMeaning
type*"stream_token"Event type discriminator.
requestIdstringEchoes the requestId from the originating send_message action.
tokenstringThe raw token text. Also present inside data.token for consumers that only inspect data.
data*objectToken event payload.
timestampintegerUnix epoch milliseconds when the event was emitted.

Emitted when the agent workflow pauses before running a state-mutating tool call that requires explicit user approval. Corresponds to smooth-operator's AgentEvent::HumanInputRequired { Confirm }. The client must surface a confirmation dialog and reply with a confirm_tool_action action using the same requestId. Until the client responds, the workflow remains paused.

The turn is parked, not failed. It stays parked until a confirm_tool_action arrives — in-process, for roughly five minutes.

FieldTypeMeaning
type*"write_confirmation_required"Event type discriminator.
requestIdstringEchoes the requestId from the originating send_message action. Must be included in the confirm_tool_action reply.
data*objectConfirmation prompt details.
timestampintegerUnix epoch milliseconds when the event was emitted.

Domain types

The persisted shapes the frames above carry. Expand one to read its fields.

Checkpoint

6 fields · show

A point-in-time snapshot of a smooth-operator agent's state. Checkpoints are written by the agent runtime and are used to resume execution after interruptions (Lambda cold starts, HITL pauses, network errors). Corresponds to the Checkpoint struct in the smooth-operator Rust crate.

FieldTypeMeaning
id*stringUnique checkpoint identifier (UUID v4).
threadId*stringsmooth-operator workflow thread identifier this checkpoint belongs to. Matches Session.threadId for the associated session.
agentId*stringThe agent whose state is captured in this checkpoint.
iteration*integerAgent loop iteration counter at the time the checkpoint was taken.
metadataobjectArbitrary string key/value metadata attached to this checkpoint (e.g. phase name, bead ID).
createdAt*stringISO 8601 timestamp when the checkpoint was created.

Citation

5 fields · show

A source the agent used to ground its answer. Each citation points back at one retrieved knowledge-base document — the chunk the model read, plus enough metadata to render an attribution link. Citations are collected by the runtime from the documents that actually grounded a turn (the auto-injected [Relevant knowledge] context and any knowledge_search tool results) and attached to the terminal eventual_response. For GitHub-sourced documents url is the blob/issue URL; documents without a web source omit it.

FieldTypeMeaning
id*stringStable identifier of the cited source document (the knowledge-base document_id). Used to deduplicate citations within a turn.
title*stringHuman-readable label for the source — typically the document's source path or, for web-sourced docs, the URL/title.
urlstringCanonical link to the source, when one exists. For GitHub-sourced documents this is the blob/issue URL stamped onto the document's source at ingest (see CONNECTORS.md). Absent for sources with no web location (e.g. uploaded files).
snippet*stringThe retrieved chunk text that grounded the answer, truncated to a bounded length for display.
score*numberRelevance score of this source for the turn's query (the knowledge-base similarity score). Higher is more relevant.

Conversation

9 fields · show

A conversation thread between participants (users, AI agents, or human agents). Corresponds to a row in the conversations table. Platform indicates the channel on which the conversation takes place.

FieldTypeMeaning
id*stringUnique conversation identifier.
platform*"web" | "messenger" | "instagram" | "email" | "discord" | "phone" | "sms" | "slack" | "whatsapp" | "tiktok"The channel on which this conversation takes place.
name*stringHuman-readable display name for the conversation.
organizationId*stringThe organization that owns this conversation.
idempotencyKey*stringClient-provided key that prevents duplicate conversations from being created for the same logical thread.
metadataJsonobjectArbitrary key/value metadata attached to the conversation (e.g. campaign source, CRM fields).
analyticsJsonobjectAnalytics and scoring data aggregated from messages in this conversation.
createdAt*stringISO 8601 timestamp when the conversation was created.
updatedAt*stringISO 8601 timestamp when the conversation was last updated.

Message

12 fields · show

A single message within a conversation. Direction is from the conversation's perspective: inbound = arriving from the user/external party; outbound = sent by the agent or platform. Corresponds to a row in the conversation_messages table.

FieldTypeMeaning
id*stringUnique message identifier.
externalIdstring | nullID assigned by an external platform (e.g. a Twilio SID or Messenger message id).
organizationIdstring | nullThe organization that owns this message.
conversationIdstring | nullThe conversation this message belongs to.
direction*"inbound" | "outbound"Message direction relative to the platform: inbound = from user/external, outbound = from agent/platform.
content*MessageContentThe message payload.
fromobject | nullAbbreviated sender descriptor (wire shape used in API responses; full participant data lives in domain/participant.schema.json).
toobject | nullAbbreviated recipient descriptor.
metadataJsonobject | nullArbitrary key/value metadata attached to this message.
analyticsJsonobject | nullAnalytics data associated with this message (e.g. sentiment scores, token counts).
createdAt*stringISO 8601 timestamp when the message was created.
updatedAtstring | nullISO 8601 timestamp when the message was last updated.

Participant

15 fields · show

A participant in a conversation. Participants may be end users, AI agents, or human support agents. Corresponds to a row in the conversation_participants table.

FieldTypeMeaning
id*stringUnique participant identifier.
conversationId*stringThe conversation this participant belongs to.
organizationId*stringThe organization that owns this participant record.
type*"user" | "ai-agent" | "human-agent"Participant role: user = end-user, ai-agent = smooth-operator agent, human-agent = live support agent.
externalIdstring | nullExternal identity (e.g. Supabase auth user UUID) for authenticated participants.
internalIdstring | nullInternal system identifier (e.g. agent UUID from the agents table).
browserFingerprintstring | nullBrowser fingerprint (ThumbmarkJS) for anonymous user identification.
browserInfoobject | nullParsed browser / device metadata collected at session start.
name*stringDisplay name for this participant.
emailstring | nullEmail address if known.
phonestring | nullPhone number in E.164 format if known.
crmContactIdstring | nullForeign key into the CRM contacts table if this participant has been matched.
metadataJsonobjectArbitrary key/value metadata attached to this participant.
createdAt*stringISO 8601 timestamp when the participant record was created.
updatedAt*stringISO 8601 timestamp when the participant record was last updated.

Session

16 fields · show

An AI conversation session. Ties together a conversation, an agent, the user and agent participants, and the smooth-operator workflow thread. Corresponds to a row in the conversation_sessions table. The threadId field is the smooth-operator thread identifier (stored as langgraph_thread_id in the DB for historical reasons; renamed to threadId in the protocol).

FieldTypeMeaning
sessionId*stringUnique session identifier.
conversationId*stringThe conversation this session is attached to.
organizationId*stringThe organization that owns this session. Mirrors organizationId on the conversation, participants, and messages so org-scoping is uniform across every domain type and storage backends can write the session's org directly.
agentIdstringThe agent handling this session. OPTIONAL in storage: create_conversation_session REJECTS an absent or blank agentId, so a session created through the protocol always has one. It stays optional here for rows that predate that validation — it used to be filled with a fresh UUID, pointing every agentless session at an agent that had never existed (th-68897a). Absence is represented by omitting the field, never by a fabricated id.
agentName*stringHuman-readable display name of the agent.
userParticipantId*stringThe participant record representing the end user in this session.
agentParticipantId*stringThe participant record representing the AI agent in this session.
threadId*stringsmooth-operator workflow thread identifier. Used to resume agent state across turns and process restarts. Stored as langgraph_thread_id in the database for historical reasons.
status"active" | "idle" | "ended"Lifecycle status of the session.
tokenCountintegerCumulative token count consumed in this session.
messageCountintegerNumber of messages exchanged in this session.
metadataobjectArbitrary key/value metadata attached to this session (e.g. browser info, campaign source).
createdAtstringISO 8601 timestamp when the session was created.
updatedAtstringISO 8601 timestamp when the session was last updated.
endedAtstring | nullISO 8601 timestamp when the session ended, or null if still active.
lastActivityAtstringISO 8601 timestamp of the most recent activity (message, keepalive, etc.).

What actually holds the languages together

Worth being precise about, because the difference matters when you write a client.

The schemas in spec/ are the source of truth, and shared conformance fixtures under spec/conformance/ are validated in the TypeScript, Go, and Python CI jobs — spec/ is a trigger path on each, so changing a schema runs them.

What is not checked is type generation. spec/codegen/ holds a README and nothing else, and only the Go client has committed generated types (go/protocol/types_gen.go). The other clients carry hand-written mirrors, and no job re-generates them to compare. The fixtures catch a wire-level disagreement; nothing catches a field that was never mirrored.