diff --git a/apps/website/content/blog/2026-08-31-we-measured-the-runtime-swap.mdx b/apps/website/content/blog/2026-08-31-we-measured-the-runtime-swap.mdx index 6b8074848..309efb2ee 100644 --- a/apps/website/content/blog/2026-08-31-we-measured-the-runtime-swap.mdx +++ b/apps/website/content/blog/2026-08-31-we-measured-the-runtime-swap.mdx @@ -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. @@ -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. @@ -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 @@ -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. diff --git a/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx b/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx index 2189bc5f6..f5b9ddd62 100644 --- a/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx +++ b/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx @@ -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 | @@ -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 diff --git a/apps/website/content/docs/ag-ui/api/inject-agent.mdx b/apps/website/content/docs/ag-ui/api/inject-agent.mdx index 8f4547233..368960b5f 100644 --- a/apps/website/content/docs/ag-ui/api/inject-agent.mdx +++ b/apps/website/content/docs/ag-ui/api/inject-agent.mdx @@ -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 ``. | -| `subagents()` | `Map` | `ACTIVITY_*` entries with `activityType: 'subagent'`, projected to the neutral subagent contract and keyed by `messageId`. | +| `subagents()` | `Map` | 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: diff --git a/apps/website/content/docs/ag-ui/api/to-agent.mdx b/apps/website/content/docs/ag-ui/api/to-agent.mdx index 9929363ac..bf66004f9 100644 --- a/apps/website/content/docs/ag-ui/api/to-agent.mdx +++ b/apps/website/content/docs/ag-ui/api/to-agent.mdx @@ -30,7 +30,7 @@ const agent = toAgent(source, { telemetry: myTelemetrySink }); |-------|------|-------------| | `customEvents()` | `Signal` | 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 ``. | -| `subagents()` | `Signal>` | `ACTIVITY_*` events with `activityType: 'subagent'`, projected to the neutral subagent contract and keyed by `messageId`. | +| `subagents()` | `Signal>` | 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. diff --git a/apps/website/content/docs/ag-ui/concepts/architecture.mdx b/apps/website/content/docs/ag-ui/concepts/architecture.mdx index b4778c981..0a5acdb1f 100644 --- a/apps/website/content/docs/ag-ui/concepts/architecture.mdx +++ b/apps/website/content/docs/ag-ui/concepts/architecture.mdx @@ -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()`. @@ -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: diff --git a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx index c4b4bc279..c3306c20c 100644 --- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx @@ -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 diff --git a/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx b/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx index fd7d89d75..a46fb7099 100644 --- a/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx +++ b/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx @@ -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. diff --git a/apps/website/content/docs/ag-ui/reference/event-mapping.mdx b/apps/website/content/docs/ag-ui/reference/event-mapping.mdx index 3f2eb3543..4f7448e62 100644 --- a/apps/website/content/docs/ag-ui/reference/event-mapping.mdx +++ b/apps/website/content/docs/ag-ui/reference/event-mapping.mdx @@ -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 ``. + +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. diff --git a/apps/website/content/docs/choosing-an-adapter/index.mdx b/apps/website/content/docs/choosing-an-adapter/index.mdx index 2e17a328f..b711d1aa4 100644 --- a/apps/website/content/docs/choosing-an-adapter/index.mdx +++ b/apps/website/content/docs/choosing-an-adapter/index.mdx @@ -87,14 +87,14 @@ Reach for the smallest layer that covers your test. Don't stand up aimock when a ## 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. -On 2026-08-31 we tested it against three runtimes that have nothing to do with LangGraph, in two languages. +We tested it 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 | Cause of any gap | |---|---|---|---|---|---|---| | **LangGraph** (via the AG-UI bridge) | Yes | Yes | Yes | Yes | Yes | — | -| **AWS Strands** (Python) | Yes | Yes | Partial | Yes | No | State + subagents: upstream | -| **Microsoft Agent Framework** (Python) | Yes | Yes | Yes | Yes | No | Subagents: upstream | -| **Mastra** (TypeScript) | Yes | Yes | Yes | Yes | No | Subagents: upstream | +| **AWS Strands** (Python) | Yes | Yes | Partial | Yes | Yes | State: upstream. Subagents: a small in-tree emitter translates native delegation signals to the protocol's `SUBAGENT_*` events | +| **Microsoft Agent Framework** (Python) | Yes | Yes | Yes | Yes | Yes | Subagents: a small in-tree emitter translates native delegation signals to the protocol's `SUBAGENT_*` events | +| **Mastra** (TypeScript) | Yes | Yes | Yes | Yes | Partial | Subagents: lifecycle and final text via the emitter; the runtime's bridge does not forward child token streams | Every gap in that table falls into one of three causes, and the distinction is the point of the column: @@ -102,7 +102,8 @@ Every gap in that table falls into one of three causes, and the distinction is t - **Upstream integration** — the protocol can express it, but the runtime's AG-UI bridge does not emit the events. Fixable upstream. - **Our adapter** — `@threadplane/ag-ui` failed to consume something the wire already carried. Our bug, and our responsibility. -No cell in the current matrix is caused by the protocol, and no cell is caused by a bug in our adapter — the two that were are fixed. +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 @@ -127,22 +128,21 @@ You pass one neutral `submit({ resume })`, and the adapter derives the wire shap 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 are red for every third-party runtime, and none of those reds are a bug.** -The three runtimes do not fail to implement one thing; they model delegation three different ways. -Strands routes handoffs through a `CUSTOM` event plus step events, with no `ACTIVITY` events at all. -Mastra reserves `ACTIVITY_*` for background tasks and observational memory. -Microsoft Agent Framework emits coarse executor-level activity snapshots and constructs `ACTIVITY_DELTA` nowhere. -The Threadplane subagent projection keys on an `activityType` of `subagent`, which is a convention our own demo backend adopts. -The protocol has carried dedicated `SUBAGENT_STARTED`, `SUBAGENT_FINISHED`, and `SUBAGENT_ERROR` events since `@ag-ui/core` 0.0.59, and no runtime emits them yet. -Treat server-declared subagents as a capability of LangGraph and of backends you control, until that changes. +**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 reports delegation through its tool frames and returns the child's final text; its bridge does not forward child tokens, so the card fills in at completion — the one Partial cell, a property of the runtime's bridge rather than of the protocol or the adapter. +Each demo backend ships its emitter in tree, roughly 125 to 310 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 are committed at [`libs/ag-ui/fixtures/runtime-transcripts/`](https://github.com/cacheplane/angular-agent-framework/tree/main/libs/ag-ui/fixtures/runtime-transcripts), verbatim from the wire. -The adapter test suite replays them on every run. +The captures for messages, tool calls, state, and interrupts are committed at [`libs/ag-ui/fixtures/runtime-transcripts/`](https://github.com/cacheplane/angular-agent-framework/tree/main/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//`; the subagent transcript the suite replays is synthetic, built to the event shape those captures record. ### Runnable examples diff --git a/apps/website/content/docs/runtimes/aws-strands/how-it-connects.mdx b/apps/website/content/docs/runtimes/aws-strands/how-it-connects.mdx index 286d5d27c..631b304b3 100644 --- a/apps/website/content/docs/runtimes/aws-strands/how-it-connects.mdx +++ b/apps/website/content/docs/runtimes/aws-strands/how-it-connects.mdx @@ -67,11 +67,10 @@ StrandsAgentConfig( Because the adapter applies a snapshot as a full replacement, both hooks return the complete state object. A hook that returns a partial object silently drops whatever the other hook had written. -## Subagents emit nothing the adapter can read +## Subagents stream through a per-tool handler -Delegation is routed through a `CUSTOM` `MultiAgentHandoff` event plus `STEP_*` events, with no `ACTIVITY` events at any point. The Threadplane subagent projection keys on an `activityType` of `subagent`, so there is nothing to project. - -The AG-UI protocol has carried dedicated `SUBAGENT_STARTED`, `SUBAGENT_FINISHED`, and `SUBAGENT_ERROR` events since `@ag-ui/core` 0.0.59. No runtime measured here emits them yet. +An in-tree emitter registered as the delegation tool's `ToolBehavior.tool_stream_event_handler` consumes the specialist's re-yielded `stream_async` events as they cross the bridge. +It emits `SUBAGENT_STARTED`, the specialist's tokens as `TEXT_MESSAGE_*` deltas attributed with `subagentRunId`, and `SUBAGENT_FINISHED`, nested between the delegation tool call's `TOOL_CALL_END` and `TOOL_CALL_RESULT`. ## Next steps diff --git a/apps/website/content/docs/runtimes/aws-strands/overview.mdx b/apps/website/content/docs/runtimes/aws-strands/overview.mdx index 7018c3502..dc9af67d2 100644 --- a/apps/website/content/docs/runtimes/aws-strands/overview.mdx +++ b/apps/website/content/docs/runtimes/aws-strands/overview.mdx @@ -21,7 +21,7 @@ The hosted example runs at [examples.threadplane.ai/runtimes/aws-strands](https: | Tool calls | Supported | `check_availability` executes server-side with no pause. | | Shared state | Partial | Snapshot-only, and opt-in per tool. See below. | | Interrupts | Supported | `book_meeting` parks in `tool_context.interrupt(...)`. | -| Subagents | Not available | The bridge emits no `ACTIVITY` events at all. | +| Subagents | Supported (streaming) | An in-tree emitter rides the bridge's tool-stream handler and forwards the specialist's streamed tokens as `SUBAGENT_*` events. | ## Shared state is partial, and the reason matters @@ -31,11 +31,11 @@ Because the adapter applies a `STATE_SNAPSHOT` as a full replacement, every hook Shared state does work on Strands. It is snapshot-only, it is opt-in per tool, and it puts the burden of assembling the whole object on each hook. That is a real constraint to design around, not a rounding error, which is why the measured matrix records it as partial rather than green. -## Subagents are not available +## How subagents surface -The Strands bridge routes delegation through a `CUSTOM` `MultiAgentHandoff` event plus `STEP_*` events, and emits zero `ACTIVITY` events. The Threadplane subagent projection keys on an `activityType` of `subagent`, so there is nothing for it to consume. +Strands wraps every value an async-generator tool yields as a `tool_stream_event`, and the bridge dispatches those events to a per-tool `ToolBehavior.tool_stream_event_handler`. The example's delegation tool re-yields the specialist's `stream_async` output, and an in-tree emitter registered as that handler (`src/subagent_emitter.py`) translates it into `SUBAGENT_STARTED`, attributed `TEXT_MESSAGE_*` deltas carrying `subagentRunId`, and `SUBAGENT_FINISHED` — so the subagent card streams the specialist's tokens live. -This is an upstream gap rather than an adapter defect, and it is shared by every third-party runtime measured so far. Multi-agent routes also crash the stale PyPI wheel, which is one reason the example pins the bridge to a git reference instead. +The wire capture behind this cell is committed at [`cockpit/runtimes/aws-strands/python/docs/wire-capture-subagents.md`](https://github.com/cacheplane/angular-agent-framework/tree/main/cockpit/runtimes/aws-strands/python/docs/wire-capture-subagents.md). Multi-agent routes crash the stale PyPI wheel, which is one reason the example pins the bridge to a git reference instead. ## Model access diff --git a/apps/website/content/docs/runtimes/getting-started/introduction.mdx b/apps/website/content/docs/runtimes/getting-started/introduction.mdx index 3a43f201a..80a6a2b46 100644 --- a/apps/website/content/docs/runtimes/getting-started/introduction.mdx +++ b/apps/website/content/docs/runtimes/getting-started/introduction.mdx @@ -39,9 +39,9 @@ TypeScript. Messages, tool calls, state, and interrupts all work, against a hand | 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 | No | -| **Microsoft Agent Framework** (Python) | Yes | Yes | Yes | Yes | No | -| **Mastra** (TypeScript) | Yes | Yes | Yes | Yes | No | +| **AWS Strands** (Python) | Yes | Yes | Partial | Yes | Yes | +| **Microsoft Agent Framework** (Python) | Yes | Yes | Yes | Yes | Yes | +| **Mastra** (TypeScript) | Yes | Yes | Yes | Yes | Partial | Every gap in that table is caused by an upstream integration, not by the AG-UI protocol and not by a defect in `@threadplane/ag-ui`. The full cause analysis, including the two adapter defects that were found and fixed, lives in [Choosing an adapter](/docs/choosing-an-adapter). @@ -88,7 +88,7 @@ Three differences turned up repeatedly, and each runtime page returns to them. **Resume payloads are not portable.** The adapter derives the wire shape from how the interrupt arrived, so application code passes one neutral `submit({ resume })` regardless of runtime. -**Subagents are unavailable on every third-party runtime.** The three runtimes model delegation in three different ways, and none of them emits the dedicated `SUBAGENT_*` events that `@ag-ui/core` has carried since 0.0.59. Treat server-declared subagents as a capability of LangGraph and of backends you control. +**Subagents now work on every runtime measured here** — streaming on AWS Strands and Microsoft Agent Framework, and as lifecycle-plus-final-text on Mastra. Each backend ships a small emitter that translates its native delegation signals into the protocol's `SUBAGENT_*` events, which `@threadplane/ag-ui` consumes directly. The per-runtime pages show the emitter and the wire capture behind each cell. ## Further reading diff --git a/apps/website/content/docs/runtimes/mastra/how-it-connects.mdx b/apps/website/content/docs/runtimes/mastra/how-it-connects.mdx index 1c366e411..e817a9a94 100644 --- a/apps/website/content/docs/runtimes/mastra/how-it-connects.mdx +++ b/apps/website/content/docs/runtimes/mastra/how-it-connects.mdx @@ -54,11 +54,10 @@ Shared state is a Mastra working-memory object under a Zod schema. The bridge em That places Mastra alongside Microsoft Agent Framework and apart from AWS Strands, whose bridge emits snapshots only. -## Subagents emit nothing the adapter can read +## Subagents surface as lifecycle plus final text -Mastra reserves the `ACTIVITY_*` events for background tasks and observational memory. The Threadplane subagent projection keys on an `activityType` of `subagent`, which Mastra does not emit. - -The AG-UI protocol has carried dedicated `SUBAGENT_STARTED`, `SUBAGENT_FINISHED`, and `SUBAGENT_ERROR` events since `@ag-ui/core` 0.0.59. No runtime measured here emits them yet. +An in-tree injector in the hosting service watches the wire for delegation tool frames — Mastra names them `agent-` — and adds `SUBAGENT_STARTED` on the tool-call start, one attributed text message carrying the child's final text, and `SUBAGENT_FINISHED` on the result. +The child's incremental tokens never reach the wire because `@ag-ui/mastra` drops its `tool-output` chunks and buffers the delegation burst until it resolves, which is why this cell is Partial. ## Next steps diff --git a/apps/website/content/docs/runtimes/mastra/overview.mdx b/apps/website/content/docs/runtimes/mastra/overview.mdx index 7b73d134e..9c407dd3c 100644 --- a/apps/website/content/docs/runtimes/mastra/overview.mdx +++ b/apps/website/content/docs/runtimes/mastra/overview.mdx @@ -21,7 +21,7 @@ The hosted example runs at [examples.threadplane.ai/runtimes/mastra](https://exa | Tool calls | Supported | `check_conditions` executes server-side with no pause. | | Shared state | Supported | A working-memory packing list, over snapshots and real JSON-Patch deltas. | | Interrupts | Supported | `reserve_campsite` suspends the run and resumes from a persisted snapshot. | -| Subagents | Not available | `ACTIVITY_*` is reserved for background tasks and observational memory. | +| Subagents | Partial | The bridge emits delegation tool frames; an in-tree injector adds `SUBAGENT_*` lifecycle and the child's final text — the runtime's bridge does not forward child tokens. | ## Upstream ships no HTTP endpoint @@ -35,11 +35,11 @@ This is the honest shape of the Mastra integration. The adapter needed no change Mastra persists memory and suspended-run snapshots to LibSQL file storage. Resume loads the suspended snapshot back, so the database path has to survive between HTTP requests and across restarts. An in-memory store breaks resume, and an ephemeral filesystem orphans every pending interrupt on redeploy. -## Subagents are not available +## How subagents surface -Mastra reserves the `ACTIVITY_*` events for background tasks and observational memory. The Threadplane subagent projection keys on an `activityType` of `subagent`, which Mastra does not emit and does not intend to. +Mastra registers a child agent as a delegation tool named `agent-`, so a delegation crosses the wire as ordinary `TOOL_CALL_*` frames whose result carries the child's final text. An in-tree injector in the hosting service (`deployments/ag-ui-mastra/subagent-emitter.mjs`) keys off those frames: it emits `SUBAGENT_STARTED` when the delegation tool call starts, an attributed text message carrying the child's final text, and `SUBAGENT_FINISHED` when the result lands. -As with the other two runtimes, this is an upstream modeling difference rather than an adapter defect. +The cell is Partial rather than green because the runtime's bridge does not forward child tokens: the child's deltas exist in-process, but `@ag-ui/mastra` drops its `tool-output` chunks and buffers the delegation burst until it resolves, so the card fills in at completion instead of streaming. That is a property of the bridge, not of the protocol or the adapter. The wire capture behind this cell is committed at [`cockpit/runtimes/mastra/angular/docs/wire-capture-subagents.md`](https://github.com/cacheplane/angular-agent-framework/tree/main/cockpit/runtimes/mastra/angular/docs/wire-capture-subagents.md). ## How the Mastra row was measured diff --git a/apps/website/content/docs/runtimes/microsoft-agent-framework/how-it-connects.mdx b/apps/website/content/docs/runtimes/microsoft-agent-framework/how-it-connects.mdx index 7d5dd4c27..1d8569ad5 100644 --- a/apps/website/content/docs/runtimes/microsoft-agent-framework/how-it-connects.mdx +++ b/apps/website/content/docs/runtimes/microsoft-agent-framework/how-it-connects.mdx @@ -54,11 +54,10 @@ The bridge emits `STATE_SNAPSHOT` followed by real `STATE_DELTA` events as the a This is the meaningful contrast with AWS Strands, whose bridge emits snapshots only and requires each hook to return the complete state object. On Microsoft Agent Framework, ordinary delta application is enough. -## Subagent activity is too coarse to project +## Subagents stream through a queue-merge wrapper -The bridge emits executor-level activity snapshots and constructs `ACTIVITY_DELTA` nowhere. The Threadplane subagent projection keys on an `activityType` of `subagent`, which is a convention adopted by Threadplane's own demo backends and by no third-party runtime. - -The AG-UI protocol has carried dedicated `SUBAGENT_STARTED`, `SUBAGENT_FINISHED`, and `SUBAGENT_ERROR` events since `@ag-ui/core` 0.0.59. No runtime measured here emits them yet. +An in-tree emitter wraps the bridge agent's run: the delegation tool streams the specialist's updates into a queue, and a pump task merges that queue with the bridge's own event stream. +The merged stream carries `SUBAGENT_STARTED`, the specialist's tokens as `TEXT_MESSAGE_*` deltas attributed with `subagentRunId`, and `SUBAGENT_FINISHED`, delivered live while the bridge generator is still suspended inside the tool. ## Next steps diff --git a/apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx b/apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx index 46c2e158a..96333c180 100644 --- a/apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx +++ b/apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx @@ -21,9 +21,9 @@ The hosted example runs at [examples.threadplane.ai/runtimes/microsoft-agent-fra | Tool calls | Supported | `lookup_expense_policy` executes server-side with no pause. | | Shared state | Supported | `predict_state_config` streams a tool argument into frontend state. | | Interrupts | Supported | `submit_expense` declares `approval_mode="always_require"`. | -| Subagents | Not available | The bridge emits no per-subagent activity stream. | +| Subagents | Supported (streaming) | An in-tree queue-merge emitter at the bridge boundary streams the specialist's deltas as `SUBAGENT_*` events. | -This is the most complete third-party row in the measured matrix: four of five surfaces are green, and the fifth is red on every runtime tested. +This is the most complete third-party row in the measured matrix: all five surfaces are green. ## Predictive state is the interesting part @@ -39,11 +39,11 @@ The bridge then streams the `expense` argument of `submit_expense` into frontend Unlike AWS Strands, this runtime emits real deltas, so state does not have to be reassembled in full on every update. -## Subagents are not available +## How subagents surface -The bridge emits coarse executor-level activity snapshots and constructs `ACTIVITY_DELTA` nowhere. The Threadplane subagent projection keys on an `activityType` of `subagent`, which no third-party runtime emits. +The bridge itself forwards nothing from a specialist agent running inside a tool body — only the tool's final result string reaches the wire natively. The example therefore wraps the bridge agent's run with an in-tree queue-merge emitter (`src/subagent_emitter.py`): the delegation tool streams the specialist's updates into a shared queue, a pump task merges that queue with the bridge's own event stream, and the merged stream carries `SUBAGENT_STARTED`, attributed `TEXT_MESSAGE_*` deltas with `subagentRunId`, and `SUBAGENT_FINISHED` — live, while the bridge generator is still suspended inside the tool. -This is an upstream gap rather than an adapter defect. The AG-UI protocol has carried dedicated `SUBAGENT_*` events since `@ag-ui/core` 0.0.59, and adopting them is upstream work. +The wire capture behind this cell is committed at [`cockpit/runtimes/microsoft-agent-framework/python/docs/wire-capture-subagents.md`](https://github.com/cacheplane/angular-agent-framework/tree/main/cockpit/runtimes/microsoft-agent-framework/python/docs/wire-capture-subagents.md). ## Model access