AG-UI Guide
The AG-UI (Agent-User Interaction) protocol is maintained by the open-source AG-UI Protocol project. It enables agents built in different languages, frameworks, and execution environments to deliver their runtime outputs to user interfaces through a unified event stream. The protocol tolerates loosely matched payloads and supports transports such as SSE and WebSocket.
tRPC-Agent-Go ships with native AG-UI integration. It provides an SSE server implementation by default, while also allowing you to swap in a custom service.Service to use transports like WebSocket and to extend the event translation logic.
Getting Started
Assuming you already have an agent, you can expose it via the AG-UI protocol with just a few lines of code:
Note: If WithPath is not specified, the AG-UI server mounts at / by default.
A complete version of this example lives in examples/agui/server/default.
For an in-depth guide to Runners, refer to the runner documentation.
On the client side you can pair the server with frameworks that understand the AG-UI protocol, such as CopilotKit. It provides React/Next.js components with built-in SSE subscriptions. This repository ships with two runnable web UI samples:
- examples/agui/client/tdesign-chat: a Vite + React client built with TDesign that demonstrates custom events, graph interrupt approvals (human-in-the-loop), message snapshot loading, and report side panels.
- examples/agui/client/copilotkit: a Next.js client built with CopilotKit.

Core Concepts
RunAgentInput
RunAgentInput is the request payload for the AG-UI chat route and the messages snapshot route. It describes the input and context required for a conversation run. The structure is shown below.
For the full field definition, refer to AG-UI Go SDK.
Minimal request JSON example:
Real-time conversation route
The real-time conversation route handles a real-time conversation request and streams the events produced during execution to the client via SSE. The default route is / and can be customised with agui.WithPath.
For the same SessionKey (AppName + userID + sessionID), only one real-time conversation request can run at a time; repeated requests return 409 Conflict.
Even if the client SSE connection is closed, the backend continues executing until it finishes normally (or is cancelled / times out). By default, a single request can run for up to 1 hour. You can adjust this with agui.WithTimeout(d), and set it to 0 to disable the timeout; the effective deadline is the earlier of the request context deadline and agui.WithTimeout(d).
A complete example is available at examples/agui/server/default.
Cancel route
If you want to interrupt a running real-time conversation, enable the cancel route with agui.WithCancelEnabled(true) (disabled by default). The default route is /cancel and can be customised with agui.WithCancelPath.
The cancel route uses the same request body as the real-time conversation route. To cancel successfully, you must provide the same SessionKey (AppName + userID + sessionID) as the corresponding real-time conversation request.
Message Snapshot route
Message snapshots restore conversation history when a page is initialised or after a reconnect. The feature is controlled by agui.WithMessagesSnapshotEnabled(true) and is disabled by default. The default route is /history, can be customised with WithMessagesSnapshotPath, and returns the event stream RUN_STARTED → MESSAGES_SNAPSHOT → RUN_FINISHED.
This route supports concurrent access for the same userID + sessionID (threadId), and you can also query the snapshot for the same session while a real-time conversation is running.
To enable message snapshots, configure the following options:
agui.WithMessagesSnapshotEnabled(true)enables message snapshots;agui.WithMessagesSnapshotPathsets the custom message snapshot route, defaulting to/history;agui.WithAppName(name)specifies the application name;agui.WithSessionService(service)injects thesession.Serviceused to look up historical events;aguirunner.WithUserIDResolver(resolver)customises howuserIDis resolved, defaulting to"user".
When handling a message snapshot request, the framework extracts threadId as the SessionID from the AG-UI request body RunAgentInput, resolves userID using the custom UserIDResolver, and then builds session.Key with appName. It then queries the Session via session.Service, converts the stored events Session.Events into AG-UI messages, wraps them in a MESSAGES_SNAPSHOT event, and sends matching RUN_STARTED and RUN_FINISHED events.
Example:
You can find a complete example at examples/agui/messagessnapshot.
The format of AG-UI's MessagesSnapshotEvent can be found at messages.
Advanced Usage
Custom transport
The AG-UI specification does not enforce a transport. The framework uses SSE by default, but you can implement the service.Service interface to switch to WebSocket or any other transport:
Custom translator
translator.New converts internal events into the standard AG-UI events. To enrich the stream while keeping the default behaviour, implement translator.Translator and use the AG-UI Custom event type to carry extra data:
For example, when using React Planner, if you want to apply different custom events to different tags, you can achieve this by implementing a custom Translator, as shown in the image below.

