Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ A committed transcript replayed through the real client proves what the runtime
| Runtime | Messages | Tool calls | State | Interrupts | Subagents |
| --- | --- | --- | --- | --- | --- |
| LangGraph (baseline) | Yes | Yes | Yes | Yes | Yes |
| AWS Strands | Yes | Yes | Partial | Yes | No |
| Microsoft Agent Framework | Yes | Yes | Yes | Yes | No |
| Mastra | Yes | Yes | Yes | Yes | No |
| AWS Strands | Yes | Yes | Partial | Yes | Yes |
| Microsoft Agent Framework | Yes | Yes | Yes | Yes | Yes |
| Mastra | Yes | Yes | Yes | Yes | Partial |

Messages, tool calls, and shared state crossed three non-LangGraph runtimes with zero changes to the adapter.
Not one line.
Expand Down Expand Up @@ -121,9 +121,9 @@ Both defects share a root.
Neither would have been found by another demo, another end-to-end test, or another review of our own code.
They were only findable by pointing the adapter at software written by people who had never heard of us.

## What stayed red
## What stayed partial

Two cells did not go green.
Two cells did not go fully green.

**Shared state on Strands is partial.**
Its bridge sends whole-document snapshots and never sends a patch, and a tool only contributes state if it opts in through a per-tool hook.
Expand All @@ -134,25 +134,16 @@ That is a real hazard.
It is documented in the example.
The cause is the upstream bridge, not the protocol and not us.

**Subagents are red for all three.**
This one surprised me, and it is the finding I would most want another framework author to read.
**Subagents were the last column to converge, and the fix was one seam per runtime.**

It is tempting to write that as three bugs.
It is not.
Strands models delegation as a handoff custom event alongside step boundaries.
Mastra treats the activity event family as belonging to background work and observational memory.
That is a defensible reading of the same specification.
Microsoft Agent Framework does emit activity snapshots, but at the granularity of an executor, and it never emits the incremental form.
The protocol standardized the events — `SUBAGENT_STARTED`, `SUBAGENT_FINISHED`, `SUBAGENT_ERROR`, plus a `subagentRunId` attribution field on ordinary content events — and the adapter consumes them directly.
What no runtime does is emit them natively.
Each one reports delegation in its own dialect: Strands surfaces the specialist's tool use and forwards its token stream, Microsoft Agent Framework streams the specialist's updates in-process into the tool body where the in-tree emitter merges them across the bridge boundary, and Mastra reports the delegation through its tool frames and returns the child's final text.
So each demo backend carries a small emitter — roughly 125 to 310 lines each — that translates its runtime's dialect into the standard events at the bridge boundary.

Three runtimes, three different mental models of what a subagent is, and none of them wrong on their own terms.
Meanwhile our own projection keys on an activity type of `subagent`, which is a convention our demo backends adopt and nobody else has any reason to.
`@ag-ui/core` 0.0.59 added dedicated subagent events.
As of this measurement, no runtime emits them.

I think that is an ecosystem finding, not a defect report.
Delegation is the surface where the AG-UI vocabulary is agreed on in syntax and not yet in meaning.
If you are building on server-declared subagents today, build on a backend you control.
You are ahead of the ecosystem, not portable across it.
Two of the three cards stream live.
Mastra's fills in at completion, because its bridge does not forward child tokens to the wire; that is the one Partial cell, and it belongs to the runtime's bridge rather than to the protocol or the adapter.
If you are building on server-declared subagents today, the contract to target is the protocol's own events; the emitter is the per-runtime cost, and it is small.

## What the deploy check found

Expand Down Expand Up @@ -180,13 +171,14 @@ That is now measured against three implementations in two languages.

Interrupts are portable as of these fixes, and they were not before, in a way no amount of internal testing would have revealed.

Subagents are not portable today, and the reason is upstream disagreement rather than a missing feature on anyone's list.
Subagents are portable through a per-runtime emitter that speaks the protocol's own events, streaming on two of the three runtimes and lifecycle-plus-final-text on the third.

