Choosing an adapter
Threadplane ships two adapters that connect a backend agent runtime to <chat> in @threadplane/chat. Both produce the same runtime-neutral Agent contract; pick one based on what your backend speaks.
At a glance
| If your backend is... | Use |
|---|---|
| LangGraph Platform (cloud or self-hosted, via the LangGraph SDK) | @threadplane/langgraph |
| Any AG-UI-protocol backend — CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, or LangGraph fronted by AG-UI | @threadplane/ag-ui |
Code comparison
The two adapters share the same public surface — provideAgent, injectAgent, and an adapter-specific AgentConfig. Component code stays the same; the provider import and config shape change.
LangGraph adapter
import { provideAgent, injectAgent } from '@threadplane/langgraph';
import { ChatComponent } from '@threadplane/chat';
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideAgent({
apiUrl: 'https://your-langgraph-deployment',
assistantId: 'your-graph-name',
}),
],
};
// component
@Component({ imports: [ChatComponent], template: `<chat [agent]="agent" />` })
export class App {
protected readonly agent = injectAgent();
}AG-UI adapter
import { provideAgent, injectAgent } from '@threadplane/ag-ui';
import { ChatComponent } from '@threadplane/chat';
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideAgent({ url: 'https://your.agent.endpoint' })],
};
// component
@Component({ imports: [ChatComponent], template: `<chat [agent]="agent" />` })
export class App {
protected readonly agent = injectAgent();
}The component body is byte-identical. The only thing that changes is the import path and the config shape passed to provideAgent().
What about both?
Mixing both adapters in one app is unusual — most apps talk to one backend. If you genuinely need both, use TypeScript import renaming:
import { provideAgent as provideLangGraphAgent } from '@threadplane/langgraph';
import { provideAgent as provideAgUiAgent } from '@threadplane/ag-ui';Each adapter uses its own private DI token internally, so providing both does not cause a runtime DI collision — but the developer ergonomics get awkward, and <chat [agent]="agent" /> can only bind one Agent at a time. Pick one adapter per app.
Testing
Three layers of test doubles, smallest scope first:
| Layer | Use | What is real | When |
|---|---|---|---|
| Contract mock | mockAgent() (chat) / mockLangGraphAgent() (langgraph) | nothing — you set messages(), status() directly | component/unit tests |
| Fake backend | provideFakeAgent({ tokens }) (both adapters) | the real adapter pipeline; canned wire events | adapter-integration tests, in-browser, no server |
| LLM fixture replay | aimock (workspace examples e2e) | the whole stack incl. a real graph; only the LLM is replayed | full end-to-end |
Reach for the smallest layer that covers your test. Do not stand up aimock when a mockAgent unit test suffices, and do not hand-roll a transport when provideFakeAgent exists.
Measured runtime support
The AG-UI adapter is protocol-first, so "any AG-UI backend plugs in" is a claim that can be tested rather than asserted. It was tested against three runtimes that have nothing to do with LangGraph, in two languages — first on 2026-08-31, and most recently on 2026-09-02 for the subagent cells.
| Runtime | Messages | Tool calls | State | Interrupts | Subagents |
|---|---|---|---|---|---|
| LangGraph (via the AG-UI bridge) | Yes | Yes | Yes | Yes | Yes |
| AWS Strands (Python) | Yes | Yes | Partial | Yes | Yes |
| Microsoft Agent Framework (Python) | Yes | Yes | Yes | Yes | Yes |
| Mastra (TypeScript) | Yes | Yes | Yes | Yes | Yes |
What sits behind the cells that needed work:
- AWS Strands — State is partial because of the upstream bridge (explained below). Subagents work through a small in-tree emitter that translates native delegation signals to the protocol's
SUBAGENT_*events. - Microsoft Agent Framework — Subagents work through the same kind of in-tree emitter, translating native delegation signals to
SUBAGENT_*events. - Mastra — Subagents work through a small in-tree emitter that observes the runtime's public stream through a tee and emits
SUBAGENT_*events plus the child's token deltas, because the runtime's bridge drops child output.
Every gap in that table falls into one of three causes, and the distinction matters more than the cell:
- Protocol — the AG-UI event vocabulary has no way to express the capability. Nothing an adapter or a backend author can do about it today.
- Upstream integration — the protocol can express it, but the runtime's AG-UI bridge does not emit the events. Fixable upstream.
- This adapter —
@threadplane/ag-uifailed to consume something the wire already carried. A Threadplane bug, and a Threadplane responsibility.
No cell in the current matrix is caused by the protocol.
The protocol carries first-class subagent events (SUBAGENT_STARTED, SUBAGENT_FINISHED, SUBAGENT_ERROR, and a subagentRunId attribution field on content events), and @threadplane/ag-ui consumes them; a backend reaches them with a small emitter at its bridge boundary.
Reading the individual cells
State on AWS Strands is partial.
The Strands AG-UI bridge never emits STATE_DELTA, and outbound state exists only where a tool opts in through per-tool ToolBehavior hooks.
Because the adapter applies a STATE_SNAPSHOT as a full replacement, every hook must return the complete state object; a partial return clobbers sibling keys.
Shared state works on Strands, but it is snapshot-only and it is opt-in per tool.
Interrupts arrive two different ways.
AWS Strands and Microsoft Agent Framework signal an interrupt only through the protocol-standard RUN_FINISHED outcome, { type: 'interrupt', interrupts: [...] }.
The LangGraph bridge signals it only through a CUSTOM event named on_interrupt.
Mastra emits both.
The adapter retains both conventions in one interrupt session. In auto mode the native batch takes precedence regardless of arrival order.
Resume payloads are not portable, so the adapter shapes them per runtime.
Mastra reads the decision from forwardedProps.command.resume and the suspended tool's identifiers from command.interruptEvent. Configure interruptTransport: 'mastra-command' for this integration because it also emits native outcomes.
AWS Strands and Microsoft Agent Framework both read the protocol-standard top-level resume array. The adapter requires every pending interrupt ID exactly once. Resolved entries carry the decision as payload; cancelled entries omit it.
The LangGraph bridge reads forwardedProps.command.resume.
You pass one neutral submit({ resume }), and the adapter shapes the request using the observed batch and configured transport profile. The backend still defines what the decision payload means.
Recovery belongs to the adapter and backend.
AG-UI can restore a thread by replaying its protocol events from a server that kept them, or restore client state through an application-owned persistence store with atomic compare-and-swap and authoritative reconciliation; only replay brings back reasoning and subagent detail. The native LangGraph adapter uses LangGraph's thread and checkpoint APIs. Neither a shared Angular approval component nor a stored client record guarantees exactly-once server execution or recreates a lost checkpoint.
The Mastra row is hosted on its own lane. Its cells come from a real Mastra server driven with live model calls, and its transcripts are committed and replayed like the others. Unlike the Strands and Microsoft Agent Framework rows, it is not served by the shared FastAPI deployment: upstream ships no plain AG-UI HTTP endpoint, so its backend is a separate Node service.
Subagents work through one emitter per backend.
The protocol standardized the events — SUBAGENT_STARTED, SUBAGENT_FINISHED, SUBAGENT_ERROR, plus subagentRunId attribution on ordinary content events — and @threadplane/ag-ui consumes them directly.
What differs per runtime is how the emitter learns about the delegation.
Strands surfaces the specialist's tool use and streamed tokens through a per-tool stream handler, so the card streams live.
On Microsoft Agent Framework the specialist's updates stream in-process into the tool body, and the in-tree emitter merges them across the bridge boundary, so the card streams live there too.
Mastra streams the child's chunks in-process on the parent stream, but its bridge drops them and withholds the delegation tool call until the child resolves; the in-tree emitter therefore observes the runtime's public stream through a tee ahead of the bridge, emits the delegation tool call eagerly, and forwards the child's deltas under the subagent identity, so the card mounts while the child runs and streams live there too.
All three cards stream live; the Mastra card streams through the tee rather than through the bridge, a property of the runtime's bridge rather than of the protocol or the adapter.
Each demo backend ships its emitter in tree, roughly 200 to 430 lines per runtime.
How this was measured
Each runtime got a real server, started against the upstream AG-UI integration and driven with live model calls.
The raw Server-Sent Events were captured off the wire, then replayed event-for-event through the pinned @ag-ui/client — schema parse plus verifyEvents — and through the adapter reducer itself.
The captures for messages, tool calls, state, and interrupts are committed at libs/ag-ui/fixtures/runtime-transcripts/, verbatim from the wire, and the adapter test suite replays them on every run.
The subagent captures are committed as a wire-capture-subagents.md document alongside each backend under cockpit/runtimes/<runtime>/; the subagent transcript the suite replays is synthetic, built to the event shape those captures record.
Runnable examples
Each runtime has a standalone Angular example bound through @threadplane/ag-ui, with its own backend:
cockpit/runtimes/aws-strands— Python, hosted in the shared FastAPI deployment.cockpit/runtimes/microsoft-agent-framework— Python, hosted in that same FastAPI deployment, running beside the Strands and LangGraph topics in one process.cockpit/runtimes/mastra— TypeScript, hosted by a Node AG-UI service, because the upstream Mastra integration ships an in-process bridge rather than an HTTP endpoint.
In all three, the component binds the agent through injectAgent() and renders <chat [agent]="agent" />; the shared-state panels and approval cards around it are demo-specific, and each example wraps provideAgent() in a factory only because it resolves its endpoint at runtime.
Why two adapters?
The Agent contract from @threadplane/chat is intentionally runtime-neutral. The two adapters exist because they bridge different on-the-wire protocols into that contract:
@threadplane/langgraphtalks to the LangGraph SDK directly. You get LangGraph-specific features (thread state, branching, multitask strategies) typed end-to-end.@threadplane/ag-uiconsumes the AG-UI event protocol. Any backend that emits AG-UI events plugs in, regardless of what graph engine sits behind it.
Both adapters are MIT-licensed and live in the same monorepo.
Frequently asked
Does Threadplane require LangGraph? No. @threadplane/ag-ui connects any AG-UI-compatible backend, and @threadplane/langgraph is the direct LangGraph adapter.
What is the difference between the two adapters? See Why two adapters? above and the measured runtime support table.
How does Threadplane differ from a raw streaming SDK? A streaming SDK gives you events. Threadplane gives you the Angular state model, chat UX, threads, approvals, generated UI, recovery, and tests on top of them.
Next steps
- LangGraph quickstart if your backend is LangGraph Platform and you want native streaming, checkpoints, threads, interrupts, and history APIs.
- AG-UI quickstart if your backend emits the AG-UI protocol from CrewAI, Mastra, or another compatible runtime.
- Chat quickstart if you want to start from the drop-in
<chat>surface and wire the adapter afterward.