Realtime API¶
The engine exposes server-sent events at GET /api/v1/stream and a bidirectional WebSocket at WS /api/v1/events. Both require the runtime bearer token.
Server-sent events¶
Connect with Authorization: Bearer <token>. The response is 200 text/event-stream with Cache-Control: no-cache, Connection: keep-alive, and X-Accel-Buffering: no. Each item contains only a compact JSON data line:
data: {"type":"...","timestamp":"...","data":{...}}
There are no SSE event, id, retry, heartbeat, history, or replay fields. A subscriber sees only future events. Each subscriber queue holds 100 events; when full, the oldest pending event is dropped.
Event type |
data |
Published by |
|---|---|---|
thread.created |
{threadId,workspaceId,title} |
REST thread creation and WebSocket implicit thread creation |
thread.updated |
{threadId,title,description} |
REST thread update only |
message.created |
{messageId,threadId,role,content} |
REST message creation and WebSocket user/tool/assistant/system messages |
graph.execution |
{thread_uuid,type,...graph fields} |
Foreground graph activity during WebSocket chat |
Graph event types and fields are run.started {run_id,graph_id}, node.started {node_id}, model.delta {node_id,delta}, node.completed {node_id}, run.completed {run_id}, run.aborted {run_id}, and run.failed {run_id,error}. Workspace changes, deletes, files, settings, models, tools, RAG, and accounts do not publish SSE events.
WebSocket connection and envelope¶
Connect to ws://{host}:{port}/api/v1/events with the Bearer header. For client compatibility only, ?token=<token> is accepted when no Bearer value was supplied. Invalid authentication closes before acceptance with code 4401.
{"v":1,"type":"chat.send","id":"turn-123","data":{}}
Inbound frames require string type; string id is optional; non-object data becomes {}; inbound v is ignored. Outbound frames always contain {v:1,type,data} and include id when correlated. A missing/unknown type produces an uncorrelated error without closing the connection.
Optional client registration¶
client.hello is optional for chat, but registered sessions appear in the tray and receive cross-client broadcasts. Send {clientId?:string,clientName?:string}. The engine responds without a correlation ID:
client.hello.ack {clientId}— requested ID accepted or a generated ID assigned.client.hello.reject {reason}— ID already belongs to an active connection; the socket remains open.
Client IDs are process-wide and must be released by disconnecting before another active socket can claim them.
Inbound WebSocket frames¶
type |
Correlation and data |
Behavior |
|---|---|---|
client.hello |
No correlation; {clientId?:string,clientName?:string} |
Registers client identity; sends ack/reject. |
ping |
ID/data ignored | Sends uncorrelated pong with data:null. |
chat.send |
id is turn ID or generated; ChatSend data |
Starts one asynchronous turn. |
chat.cancel |
Envelope id, otherwise data.turn_id |
Cancels only the matching active turn; a non-match is silent. |
chat.notification |
id or generated; {content:nonblank string,thread_uuid:string} |
Persists a system message; sends correlated done/error. |
tool.approval.response |
Optional turn id; {approval_id:string,decision:string} |
Only case-insensitive approve approves; every other decision denies. |
tool.register |
{tools:[{id:string},...]} |
Registers allowlisted UI tools for this connection; no acknowledgement. |
tool.unregister |
{id:string} |
Removes one registered UI tool; no acknowledgement. |
client.tool.result |
Mandatory turn id; {request_id:string,ok:boolean,result?:any,error?:string} |
Completes a pending client/UI tool call. |
profile.set |
Ignored | Accepted placeholder. |
tool.result |
Ignored | Accepted legacy placeholder. |
| any other type | — | Sends error {error:"Unknown frame type: ..."}. |
A malformed tool.register.data.tools value that is not iterable is not guaranteed a protocol error and can terminate the handler. Send an array.
ChatSend¶
{
content: string,
thread_uuid: string XOR workspace_uuid: string,
model_id?: string,
tools_enabled?: boolean = true,
file_context?: FileContext
}
Exactly one target is required. content must be a string but may be empty. Only literal JSON false disables built-in tools. Client-registered UI tools remain available regardless of tools_enabled.
FileContext = {
active_file?: null|ContextFile,
tagged_files?: ContextFile[] (max 20),
attachments?: Attachment[] (max 5)
}
ContextFile = { root_index: integer>=0, path: string(1..1024), label?: any }
Attachment = {
name: plain filename(1..255), media_type: string(max 255),
data: valid base64 (max decoded 2 MiB)
}
Total decoded attachments are limited to 8 MiB. Tagged files are deduplicated by (root_index,path). If label is not a nonblank string, it defaults to path; nonblank labels are truncated to 1,024 characters. Empty attachment media type becomes application/octet-stream. File existence and root scope are checked only when a workspace-file tool reads the reference.
Binary parts are forwarded to non-echo models only for these exact, case-sensitive MIME strings: application/pdf, application/msword, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, text/plain, text/csv, text/html, text/markdown, image/jpeg, image/png, image/gif, image/webp, audio/wav, audio/mpeg, audio/ogg, audio/flac, audio/aiff, and audio/aac. Other media types are accepted within attachment limits but their bytes are not forwarded as model binary parts; only their context metadata remains available.
Client/UI tools¶
Only these IDs are accepted by tool.register: set_ui_theme_color, set_ui_lighting_mode, open_ui_workspace_file, navigate_ui, and set_workspace_builtin_tool_enabled. Definitions beyond id are ignored. Every UI tool always requests approval, then emits client.tool.request; the client has 60 seconds to return client.tool.result with the same turn ID and request ID.
Outbound WebSocket frames¶
type |
id |
data |
|---|---|---|
client.hello.ack |
None | {clientId} |
client.hello.reject |
None | {reason} |
pong |
None | null |
error |
None | {error} |
chat.delta |
Turn | {thread_uuid,delta} |
chat.done |
Turn | {thread_uuid} |
chat.cancelled |
Turn | {thread_uuid} |
chat.error |
Turn | {thread_uuid,error} |
chat.notification.done |
Notification | {thread_uuid,message_uuid} |
chat.notification.error |
Notification | {error} |
agui.event |
Turn | {thread_uuid,type,...graph fields} |
tool.approval.request |
Turn | {approval_id,thread_uuid,tool_name,arguments,operation} |
client.tool.request |
Turn | {request_id,tool_name,arguments} |
thread.created |
None | {threadId,workspaceId,title} |
message.created |
None | {messageId,threadId,role,content} |
The agui.event name is active even though HTTP POST /agui is a placeholder. It carries graph lifecycle/model-delta events described in the SSE section.
Chat turn sequence¶
- The client sends
chat.sendwith a uniqueid. - With
workspace_uuid, the engine creates a thread titled from normalized message text (60 characters maximum; blank becomesNew conversation). Withthread_uuid, it loads that thread and workspace. - Model precedence is explicit
model_id, thread default, workspace default, thenecho. The resolved configuration ID is persisted back tothread.default_model_id, including a per-turn override. - The user message commits before model execution. An implicitly created thread also becomes visible at this point and emits
thread.created. - Graph activity arrives as correlated
agui.event; approvals/client tool calls pause until answered. Tool records are committed as compact JSON role=toolmessages. - The assistant message commits. When the final text is non-empty, the engine sends one
chat.deltacontaining that complete text; it always follows withchat.done.
Do not assume chat.delta is token streaming
Stream-enabled graph nodes emit token/chunk updates as agui.event with type model.delta. A non-empty final response also produces one full-text chat.delta. When no graph engine is attached, that optional full-text frame is the only delta; an empty final response proceeds directly to chat.done.
Cancellation or failure after step 4 leaves the thread and user message stored; committed tool messages can also remain without an assistant reply. chat.cancelled or chat.error reports the outcome.
Approval, cancellation, and concurrency¶
Only one turn may run per WebSocket connection. A second send produces an uncorrelated error; separate connections can concurrently operate on the same thread because there is no per-thread lock. Approval requests have no timeout and wait until response, cancellation, or disconnect. Disconnect cancels the active turn and pending approval/client-tool futures.
A tool.approval.response may omit the turn ID, but if present it must match. A client.tool.result must include and match the turn ID. Stale, missing, or mismatched request IDs produce uncorrelated error frames.
Cross-client broadcasts¶
Only hello-registered sessions receive WebSocket broadcasts. Implicit WebSocket thread creation broadcasts thread.created; WebSocket user/tool/assistant/system persistence broadcasts message.created; the originating client is excluded. REST mutations publish SSE but are not WebSocket-broadcast. Deletes and workspace changes have no realtime notification. Clients should refresh after reconnect and must not treat these frames as a complete replication log.