AG-UI · Reference

Event Mapping

@threadplane/ag-ui reduces AG-UI protocol events into the Agent contract from @threadplane/chat.

This page is the compatibility map. If your backend emits these events with the expected fields, the chat UI can render the run without knowing which runtime produced it.

#Summary

AG-UI eventAgent fieldBehavior
RUN_STARTEDstatus, isLoading, error, interruptSets status to running, isLoading to true, clears error, and clears any pending interrupt.
RUN_FINISHEDstatus, isLoadingSets status to idle and isLoading to false.
RUN_ERRORstatus, isLoading, errorSets status to error, stops loading, and stores the event message when present.
TEXT_MESSAGE_STARTmessagesCreates or reuses an assistant message slot.
TEXT_MESSAGE_CONTENTmessagesAppends delta to the message content.
TEXT_MESSAGE_ENDmessagesNo-op today. Content is already accumulated from deltas.
REASONING_MESSAGE_STARTmessagesCreates or reuses an assistant message slot with reasoning.
REASONING_MESSAGE_CONTENTmessagesAppends delta to message.reasoning.
REASONING_MESSAGE_CHUNKmessagesTreated the same as REASONING_MESSAGE_CONTENT.
REASONING_MESSAGE_ENDmessagesAdds reasoningDurationMs when timing is available.
TOOL_CALL_STARTtoolCalls, messagesAdds a running tool call; also links the call to its parent assistant message (via parentMessageId), creating a message slot if needed.
TOOL_CALL_ARGStoolCallsAccumulates partial JSON fragments, keeps the last-good parsed args, and finalizes parsing on TOOL_CALL_END.
TOOL_CALL_RESULTtoolCallsStores the tool result on the matching tool call.
TOOL_CALL_ENDtoolCallsMarks the matching tool call complete.
STATE_SNAPSHOTstate, messagesReplaces state and merges citations from state.citations.
STATE_DELTAstate, messagesApplies JSON Patch to state and merges citations from state.citations.
MESSAGES_SNAPSHOTmessages, toolCallsReplaces the message list and merges snapshot-embedded tool calls into toolCalls (by id).
CUSTOM (name: on_interrupt)interruptSets the interrupt signal to { id, value, resumable: true }. The value is JSON-parsed when it arrives as a string.
CUSTOM (other names)events$Emits a runtime-neutral custom event.
unknown eventnoneIgnored. Future protocol events do not crash the adapter.

#Run lifecycle

The adapter exposes lifecycle through status, isLoading, and error.

agent.status();    // 'idle' | 'running' | 'error'
agent.isLoading(); // boolean
agent.error();     // unknown

RUN_STARTED clears a previous error. RUN_ERROR and onRunFailed both put the agent into the error state.

onRunFailed comes from the AG-UI subscriber API rather than a protocol event. The adapter treats it like a run failure: status = 'error', isLoading = false, and error is the thrown value.

#Messages

Text messages are accumulated by messageId.

{ type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' }
{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: 'Hello' }
{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: ' there' }
{ type: 'TEXT_MESSAGE_END', messageId: 'm1' }

The resulting message is:

{ id: 'm1', role: 'assistant', content: 'Hello there' }

TEXT_MESSAGE_END does not finalize anything separately. The content already lives in the signal.

MESSAGES_SNAPSHOT replaces the full message array. Use it when the backend is authoritative for the whole conversation.

#Reasoning

Reasoning events write to the same assistant message when they share the same messageId as the final text response.

{ type: 'REASONING_MESSAGE_START', messageId: 'm1', role: 'assistant' }
{ type: 'REASONING_MESSAGE_CONTENT', messageId: 'm1', delta: 'Checking policy.' }
{ type: 'REASONING_MESSAGE_END', messageId: 'm1' }
{ type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' }
{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: 'Approved.' }

The message keeps both fields:

{
  id: 'm1',
  role: 'assistant',
  reasoning: 'Checking policy.',
  reasoningDurationMs: 12,
  content: 'Approved.',
}

The duration is measured in the browser from start to end event. It is useful for display, not billing or tracing.

#Tool calls

Tool calls are stored independently from messages in agent.toolCalls().

{ type: 'TOOL_CALL_START', toolCallId: 'search-1', toolCallName: 'search' }
{ type: 'TOOL_CALL_ARGS', toolCallId: 'search-1', delta: '{"q":"Angular"}' }
{ type: 'TOOL_CALL_RESULT', toolCallId: 'search-1', content: { hits: 3 } }
{ type: 'TOOL_CALL_END', toolCallId: 'search-1' }

The resulting tool call is:

{
  id: 'search-1',
  name: 'search',
  args: { q: 'Angular' },
  result: { hits: 3 },
  status: 'complete',
}

TOOL_CALL_ARGS accepts partial JSON fragments. The reducer appends each delta to an internal buffer for the tool call, parses whenever the buffer becomes valid JSON, and keeps the last-good args while more fragments arrive. On TOOL_CALL_END, it performs one final parse before marking the call complete.

If the accumulated buffer never parses, args stay at the last-good value or {}.

#State

STATE_SNAPSHOT replaces the full state object:

{ type: 'STATE_SNAPSHOT', snapshot: { topic: 'billing' } }

STATE_DELTA applies JSON Patch operations to the current state:

{
  type: 'STATE_DELTA',
  delta: [
    { op: 'replace', path: '/topic', value: 'support' },
    { op: 'add', path: '/caseId', value: 'case-123' },
  ],
}

After either state event, the adapter also runs the citations bridge. If state.citations contains entries keyed by message id, those citations are copied onto the matching messages.

See Citations for the expected shape.

#Custom events

CUSTOM events are exposed through events$.

When the custom event name is state_update and the value is an object, the adapter emits:

{ type: 'state_update', data: value }

For every other custom event name, it emits:

{ type: 'custom', name, data: value }

Use state for durable UI state. Use events$ for transient events, telemetry hooks, or UI side effects that should not be stored as conversation state.

Every non-on_interrupt CUSTOM event is fanned out to two surfaces, not one. Alongside the events$ emission above, the reducer also appends { name, data } to the AG-UI-specific customEvents() signal documented on injectAgent() and toAgent(). Reach for the signal when you want an accumulated per-run snapshot for reactive rendering (for example, a2ui-partial generative UI); reach for events$ when you want a transient stream for side-effects or telemetry.

#Submit and stop

agent.submit({ message }) performs three steps:

  1. Builds a local user message.
  2. Appends it to messages and calls source.addMessage().
  3. Calls source.runAgent().

If message is omitted, no user message is appended, but runAgent() still runs.

agent.stop() calls source.abortRun(). Cancellation depends on the AG-UI source implementation.

#Activity and subagents

ACTIVITY_SNAPSHOT and ACTIVITY_DELTA events project into the AG-UI-specific subagents() signal. Use that signal for progress cards and nested task views backed by AG-UI activity streams.

#Unsupported protocol areas

The adapter supports CUSTOM on_interrupt events for the runtime-neutral interrupt() signal, and it supports ACTIVITY-backed subagent progress. It does not implement history or time-travel. Unknown protocol events are ignored rather than treated as errors.