Streaming: SSE & AG-UI
- Transport
- Server-Sent Events (
text/event-stream) over HTTPS POST/GET. No WebSocket endpoint is documented on this page. - Auth
- Bearer JWT for the tenant-facing endpoints ( Authentication ); share/demo streams use their own credentials.
- Two wire formats
- The legacy chat stream (in-band marker chunks) and the structured AG-UI stream. Both run the same turn through
AgentChatService.
SSE endpoints in core
| Verb and path | Controller | Auth | Purpose |
|---|---|---|---|
POST /api/agents/{agentId}/chat | AgentController | JWT, agent RUNNER | Streamed agent turn (token chunks). |
POST /api/agents/{agentId}/agui/run | AgUiRunController | JWT, RUNNER | AG-UI run (consumes application/json). |
GET /api/agents/{agentId}/agui/runs/{runId}/events | AgUiRunController | JWT, RUNNER | Rejoin a run after a dropped connection. |
GET /api/agents/{agentId}/jobs/{jobId}/trace/stream | AgentJobObservabilityController | JWT, VIEWER | Live trace of an autonomous job; closes when the job reaches a terminal state. |
POST /api/share/agents/{shareId}/chat/stream | AgentShareController | None (share id) | Anonymous shared-agent chat; body message (max 2000 chars), optional sessionKey. 429 rate_limited, 410 expired, 422 invalid. |
POST /api/share/agents/{shareId}/voice/turn; POST /api/voice/agents/{agentId}/turn | AgentShareVoiceController; VoiceCallController | Share id; JWT | Voice turn, multipart request, SSE response. |
POST /api/quick-task/stream | QuickTaskController | JWT | Quick-task stream. |
POST /api/briefs/plan | BriefController | JWT with PERM_MANAGE_AGENTS (catch-all /api/briefs/** matcher) | Brief planning stream. |
POST /api/demo/studio/agents/{agentId}/mission; POST /api/demo/studio/workflows/{workflowId}/run | DemoStudioController | X-Demo-Token | Demo Agent Studio streams. |
Workflow runs are not streamed: POST /api/workflows/{id}/run returns the run
(WorkflowRunResponse). With "background": true it returns the freshly persisted RUNNING row
and the client polls GET /api/workflows/{id}/runs/{runId} (see Workflow Engine).
Request fields (WorkflowRunRequest): input, requestedBy, clientRunId,
attachments, background. The A2A message/stream method also answers with
text/event-stream; see Developer API.
Chat stream (POST /api/agents/{agentId}/chat)
Request (ChatRequest)
| Field | Type | Notes |
|---|---|---|
message | string, required | Max 8000 characters (bean validation). |
sessionId | string | Generated if omitted. May not start with eval: or sim: (400). |
attachmentKey, attachmentMimeType, attachments, visionSessionId, turnRef | various | Attachment and vision context. Upload via POST /api/agents/{agentId}/chat/attachments (multipart). |
forceFastDepth | boolean | Used by the voice bridge to force the FAST reasoning depth. |
stepEvents | boolean | Opt in to tool-activity events on the stream. |
toolNames | string[] | Narrows this one turn to the listed tools of the agent (the playground's "this run only" tool switches). Narrowing only: a name the agent does not offer you for this turn is refused (400, naming it); a tool that is kept is still behind its approval, autonomy and guardrail gates; the agent's saved tool access is never changed. Omitted leaves the agent's tools as they are; an empty list runs the turn without tools. At most 200 names of 128 characters. The turn the platform starts after a human decides a gated call keeps the same selection, and the turn journal's intake event lists it (runTools=). |
Response framing
- Headers:
X-Session-Id(the session id, exposed via CORS),Cache-Control: no-cache,X-Accel-Buffering: no. - Body is a
Flux<String>: each chunk is written as its own SSEdata:event. Spring adds no space afterdata:, so clients strip the prefix by fixed length, not by trimming. - An empty chunk can occur (provider empty delta, keepalive on buffered reasoning depths). Ignore it.
- Multi-line chunks are split into several
data:lines by the SSE writer.
In-band markers
| Constant | Value | Meaning |
|---|---|---|
STREAM_ERROR_MARKER | @@AGENT_CHAT_STREAM_ERROR_9f2b1c@@ | Chunk prefix; the rest is a failure message. Emitted only if the stream already wrote to the wire (status line committed). A failure before the first byte is a normal HTTP error instead (402, 503 and so on, via GlobalExceptionHandler). |
STEP_EVENT_MARKER | @@AGENT_STEP_EVENT_4d81ba@@ | Chunk prefix followed by single-line JSON step event (tool call, tool result, error, plan, guardrail, handoff, run start/finish). Emitted only when stepEvents:true. |
INTERRUPTED_REPLY_MARKER | \n\n[reply interrupted] | Appended to a partially persisted reply when a turn is cancelled or the client disconnects (package-private constant in AgentChatService). |
AgentStepEventEmitter.
Turn-lifecycle step events
Besides the tool and run events, a stream that set stepEvents:true carries five events that say what the turn is doing beyond working. Their fields are codes, counts, names and ids, never wording, so a client keys its chips and notices on them and not on the notice text the platform also appends to the reply. The schemas are the agentStep_* messages of docs/api/asyncapi.json.
type | Fields | When |
|---|---|---|
finish | finishReason: stop, length, safety, malformed_tool_call, empty or interrupted; optional detail (the provider's word for a refusal) | Once per reply, before run_finished. Offer "Continue" on length and interrupted; show the refusal notice on safety. |
retry | attempt (the try about to run, 1 is the first), maxAttempts, reason | The same model is called again after a failure before its first token, or an unusable tool call is asked for again. |
failover | toModel (absent when not known by name), reason | The turn moved to the agent's fallback provider or to the next model of the platform-hosted pool. |
continuation | pass, maxPasses, reason: cut_off, unfinished_step or approval | Another model pass carries the reply on. |
approval_required | tool, approvalId | A tool call was recorded as a pending approval; sent after that call's tool_result. |
reason on retry and failover is one of rate_limited, timeout, provider_unavailable, auth_failed, billing, model_not_found, provider_error or, on retry only, malformed_tool_call. A client without a finish event (an older core) falls back to recognising the reply-text markers.
Run control
| Verb and path | Behaviour |
|---|---|
POST /api/agents/{agentId}/chat/sessions/{sessionId}/cancel | Requires RUNNER. Records a Redis flag polled by the running turn (about every 500 ms); replies 202 with a map containing cancelRequested: true. Works across replicas. |
POST /api/agents/{agentId}/chat/sessions/{sessionId}/rewind | Body SessionRewindRequest; semantics not documented on this page. |
AG-UI (POST /api/agents/{agentId}/agui/run)
A protocol-shaped alternative that lets any AG-UI client (for example @ag-ui/client's
HttpAgent, CopilotKit, or a hand-written SSE reader) drive an agent with no VegaDūta-specific
code. It is strictly additive: same authorization, session semantics, moderation, guardrails, metering and
persistence as /chat; threadId is the sessionId.
Request (RunAgentInput)
Fields: threadId, runId, parentRunId, state, messages[]
(id, role, content, toolCalls, toolCallId, name),
tools[], context[], forwardedProps; unknown fields are ignored. The controller runs the
last user message's text (string content, or the joined text parts of an array). No user text: 400.
Over the controller's maximum message length: 400. Missing threadId/runId are generated as UUIDs. A supplied runId must be 1 to 128 letters, digits, - or _ (otherwise 400).
In the controller code read for this page, only the last user message and the ids are passed to the turn; tools and state are not forwarded to it.
Event sequence
RUN_STARTED{threadId,runId}
STATE_SNAPSHOT{snapshot:{sessionId,agentId}}
TEXT_MESSAGE_START{messageId,role:"assistant"}
TEXT_MESSAGE_CONTENT{messageId,delta} (one per non-empty token)
TEXT_MESSAGE_END{messageId}
TOOL_CALL_START{toolCallId,toolCallName,parentMessageId}
TOOL_CALL_ARGS{toolCallId,delta} (whole redacted args JSON as one delta)
TOOL_CALL_END{toolCallId}
TOOL_CALL_RESULT{messageId,toolCallId,content,role:"tool"}
CUSTOM{name:"plan"|"guardrail"|"handoff",value:{...}}
RUN_FINISHED{threadId,runId} or RUN_ERROR{message,code} (never both)
- Every event carries
typeandtimestamp; null fields are omitted. Each SSE frame is one single-line JSONdata:value with anid:equal to its sequence number. - Text after a tool call opens a new assistant message with a fresh
messageId. - A tool exception surfaces as
TOOL_CALL_RESULT(content says it failed), notRUN_ERROR; the run's terminalRUN_ERRORcomes only from a real stream failure. - Not emitted:
STEP_STARTED/FINISHED,STATE_DELTA,MESSAGES_SNAPSHOT,RAW. Clients must treat these as optional. The protobuf transport is not offered. - A
:keepaliveSSE comment is sent every 15 seconds of silence; SSE clients ignore comments. - Pre-stream failures (unknown agent or no access: 404, malformed body: 400, tenant kill switch, guardrail 422) are plain HTTP errors with no events. A request whose
Acceptcannot taketext/event-streamgets 406.
Resume
GET /api/agents/{agentId}/agui/runs/{runId}/events replays buffered events after a position, then attaches to the live
tail if the run executes on this pod. Position: ?after=<seq> or the standard Last-Event-ID header
(after wins). If the run is on another pod or its live window passed you receive the buffered history and a clean
end of stream; poll again until the buffer contains RUN_FINISHED or RUN_ERROR. A rejoin is scoped to the agent in the path and to the signed-in user who started the run: for anyone else, or another agent, the stream is empty, exactly as for a run that does not exist.
Companion: GET /api/agents/{agentId}/agui/tools (JSON, VIEWER) lists the agent's tools as AG-UI
Tool objects (name, description, permissive parameters schema).
sequenceDiagram
participant U as AG-UI client
participant A as AgUiRunController
participant S as AgentChatService
participant B as Run buffer (Redis)
U->>A: POST /agui/run (Bearer JWT, Accept: text/event-stream)
A->>A: requireAccess RUNNER
A->>S: chat(..., stepEvents active)
S-->>A: tokens + step events
A->>B: record(event) returns seq
A-->>U: data: {RUN_STARTED} id: 1
A-->>U: data: {TEXT_MESSAGE_CONTENT} id: n
Note over U,A: connection drops
U->>A: GET /runs/{runId}/events?after=n
A->>B: readAfter(n)
A-->>U: buffered events, then live tail
Client guidance
- Send
Accept: text/event-streamandAuthorization: Bearer <token>(browserEventSourcecannot set the header; usefetchstreaming). - Behind proxies keep idle timeouts above the 15 s keepalive;
X-Accel-Buffering: nois already set on chat. - Reconnect AG-UI with
Last-Event-ID. The legacy chat stream has no resume; cancel then re-ask.
Evidence & references
core/agents-turn/src/main/java/com/agenticai/core/agents/turn/AgentChatStreamController.java—chat;AgentChatSessionController.java—cancelChatTurn;core/agents/src/main/java/com/agenticai/core/agents/core/AgentController.java—STREAM_ERROR_MARKER,SESSION_ID_HEADER.core/agents/.../dto/ChatRequest.java;core/agents/.../tools/AgentStepEventEmitter.java(STEP_EVENT_MARKER, framing rationale).core/agents/.../agui/AgUiRunController.java,AgUiEvent.java,AgUiEventMapper.java,RunAgentInput.java,AgUiRunEventBuffer.java.core/agents/.../job/AgentJobObservabilityController.java,share/AgentShareController.java,demo/studio/DemoStudioController.java;core/voice/.../VoiceCallController.java,AgentShareVoiceController.java;core/app/.../quicktask/QuickTaskController.java;core/brief/.../BriefController.java.core/workflow/.../WorkflowController.javaanddto/WorkflowRunRequest.java— run and background-run.- Related: Agents & Agent Runtime, Developer API.