The matrix now lives in the [adapter guide](/docs/choosing-an-adapter), with a cause column on every gap.
Split three ways: the protocol cannot express it, the upstream bridge does not emit it, or our adapter failed to consume it.
That third category is the one I care about keeping honest, and it is currently empty.

The transcripts are committed as test fixtures, verbatim from the wire.
The transcripts for messages, tool calls, state, and interrupts are committed as test fixtures, verbatim from the wire.
The subagent captures are committed as wire-capture documents alongside each backend, and the subagent transcript the suite replays is synthetic, built to the event shape those captures record.
Interrupt detection and resume shaping are now pinned to what three real runtimes actually sent us on a specific day.
The next regression in either one fails a test.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Messages and state live in memory on the client.
| --- | --- | --- | --- |
| Streaming chat, tool calls, generative UI | Yes | Yes | Parity |
| Interrupts / approvals | Yes, first-class | Yes, via a `CUSTOM` event | Protocol — AG-UI defines no interrupt event |
| Subagent delegation | Yes, inferred client-side | Yes, server-declared | Protocol — and AG-UI's model is better |
| Subagent delegation | Yes, inferred client-side (with server-announced bindings) | Yes, server-declared via the protocol's `SUBAGENT_*` events | Protocol — dedicated subagent events exist on the wire |
| Thread list, rename, delete | Yes | No | Protocol — no thread CRUD exists |
| Reload and restore a conversation | Yes | No | Protocol — no "fetch state of thread X" |
| Checkpoint history, time travel, branching | Yes | No | Protocol — no checkpoint concept |
Expand Down Expand Up @@ -293,17 +293,18 @@ So what our parity data demonstrates is a **transport swap over one runtime**.
It is not a runtime swap.

That distinction matters for anyone reading this as a portability argument.
Whether the neutral `Agent` contract holds up against a genuinely non-LangGraph AG-UI backend — CrewAI, Mastra, Pydantic AI, something you wrote — is untested by us.
Whether the neutral `Agent` contract holds up against a non-LangGraph AG-UI backend we have not measured — CrewAI, Pydantic AI, something you wrote — is untested by us.
The backends we have measured since, Mastra among them, are covered in [We Measured the Runtime Swap](/blog/we-measured-the-runtime-swap).
I believe it holds, because the contract is built on the protocol event vocabulary rather than on any runtime's shapes.
But belief is not measurement, and I am not going to dress one up as the other.

Two smaller caveats.
The interrupt path currently depends on a `CUSTOM` event name that the LangGraph bridge emits, so an unrelated AG-UI backend would need to adopt that convention.
And the subagent path depends on the backend emitting native `ACTIVITY` events, which our demo backend does deliberately.
The interrupt path accepts two conventions — the protocol-standard `RUN_FINISHED` interrupt outcome, and the `CUSTOM` `on_interrupt` event the LangGraph bridge emits — and within a single run the first signal wins.
And the subagent path rests on the protocol's `SUBAGENT_*` events, which the adapter consumes directly; each measured runtime reaches them through a small in-tree emitter, as [the measurement post](/blog/we-measured-the-runtime-swap#what-stayed-partial) records.

*Editor's note, added after publication: we went and measured it.*
*Three genuinely non-LangGraph backends, two languages, wire transcripts replayed through the shipped client.*
*The results, including two adapter defects the exercise exposed, are in [We Measured the Runtime Swap](/blog/we-measured-the-runtime-swap).*
*Editor's note: the measurement exists.*
*Three genuinely non-LangGraph backends, two languages, wire transcripts replayed through the shipped client — covering messages, tool calls, state, interrupts, and subagents.*
*The results are in [We Measured the Runtime Swap](/blog/we-measured-the-runtime-swap).*

