Architecture
@threadplane/ag-ui is an adapter. It does not replace @threadplane/chat, and it does not define a new chat runtime.
The package takes an AG-UI AbstractAgent, listens to its protocol events, and exposes the runtime-neutral Agent contract that the chat components already understand.
#The boundary
The important boundary is the Agent contract from @threadplane/chat.
Your components should depend on Agent, not on AG-UI transport details. That keeps the UI portable across AG-UI, LangGraph, and custom adapters.
toAgent(source) is the low-level boundary. It accepts any AG-UI AbstractAgent implementation:
Most Angular apps should use DI instead:
provideAgent() creates an AG-UI HttpAgent and registers the wrapped Agent under an internal DI token. Retrieve it with injectAgent().
You can also inject the token directly:
#Runtime data flow
toAgent() subscribes to source.subscribe({ onEvent, onRunFailed }).
Every AG-UI event is passed through the reducer. The reducer updates Angular signals:
messagesfor user, assistant, and reasoning content.status,isLoading, anderrorfor run lifecycle.toolCallsfor tool call starts, arguments, results, and completion.statefor AG-UI state snapshots and JSON Patch deltas.interruptcleared onRUN_STARTEDand set by theCUSTOMon_interruptevent.events$for custom events.
When the user submits input, the adapter builds a user message, appends it locally, adds it to the AG-UI source with source.addMessage(), then calls source.runAgent().
This is optimistic on purpose. The user message appears immediately while the backend starts the run.
#Live a2ui streaming via customEvents
The adapter exposes a customEvents signal on the agent returned by
toAgent / injectAgent, accumulating every non-on_interrupt CUSTOM
AG-UI event for the current run (reset on each RUN_STARTED). The chat
composition feature-detects this signal to drive progressive a2ui
surface rendering — token-by-token, as the backend streams a2ui-partial
events — matching the LangGraph adapter. Without it, a2ui still renders from
the final tool-call surface; with it, surfaces build up live.
The consuming side — how the chat composition turns these events into rendered surfaces — lives in chat's A2UI overview.
#Provider choices
Use provideAgent() when you have a real AG-UI HTTP endpoint.
The config maps to the AG-UI HttpAgent options exposed by this package, plus an optional telemetry sink:
| Option | Type | Description |
|---|---|---|
url | string | Required. AG-UI backend HTTP/SSE endpoint. |
agentId | string | Optional. Identifies a specific agent on the backend. |
threadId | string | Optional. Resume an existing conversation thread. |
headers | Record<string, string> | Optional. Custom request headers (auth, tracing). |
telemetry | AgentRuntimeTelemetrySink | false | Optional. App-owned telemetry sink — opt-in, emits nothing unless supplied. See @threadplane/telemetry. |
#Factory config for route params and DI
provideAgent() also accepts a () => AgentConfig factory. The factory runs inside an Angular injection context, so it can call inject() to read services or route params when it builds the config:
This is the cleanest way to derive threadId (or auth headers) from runtime state at construction time — see the threadId-recreation note below.
threadId here is a plain string consumed once at construction — the provider does not accept an Angular Signal, and the adapter does not observe changes to it at runtime. The AG-UI protocol carries events, not snapshots, and defines no server-side endpoint for "fetch the messages of thread X". To move a user between threads with their prior conversation restored, you have two options:
- Recreate the provider. Inject
provideAgent({ ..., threadId: newId })from a fresh injector when the active thread changes. Any prior message history must come from your own host service — pre-populatesetMessages()on the source before the adapter boots, or render a "loading…" surface while you fetch it. - Use the LangGraph adapter instead.
@threadplane/langgraphacceptsthreadId: Signal<string | null>and hydrates messages from the latest checkpoint on every change. See its Persistence guide. Use AG-UI when your runtime publishes events without checkpoint storage; use LangGraph when the server owns durable thread state.
Use provideFakeAgent() when you need the UI to run without a backend:
Use toAgent() directly when you own a custom AbstractAgent subclass or need to test a specific event stream.
#Lifecycle gotchas
The wrapped Agent does not own the AG-UI source lifecycle. toAgent() subscribes to the source and expects the source instance to live for the same lifetime as the adapter.
In Angular apps, prefer the provider API so the agent instance is scoped by DI. If you construct agents manually, create one adapter per source instance and keep that pairing stable.
stop() calls source.abortRun(). The actual cancellation behavior depends on the AG-UI source. HttpAgent implements abort behavior; a custom source may treat it as a no-op unless you implement cancellation.
regenerate(index) is supported by the shared Agent contract. It requires the target message to be an assistant message, finds the preceding user message, trims later messages, syncs the trimmed list back to the AG-UI source with setMessages(), and runs again. It throws if another run is loading.
#Current scope
The AG-UI adapter currently covers:
- Streaming assistant messages from
TEXT_MESSAGE_*. - Reasoning messages from
REASONING_MESSAGE_*. - Run status and errors from
RUN_*. - Tool calls from
TOOL_CALL_*. - Shared state from
STATE_SNAPSHOTandSTATE_DELTA. - Message replacement from
MESSAGES_SNAPSHOT. - Custom events from
CUSTOM. - Citations stored under
state.citations.
These features are intentionally out of scope for the AG-UI adapter today:
- Interrupt workflows.
- Subagents.
- History and time-travel. AG-UI is an event-stream protocol — it doesn't define a server-side "fetch state of thread X" endpoint, so the adapter can't hydrate prior messages on a
threadIdchange the way a checkpoint-aware runtime can. The Provider choices section above describes the two patterns AG-UI consumers use to work around this.
If those are central to your product, use the LangGraph adapter for that surface or build a custom adapter against the @threadplane/chat Agent contract. The Writing an Adapter guide walks through the thread-loading design choice in detail.
#Next steps
- Event Mapping shows exactly how AG-UI events map to
Agentfields. - Fake Agent covers offline demos and tests.
- Citations explains the
state.citationsbridge. - Troubleshooting covers common integration failures.