AG-UI Protocol

Runtime options

useAgUiRuntime options, adapters, supported events, thread list.

Reference for the runtime's API surface. Start with quickstart if you have not already.

useAgUiRuntime options

OptionTypeDescription
agentHttpAgentAn AG-UI client agent (from @ag-ui/client). Required.
loggerPartial<Logger>Optional logger overrides. The runtime logs event-parser warnings and run lifecycle events.
showThinkingbooleanWhether to render THINKING_* and REASONING_* events as visible reasoning. Defaults to true.
autoCancelPendingToolCallsbooleanCancel unresolved client-side tool calls automatically when the user sends, edits, or reloads a message. Defaults to true. See below.
onError(e: Error) => voidError callback fired on RUN_ERROR events and protocol errors.
onCancel() => voidCancellation callback fired when the user cancels a run.
adaptersUseAgUiRuntimeAdaptersStandard adapter slots (see below).

Adapter slots

AdapterSlotNotes
Attachmentsadapters.attachmentsSee attachment adapter.
Speechadapters.speechText-to-speech. See speech adapter.
Dictationadapters.dictationSpeech-to-text input.
Feedbackadapters.feedbackThumbs up / down. See feedback adapter.
Historyadapters.historyPer-thread message persistence.
Thread listadapters.threadListMulti-thread switching (experimental, see below).

Loading conversation history

If your backend exposes the persisted AG-UI messages of a conversation (for example a GET /agents/state endpoint), use fromAgUiMessages to convert them to assistant-ui messages and return them from the history adapter so the thread is restored on page load:

import { fromAgUiMessages } from "@assistant-ui/react-ag-ui";
import { ExportedMessageRepository } from "@assistant-ui/react";

const runtime = useAgUiRuntime({
  agent,
  adapters: {
    history: {
      async load() {
        const { messages } = await fetch("/agents/state").then((r) => r.json());
        return ExportedMessageRepository.fromArray(fromAgUiMessages(messages));
      },
      async append({ message }) {
        // persist the newly sent message on your backend
      },
    },
  },
});

Messages sent during the session are always forwarded to the agent through the run input, independent of append. A no-op append is therefore only safe when your backend already persists the conversation on its own; otherwise those messages are gone on the next page load.

fromAgUiMessages accepts an optional second argument: pass { showThinking: false } to match a runtime configured with showThinking: false, so imported reasoning messages are dropped at conversion time, the same way a live run never stores them.

fromAgUiMessages reconstructs the text, reasoning, and tool calls of each message. Multimodal user input (image, audio, video, and document parts, as well as legacy binary parts) is restored as attachments on the user message, so a backend that persists multimodal messages shows them again on reload and re-sends them on the next run. Legacy binary parts that only reference a file id are not restored.

An assistant message whose tool call has no matching tool result is reconstructed with requires-action status, the same status the runtime derives for a pending tool call, so a reloaded human-in-the-loop call (for example an ask_user tool) is actionable rather than stuck. This matches how every other external-store runtime surfaces a pending tool call on reload. The AG-UI wire snapshot carries no run outcome, so a tool call that a successful run intentionally left without a result is also surfaced as actionable.

Interrupts are restored when your backend persists them on the assistant message. The AG-UI message body has no interrupt field, so persist the runtime's own metadata.custom.agui.interrupts array alongside the message; fromAgUiMessages reads it back, reconstructs requires-action / interrupt status, and re-attaches the metadata, so getPendingInterrupts, useAgUiInterrupts, and submitInterruptResponses work on reload. When both a pending tool call and an interrupt are present on the same message, interrupt status wins. Without the persisted array, interrupt state cannot be reconstructed.

Thread list (experimental)

The thread list adapter is currently experimental and may change without notice.

UseAgUiThreadListAdapter lets you back the thread list with your own state.

OptionTypeDescription
threadIdstringThe currently active thread ID.
onSwitchToNewThread() => Promise<void>Called when the user creates a new thread. Reset your thread state here.
onSwitchToThread(threadId: string) => Promise<{ messages, state? }>Called when the user switches threads. Return the persisted messages (and optional opaque state).
const runtime = useAgUiRuntime({
  agent,
  adapters: {
    threadList: {
      threadId: currentThreadId,
      onSwitchToNewThread: async () => {
        setCurrentThreadId(await createThread());
      },
      onSwitchToThread: async (id) => {
        const { messages, state } = await loadThread(id);
        setCurrentThreadId(id);
        return { messages, state };
      },
    },
  },
});

Interrupts (experimental)

The interrupt API is experimental and may change without notice.