You can find the complete code example in examples/agui/server/react.
Custom UserIDResolver
By default every request maps to the fixed user ID "user". Implement a custom UserIDResolver if you need to derive the user from the RunAgentInput:
Custom RunOptionResolver
By default, the AG-UI Runner does not append extra agent.RunOptions to the underlying runner.Run. Implement RunOptionResolver, inject it with aguirunner.WithRunOptionResolver, and translate client-provided configuration (for example, modelName or knowledgeFilter) from ForwardedProps.
RunOptionResolver executes for every incoming RunAgentInput. Its return value is forwarded to runner.Run in order. Returning an error surfaces a RunError to the client, while returning nil means no extra options are added.
Custom StateResolver
By default, the AG-UI Runner does not read RunAgentInput.State and write it into RunOptions.RuntimeState.
If you want to derive RuntimeState from state, implement StateResolver and inject it with aguirunner.WithStateResolver. The returned map is assigned to RunOptions.RuntimeState before calling the underlying runner.Run, overriding any RuntimeState set by other options (for example, RunOptionResolver).
Note: returning nil means no override, while returning an empty map clears RuntimeState.
Observability Reporting
Attach custom span attributes in RunOptionResolver; the framework will stamp them onto the agent entry span automatically:
Pair this with an AfterTranslate callback to accumulate output and write it to trace.output. This keeps streaming events aligned with backend traces so you can inspect both input and final output in your observability platform.
For a Langfuse-specific example, see examples/agui/server/langfuse.
Event Translation Callback
AG-UI provides an event translation callback mechanism, allowing custom logic to be inserted before and after the event translation process.
translator.BeforeTranslateCallback: Triggered before the internal event is translated into an AG-UI event. The return value convention:- Return
(customEvent, nil): UsecustomEventas the input event for translation. - Return
(nil, nil): Retain the current event and continue with the subsequent callbacks. If all callbacks returnnil, the original event will be used. - Return an error: Terminates the current execution, and the client will receive a
RunError.
- Return
translator.AfterTranslateCallback: Triggered after the AG-UI event translation is completed and just before it is sent to the client. The return value convention:- Return
(customEvent, nil): UsecustomEventas the final event to be sent to the client. - Return
(nil, nil): Retain the current event and continue with the subsequent callbacks. If all callbacks returnnil, the original event will be sent. - Return an error: Terminates the current execution, and the client will receive a
RunError.
- Return
Usage Example:
Event translation callbacks can be used in various scenarios, such as:
- Custom Event Handling: Modify event data or add additional business logic during the translation process.
- Monitoring and Reporting: Insert monitoring and reporting logic before and after event translation. A full example of integrating with Langfuse observability platform can be found at examples/agui/server/langfuse.
RunAgentInput Hook
You can use WithRunAgentInputHook to mutate the AG-UI request before it reaches the runner. The following example reads other_content from ForwardedProps and appends it to the latest user message:
Key points:
- Returning
nilkeeps the original input object while preserving in-place edits. - Returning a custom
*adapter.RunAgentInputreplaces the original input; returningnilkeeps it. - Returning an error aborts the request and the client receives a
RunErrorevent.
Session Storage and Event Aggregation
When constructing the AG-UI Runner, pass in SessionService. Events generated by real-time conversations will be written into the session through SessionService, making it convenient to replay the history later via MessagesSnapshot.
In streaming response scenarios, a single reply usually consists of multiple incremental text events. Writing all of them directly into the session can put significant pressure on SessionService.
To address this, the framework first aggregates events and then writes them into the session. In addition, it performs a periodic flush once per second by default, and each flush writes the current aggregation result into the session.
aggregator.WithEnabled(true)is used to control whether event aggregation is enabled. It is enabled by default. When enabled, it aggregates consecutive text events that share the samemessageId. When disabled, no aggregation is performed on AG-UI events.aguirunner.WithFlushInterval(time.Second)is used to control the periodic flush interval of aggregated results. The default is 1 second. Setting it to 0 disables the periodic flush mechanism.
If more complex aggregation strategies are required, you can implement aggregator.Aggregator and inject it through a custom factory. Note that although an aggregator is created separately for each session, avoiding cross-session state management and concurrency handling, the aggregation methods themselves may still be called concurrently, so concurrency must still be handled properly.
For example, while remaining compatible with the default text aggregation, you can accumulate the content of custom events of type "think" and then persist them.
A complete example can be found at examples/agui/server/thinkaggregator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
Setting the BasePath for Routes
agui.WithBasePath sets the base route prefix for the AG-UI service. The default value is /, and it is used to mount the real-time conversation route, message snapshot route, and the cancel route (when enabled) under a unified prefix, avoiding conflicts with existing services.
agui.WithPath, agui.WithMessagesSnapshotPath, and agui.WithCancelPath only define sub-routes under the base path. The framework will concatenate them with the base path to form the final accessible routes.
Here’s an example of usage:
In this case, the real-time conversation route will be /agui/chat, the cancel route will be /agui/cancel, and the message snapshot route will be /agui/history.
GraphAgent Node Activity Events
With GraphAgent, a single run typically executes multiple nodes along the graph. To help the frontend highlight the active node and render Human-in-the-Loop prompts, the framework can emit node lifecycle and interrupt-related ACTIVITY_DELTA events. For the event format, see the AG-UI official docs. They are disabled by default and can be enabled when constructing the AG-UI server.
Node lifecycle (graph.node.lifecycle)
This event is disabled by default; enable it via agui.WithGraphNodeLifecycleActivityEnabled(true) when constructing the AG-UI server.
When enabled, the server emits ACTIVITY_DELTA for start / complete / error phases with the same activityType: graph.node.lifecycle, and uses /node.phase to distinguish the phase.
For the node start phase (phase=start), it is emitted before a node runs and writes the current node to /node via add /node:
This event helps the frontend track progress. The frontend can treat /node.nodeId as the currently active node and use it to highlight the graph execution.
For the node success end phase (phase=complete), it is emitted after a node finishes successfully and writes the node result to /node via add /node:
For the node failure end phase (phase=error, non-interrupt), it includes an error message in /node:
Interrupt (graph.node.interrupt)
This event is disabled by default; enable it via agui.WithGraphNodeInterruptActivityEnabled(true) when constructing the AG-UI server.
activityType is graph.node.interrupt. It is emitted when a node calls graph.Interrupt(ctx, state, key, prompt) and there is no available resume input. The patch writes the interrupt payload to /interrupt via add /interrupt, including nodeId, key, prompt, checkpointId, and lineageId:
This event indicates the run is paused at the node. The frontend can render /interrupt.prompt as the interrupt UI and use /interrupt.key to decide which resume value to provide. checkpointId and lineageId can be used to resume from the correct checkpoint and correlate runs.
In multi-level GraphAgent setups, subgraph interrupts bubble up and the stream may contain multiple graph.node.interrupt events by default. If the client only wants to keep the outermost interrupt used for resuming, enable agui.WithGraphNodeInterruptActivityTopLevelOnly(true); when enabled, only the outermost interrupt event is emitted.
Resume ack (graph.node.interrupt)
When a run starts with resume input, the AG-UI server emits an extra ACTIVITY_DELTA at the beginning of the run, before any graph.node.lifecycle events. It uses activityType: graph.node.interrupt, clears the previous interrupt state by setting /interrupt to null, and writes the resume input to /resume. /resume contains resumeMap or resume plus optional checkpointId and lineageId:
For a complete example, see examples/agui/server/graph. For a client implementation, see examples/agui/client/tdesign-chat.
Best Practices
Generating Documents
If a long-form document is inserted directly into the main conversation, it can easily “flood” the dialogue, making it hard for users to distinguish between conversation content and document content. To solve this, it’s recommended to use a document panel to carry long documents. By defining a workflow in the AG-UI event stream — “open document panel → write document content → close document panel” — you can pull long documents out of the main conversation and avoid disturbing normal interactions. A sample approach is as follows.
-
Backend: Define tools and constrain call order
Provide the Agent with two tools: open document panel and close document panel, and constrain the generation order in the prompt: when entering the document generation flow, execute in the following order:
- Call the “open document panel” tool first
- Then output the document content
- Finally call the “close document panel” tool
Converted into an AG-UI event stream, it looks roughly like this:
-
Frontend: Listen for tool events and manage the document panel
On the frontend, listen to the event stream:
- When an
open_report_documenttool event is captured: create a document panel and write all subsequent text message content into that panel. - When a
close_report_documenttool event is captured: close the document panel (or mark it as completed).
- When an
The effect is shown below. For a full example, refer to examples/agui/server/report. The corresponding client implementation lives in examples/agui/client/tdesign-chat.