## Conclusion

Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/ag-ui/api/inject-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ The AG-UI adapter extends the neutral `Agent` contract with AG-UI-specific proto
|-------|------|-------------|
| `customEvents()` | `CustomStreamEvent[]` | Custom events emitted by the backend during a run. Accumulates per run; resets on each new `submit()`. |
| `clientTools` | `ClientToolsCapability` | Browser client-tool catalog, pending calls, and result resolution used by `<chat [clientTools]>`. |
| `subagents()` | `Map<string, Subagent>` | `ACTIVITY_*` entries with `activityType: 'subagent'`, projected to the neutral subagent contract and keyed by `messageId`. |
| `subagents()` | `Map<string, Subagent>` | Subagent runs from `SUBAGENT_*` events, keyed by `subagentRunId`, plus the legacy `ACTIVITY_*` convention (`activityType: 'subagent'`, keyed by `messageId`), projected to the neutral subagent contract. |

`injectAgent()` returns the `AgUiAgent` type — the neutral `Agent` contract plus these AG-UI-specific fields — so they are reachable directly, no cast required:

Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/ag-ui/api/to-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const agent = toAgent(source, { telemetry: myTelemetrySink });
|-------|------|-------------|
| `customEvents()` | `Signal<CustomStreamEvent[]>` | Custom events accumulated during a run. Resets at the start of each new run. |
| `clientTools` | `ClientToolsCapability` | Browser client-tool catalog, pending calls, and result resolution. The chat composition uses this when you pass `<chat [clientTools]>`. |
| `subagents()` | `Signal<Map<string, Subagent>>` | `ACTIVITY_*` events with `activityType: 'subagent'`, projected to the neutral subagent contract and keyed by `messageId`. |
| `subagents()` | `Signal<Map<string, Subagent>>` | Subagent runs from `SUBAGENT_*` events, keyed by `subagentRunId`, plus the legacy `ACTIVITY_*` convention (`activityType: 'subagent'`, keyed by `messageId`), projected to the neutral subagent contract. |

The standard `Agent` signals (`messages`, `status`, `isLoading`, `error`, `toolCalls`, `state`, `interrupt`) and actions (`submit`, `stop`, `regenerate`) are all present.