When the agent emits RUN_FINISHED with outcome = { type: "interrupt", interrupts: [...] }, the active assistant message is marked requires-action with reason: "interrupt" and the Interrupt[] payload is written to metadata.custom.agui.interrupts. Render an approval / input UI from there.

useAgUiRuntime returns an AgUiAssistantRuntime with two extra methods:

MethodDescription
unstable_getPendingInterrupts(): readonly AgUiInterrupt[]Snapshot of the open interrupts on the most recent assistant message.
unstable_submitInterruptResponses(responses: ResumeEntry[]): Promise<void>Submits one ResumeEntry per open interrupt and resumes the run.

responses must address every open interrupt; missing entries, unknown ids, or expired interrupts (expiresAt) reject before any network call. Each entry is { interruptId, status: "resolved" | "cancelled", payload? }. The next RunAgentInput carries resume: ResumeEntry[].

const runtime = useAgUiRuntime({ agent });
const pending = runtime.unstable_getPendingInterrupts();

await runtime.unstable_submitInterruptResponses(
  pending.map((i) => ({
    interruptId: i.id,
    status: "resolved",
    payload: { approved: true },
  })),
);

Steering away from an interrupt

When the user ignores the interrupt UI and just sends a new message, use the useAgUiSteerAway hook. Every open interrupt defaults to status: "cancelled", the new message is appended, and the run resumes with resume: ResumeEntry[] on the wire.

The same hook also steers away from pending client-side tool calls. When the head assistant message is in requires-action with reason: "tool-calls" (frontend tools awaiting a result) and the user sends a new message, every unresolved tool call is cancelled with an error result, the message is completed, and a single fresh run starts with those cancellations in its history. Passing responses in this case throws, since responses only address interrupts. With nothing pending, steerAway behaves like a normal append.

const steerAway = useAgUiSteerAway();

// the user typed a new message instead of answering the interrupt
await steerAway("actually, let's do something else");

The message accepts a plain string or a partial AppendMessage (the parent defaults to the current head, which is the interrupted assistant message). Pass responses to override the status of specific interrupts; the rest still default to cancelled.

await steerAway("continue without the file", [
  { interruptId: "tool-1", status: "resolved", payload: { approved: true } },
]);

Auto-cancelling pending tool calls

By default the runtime does the tool-call half of this automatically: when client-side tool calls are still unresolved and the user sends a new message, edits an earlier one, or reloads, every unresolved tool call receives the same cancellation error result, the assistant message is completed, and the run proceeds with those cancellations in its history. Set autoCancelPendingToolCalls: false to opt out, in which case pending tool calls stay unresolved on a plain send and steering away remains the explicit way to cancel them.

Pending interrupts are never auto-cancelled: sending while an interrupt is open still throws, and the interrupt must be answered with useAgUiSubmitInterruptResponses or discarded with useAgUiSteerAway.

Supported events

The runtime parses the AG-UI event stream and maps each event type to assistant-ui state.

EventEffect
RUN_STARTED / RUN_FINISHEDToggles thread isRunning. RUN_FINISHED.outcome is honored (success / interrupt).
RUN_CANCELLEDMarks the in-flight assistant message as cancelled.
RUN_ERRORMarks the message as errored; fires onError.
TEXT_MESSAGE_START / _CONTENT / _ENDStreams text content into an assistant message.
TEXT_MESSAGE_CHUNKAppends a delta without explicit lifecycle.
THINKING_START / _ENDWraps reasoning blocks (when showThinking is on).
THINKING_TEXT_MESSAGE_*Streams thinking text deltas.
REASONING_START / _MESSAGE_* / _ENDStreams structured reasoning per message.
TOOL_CALL_START / _ARGS / _ENDStreams tool calls into the current assistant message.
TOOL_CALL_CHUNKStreams tool deltas without explicit lifecycle.
TOOL_CALL_RESULTAttaches a tool result to a tool call.
STATE_SNAPSHOTReplaces the agent's external state.
STATE_DELTAApplies a JSON-patch-style delta to the agent's state.
MESSAGES_SNAPSHOTReplaces the full message list (used for thread restore).
CUSTOMForwarded to your custom event handling.
RAWUntyped passthrough for unrecognized events.

Feature support

FeatureSupported
Streaming textYes
Thinking / reasoning blocksYes
Tool calls and resultsYes
Tool result handoff (client-side execution)Yes
State snapshots and deltasYes
CancellationYes
Message editingYes
Message reloadYes
Run resumptionYes
Interrupts (human-in-the-loop)Experimental (unstable_* API, see Interrupts)
Multi-threadExperimental (adapters.threadList)
History persistenceVia history adapter