External Tools
When a tool must be executed on the client side or within business services, you can use the external tool pattern. The backend only generates the tool call and interrupts execution at the tool node. The frontend runs the tool and sends the result back. The backend then resumes from the interrupt point and continues the run. This pattern requires the tool result to be included in the LLM context and persisted to session history so that a Message Snapshot can replay a complete conversation later.
It is recommended to enable agui.WithGraphNodeInterruptActivityEnabled(true) on the server. This allows lineageId and checkpointId to be carried in the graph.node.interrupt event so the client can locate the resume point and initiate the next request.
A single external tool invocation corresponds to two requests. The first request uses role=user. When the LLM triggers a tool call, the event stream emits TOOL_CALL_START, TOOL_CALL_ARGS, and TOOL_CALL_END, then emits ACTIVITY_DELTA graph.node.interrupt at the tool node and closes the SSE stream. The client reads toolCallId and the tool arguments from the tool call events, and reads lineageId from the interrupt event.
The second request uses role=tool to send the tool result back to the server. The toolCallId must match the first request, content is the tool output string, and forwardedProps.lineage_id must be set to the lineageId returned by the interrupt event. The server first translates this tool message into a TOOL_CALL_RESULT event and persists it to the session, then resumes from the corresponding checkpoint and continues generating the final answer.
If the LLM does not trigger any tool call in the first request, no interrupt event will be emitted and a second request is not required.
Both requests must use the same threadId, and each request should use a new runId. The framework only processes the last message in messages. Only role=user and role=tool are supported, and content must be a string.
Request examples:
First request:
Second request:
Example event stream:
For a complete example, see examples/agui/server/externaltool. For a frontend implementation, see examples/agui/client/tdesign-chat.