@threadplane/ag-ui
v0.3.2
Published
AG-UI adapter for the Angular AI agent UI — connect any AG-UI-compatible backend to the same chat surface.
Maintainers
Readme
@threadplane/ag-ui
The AG-UI adapter for Threadplane, the open-source thread-plane for agents. Wraps an AG-UI AbstractAgent into the runtime-neutral Agent contract that @threadplane/chat consumes, so any AG-UI-compatible backend drives the same chat surface.
Part of Threadplane.
Talking to LangGraph Platform directly? See
@threadplane/langgraph— same API shape, LangGraph SDK underneath.
What it does
- Bridges any AG-UI-compatible backend into the Threadplane chat surface via
toAgent(). - Supports AG-UI-compatible runtimes including LangGraph, CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, and AWS Strands.
- Exposes messages, status, tool calls, and raw AG-UI state as Angular Signals, plus
submit()/stop()/regenerate()actions — coverage depends on what the AG-UI backend emits. - Ships
FakeAgentandprovideFakeAgenttest doubles for unit testing without a live backend.
Install
npm install @threadplane/chat @threadplane/ag-ui @ag-ui/client @ag-ui/core markedPeer dependencies: @threadplane/chat: *, @angular/core: ^20.0.0 || ^21.0.0 || ^22.0.0, @ag-ui/client: ^1.0.1, @ag-ui/core: ^1.0.1, rxjs: ~7.8.0
marked is the required markdown parser peer used by @threadplane/chat when you render assistant messages through <chat>.
Quick start
Register the agent in your ApplicationConfig, then inject it into a component and bind it to <chat>.
// app.config.ts
import { provideAgent } from '@threadplane/ag-ui';
export const appConfig: ApplicationConfig = {
providers: [provideAgent({ url: 'https://your.agent.endpoint' })],
};// app.component.ts
import { Component } from '@angular/core';
import { ChatComponent } from '@threadplane/chat';
import { injectAgent } from '@threadplane/ag-ui';
@Component({
imports: [ChatComponent],
template: `<chat [agent]="agent" />`,
})
export class AppComponent {
protected readonly agent = injectAgent();
}Both @threadplane/langgraph and @threadplane/ag-ui expose provideAgent/injectAgent. Components using the neutral Agent contract can share their UI; provider configuration and adapter-specific extensions differ.
Capabilities
toAgent() translates AG-UI events into Angular Signals on the runtime-neutral Agent contract:
| Signal | Description |
|---|---|
| messages() | Chat message history |
| status() | 'idle' \| 'running' \| 'error' |
| isLoading() | True while a run is active |
| toolCalls() | In-progress and completed tool calls |
| error() | Last run error, if any |
| state() | Raw AG-UI state snapshot |
| customEvents() | Non-on_interrupt CUSTOM events for live a2ui and app-specific side effects |
| subagents() | ACTIVITY_* entries with activityType: 'subagent', projected to the neutral subagent contract |
| clientTools | Browser client-tool catalog, pending calls, and result resolution used by <chat [clientTools]> |
| rawEvents$ | Observable<BaseEvent> of every protocol event the adapter reduced, in order, for hosts that fold the full stream themselves. Suppressed events are not emitted; a run's terminal RUN_ERROR is emitted once, after error() and status() settle, redacted exactly as error() is when errors are protected; replayed hydration emits every replayed event, while persisted hydration restores a snapshot and emits nothing. Completes on dispose() |
Which capabilities populate depends on the events the AG-UI backend emits. submit(), stop(), and regenerate() are supported.
Restoring a thread
Configure replay to rebuild a reopened thread from the server's recorded AG-UI events. The adapter folds them through the live reducer, so messages, tool calls, reasoning, subagent cards and pending interrupts come back as the live view showed them. httpReplay({ url }) reads the common GET → { events } shape; a 404, an empty result or a failure falls back to persistence, which restores messages, state and the interrupt session only.
provideAgent({
url: '/api/agent',
threadId,
replay: httpReplay({ url: (id) => `/api/threads/${encodeURIComponent(id)}/events` }),
});Interrupts (human-in-the-loop)
agent.interrupt() is a Signal<AgentInterrupt | undefined> projected from native RUN_FINISHED interrupt outcomes or compatibility CUSTOM on_interrupt events. Native batches take display precedence in auto and protocol; explicit command profiles display the compatibility interrupt. String-serialized compatibility values are JSON-parsed automatically.
Resume with agent.submit({ resume }). Select interruptTransport in provideAgent() to match the backend:
| Profile | Resume transport |
| --- | --- |
| auto (default) | Prefers a native batch, including mixed native/compatibility delivery; otherwise detects Mastra correlation data or uses the legacy command. |
| protocol | Top-level resume entries, one per native interrupt ID. |
| legacy-command | forwardedProps.command.resume, as used by the LangGraph AG-UI bridge. |
| mastra-command | forwardedProps.command.resume plus command.interruptEvent containing the observed tool-call and run IDs. |
The current Mastra backend requires explicit mastra-command: it emits native and compatibility interrupts but consumes the command transport. Native cancellation uses { interruptId, status: 'cancelled' } without a payload; an application's { approved: false } is a resolved decision with backend-defined meaning.
Pair with <chat-approval-card> from @threadplane/chat for Approve and Cancel controls. This single-decision example uses a backend that expects { approved: boolean }:
import { Component } from '@angular/core';
import { ChatComponent, ChatApprovalCardComponent } from '@threadplane/chat';
import type { ChatApprovalAction } from '@threadplane/chat';
import { injectAgent } from '@threadplane/ag-ui';
@Component({
imports: [ChatComponent, ChatApprovalCardComponent],
template: `
<chat [agent]="agent" />
<chat-approval-card
[agent]="agent"
matchKind="refund_approval"
(action)="onAction($event)" />
`,
})
export class App {
protected readonly agent = injectAgent();
onAction(action: ChatApprovalAction) {
if (action === 'edit') return;
void this.agent.submit({ resume: { approved: action === 'approve' } });
}
}See cockpit/ag-ui/interrupts for a complete working example, and the LangGraph interrupts guide for that adapter's behavior. Both share interrupt() and submit({ resume }); AG-UI's interruptSession, ready, reconcileInterrupt(), dispose(), persistence configuration, and interruptGeneration submit option are adapter extensions. Browser persistence cannot restore a lost server checkpoint or prove backend completion.
Citations
bridgeCitationsState(thread, messages) populates Message.citations from AG-UI state. Citations live under the citations key of the agent state, keyed by message ID (state.citations[messageId]).
Example state shape:
{
"state": {
"citations": {
"msg-123": [
{
"id": "src1",
"index": 1,
"title": "Example Source",
"url": "https://example.com",
"snippet": "Relevant excerpt from the source..."
}
]
}
}
}Each citation supports id, index, title, url, snippet, and custom extra fields. The message ID key matches the corresponding message in the chat history.
Testing
// Fake backend — streams canned tokens, no server:
import { provideFakeAgent } from '@threadplane/ag-ui';
providers: [provideFakeAgent({ tokens: ['Hello', ' world'] })];For component/unit tests, use the neutral writable-signal mock mockAgent()
from @threadplane/chat — the AG-UI agent is the neutral Agent contract,
so there is no adapter-specific mock. See
Choosing an adapter → Testing.
Reliability
@threadplane/ag-ui shares the same runtime-neutral Agent contract as @threadplane/langgraph, making it interchangeable at the <chat [agent]> binding. The library follows a patch-only 0.0.x release policy. The CI job "Library — lint / test / build" runs lint, test, and build on every pull request.
Documentation
Installation collection
This package includes automatic first-party installation collection, including CI. It reports the package/version, basic execution environment, a random installation identifier, configured Git display name and full email, and a recognized repository hosting provider/owner when available. CI is labeled separately; install counts are not developer counts, and Git identity is an unverified hint. A usable install email can qualify for the generic founder hello after a linked development activation; this does not verify email ownership, employment, or account membership. Existing unsubscribe, reply, bounce, and suppression controls still apply.
An enabled non-CI install writes a random package-local correlation token to the
./development-install export. It contains no email or Git metadata and is separate
from the home installation ID. Published packages contain null; development runtime
events can carry the installed token. Production bundles remove it and do not collect
runtime events. CI alone cannot create an eligible bridge or trigger a hello.
Disabled and CI lifecycle runs first try to reset an earlier token to null. Copied or cached packages can retain tokens: read-only packages may prevent that reset, and skipped scripts leave existing files unchanged. A token therefore does not prove a unique installation or developer. The independent browser opt-out below still stops runtime transmission, including when a stale token remains.
Set DO_NOT_TRACK=1 or TPLANE_TELEMETRY_DISABLED=1 before installing to disable
collection before identity reads, persistence, or network. Package-manager controls
such as --ignore-scripts also prevent the hook from running. Installation succeeds
independently of collection, with one request and a five-second execution budget.
The random ID is stored at ~/.threadplane/installation-id where writable; otherwise
it lasts for one invocation. Git includes, system config, and command/environment
identity overrides are not inspected. Unsupported checkout/configuration layouts,
blocked scripts or network, and reused caches can leave gaps or duplicate identities.
Raw repository URLs/names, local paths, source code, credentials, and application
content are excluded from install reports. See Privacy.
License
MIT. See LICENSE.
Development browser collection
Supported LangGraph/AG-UI runtime use and real JSON-render component mounts automatically
report development progress to Threadplane. Angular development mode and browser APIs are
required. Production builds, SSR, imports, and automated browsers reporting
navigator.webdriver are inert. Creating an agent (or a render element) in a
development-mode browser reports one session start per integration per session; milestones
are reported only when the runtime is actually used.
Reports contain package/version, integration, closed milestones, timestamps, a random
browser-origin ID, and a session with a 30-minute inactivity boundary. They exclude
prompts, messages, application state, private URLs, thread/run IDs, and credentials.
The IDs describe an origin/session, not a verified person or repository.
Set adapter telemetry: false to disable automatic collection; a custom sink replaces
the automatic destination while preserving its existing callbacks. Chat carries that
choice into nested JSON renderers. Standalone render supports
provideRender({ telemetry: false }) or <render-spec [telemetry]="false" ... />.
For a page-wide control, call setDevelopmentCollectionEnabled(false) from
@threadplane/telemetry/browser before creating runtimes. From the browser console,
run localStorage.setItem('THREADPLANE_TELEMETRY_DISABLED', '1') and reload.
Node environment variables do not configure a compiled browser app.
The credential-free announcement exchange records progress before returning optional plain-text console announcements. No click or registration is required. It has bounded queues/retries and a three-second request deadline; collection failures do not affect application use. A linked eligible install email can receive the generic founder hello; runtime evidence alone does not approve a contact. See the telemetry controls and collection details and Privacy.