Expand Down
4 changes: 2 additions & 2 deletions apps/website/content/docs/ag-ui/concepts/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Every AG-UI event is passed through the reducer. The reducer updates Angular sig
- `interrupt` cleared on `RUN_STARTED` and set by the `CUSTOM` `on_interrupt` event.
- `events$` for runtime-neutral custom-event side effects.
- `customEvents` for accumulated non-`on_interrupt` `CUSTOM` events used by live a2ui and app-specific reactive UI.
- `subagents` for `ACTIVITY_*` events with `activityType: 'subagent'`.
- `subagents` for `SUBAGENT_*` events and for `ACTIVITY_*` events with `activityType: 'subagent'`.

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()`.

Expand Down Expand Up @@ -183,7 +183,7 @@ The AG-UI adapter currently covers:
- Custom events from non-`on_interrupt` `CUSTOM` events, surfaced through both `events$` and `customEvents`.
- Interrupts from `CUSTOM` events named `on_interrupt`.
- Browser client tools via `AgUiAgent.clientTools`.
- Subagent progress from `ACTIVITY_SNAPSHOT` and `ACTIVITY_DELTA` events whose `activityType` is `subagent`.
- Subagent progress from `SUBAGENT_STARTED`/`SUBAGENT_FINISHED`/`SUBAGENT_ERROR` events with `subagentRunId`-attributed content, and from `ACTIVITY_SNAPSHOT`/`ACTIVITY_DELTA` events whose `activityType` is `subagent`.
- Citations stored under `state.citations`.

These features are intentionally out of scope for the AG-UI adapter today:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Here's what the first release handles:
- `state` (snapshots and JSON-Patch deltas)
- `events$` (custom events; discriminates `state_update`)
- Interrupts (from `CUSTOM` events named `on_interrupt`)
- Subagent/activity progress (from `ACTIVITY_*` events)
- Subagent progress (from `SUBAGENT_*` events, or `ACTIVITY_*` with `activityType: 'subagent'`)

Out of scope for now (use `@threadplane/langgraph` if you need LangGraph Platform-specific APIs):
- History / time-travel
Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/ag-ui/guides/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Pass the index of the assistant message you want to replace, not the user messag

History and time-travel are not implemented by the AG-UI adapter today.

Current scope is messages, status/loading/error, tool calls, state/custom events, reasoning messages, message snapshots, interrupts from `CUSTOM` `on_interrupt` events, subagent/activity progress from `ACTIVITY_*` events, and citations from state.
Current scope is messages, status/loading/error, tool calls, state/custom events, reasoning messages, message snapshots, interrupts from `CUSTOM` `on_interrupt` events, subagent progress from `SUBAGENT_*` and `ACTIVITY_*` events, and citations from state.

Use `@threadplane/langgraph` if you need LangGraph Platform thread history APIs, or write a custom adapter against the `@threadplane/chat` `Agent` contract if you need AG-UI plus product-specific behavior.

Expand Down
20 changes: 19 additions & 1 deletion apps/website/content/docs/ag-ui/reference/event-mapping.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,24 @@ If `message` is omitted, no user message is appended, but `runAgent()` still run

`ACTIVITY_SNAPSHOT` and `ACTIVITY_DELTA` events project into the AG-UI-specific `subagents()` signal. Use that signal for progress cards and nested task views backed by AG-UI activity streams.

### `SUBAGENT_*` events

The adapter also consumes the protocol's first-class subagent lifecycle events:

| Event | Required fields | Optional fields | Effect |
|---|---|---|---|
| `SUBAGENT_STARTED` | `subagentRunId`, `name` | `description`, `parentSubagentRunId`, `parentToolCallId`, `parentMessageId` | Creates (or fills in) the subagent's entry with status `running`. |
| `SUBAGENT_FINISHED` | `subagentRunId` | `result`, `outcome` (`{ type: 'success' }` or `{ type: 'suspended', interruptIds? }`) | Marks the entry `complete`; a `suspended` outcome keeps it `running`. |
| `SUBAGENT_ERROR` | `subagentRunId`, `message` | `code` | Marks the entry `error` and records the message. |

Alongside the lifecycle events, ordinary content events (`TEXT_MESSAGE_START/CONTENT/END` and `TOOL_CALL_START/ARGS/END/RESULT`) may carry a `subagentRunId` attribution field. The routing rule: an attributed text or tool event feeds the child's card, never the parent transcript. An attributed content event that arrives before `SUBAGENT_STARTED` still gets a card, which `SUBAGENT_STARTED` then fills in with identity.

Entries in the `subagents()` map are keyed by `subagentRunId`. The `Subagent.toolCallId` field carries `parentToolCallId ?? subagentRunId` and is what anchors the card to its delegation tool call in `<chat>`.

A `suspended` outcome keeps the card in the `running` state: the subagent run resumes under the same `subagentRunId`, and the interrupt itself surfaces through the runtime-neutral `interrupt()` signal, not through the card.

The legacy convention — `ACTIVITY_SNAPSHOT`/`ACTIVITY_DELTA` with an `activityType` of `subagent` — remains supported and projects into the same signal.

## Unsupported protocol areas

The adapter supports `CUSTOM` `on_interrupt` events for the runtime-neutral `interrupt()` signal, and it supports ACTIVITY-backed subagent progress. It does not implement history or time-travel. Unknown protocol events are ignored rather than treated as errors.
The adapter supports `CUSTOM` `on_interrupt` events for the runtime-neutral `interrupt()` signal, and it supports subagent progress through both the `SUBAGENT_*` events and the ACTIVITY-backed convention. It does not implement history or time-travel. Unknown protocol events are ignored rather than treated as errors.
Loading
Loading