Skip to content

Commit 5e3d866

Browse files
bloveclaude
andcommitted
docs(website): add an agent runtimes docs section
One `runtimes` docs library with a section per measured AG-UI runtime — AWS Strands, Microsoft Agent Framework, and Mastra — each carrying an overview, a local quickstart, and a page recording the wire conventions measured on 2026-08-31. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c20ab69 commit 5e3d866

13 files changed

Lines changed: 774 additions & 2 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
title: How It Connects
3+
description: The AG-UI wire conventions measured for AWS Strands: outcome interrupts, top-level resume entries, and snapshot-only state.
4+
---
5+
6+
# How AWS Strands Connects
7+
8+
This page records the AG-UI wire behavior measured for the Strands bridge on 2026-08-31. It describes what the runtime emitted, not what the protocol permits in general. The captured Server-Sent Events are committed at [`libs/ag-ui/fixtures/runtime-transcripts/`](https://github.com/cacheplane/angular-agent-framework/tree/main/libs/ag-ui/fixtures/runtime-transcripts) and replayed by the adapter test suite on every run.
9+
10+
## Transport
11+
12+
The example serves a single AG-UI endpoint from FastAPI.
13+
14+
| Route | Method | Notes |
15+
|---|---|---|
16+
| `/agent` | `POST` | `RunAgentInput` JSON in, Server-Sent Events out. |
17+
| `/ok` | `GET` | Unauthenticated health check. |
18+
19+
On the Angular side that is an ordinary `provideAgent({ url: '/agent' })`. Nothing about the adapter configuration is Strands-specific.
20+
21+
## Interrupts use the outcome convention
22+
23+
Strands signals an interrupt through the protocol-standard run outcome and never through a `CUSTOM` event:
24+
25+
```json
26+
{
27+
"type": "RUN_FINISHED",
28+
"outcome": {
29+
"type": "interrupt",
30+
"interrupts": [{ "interruptId": "...", "value": { } }]
31+
}
32+
}
33+
```
34+
35+
This is the opposite of the LangGraph bridge, which signals interrupts only through a `CUSTOM` event named `on_interrupt` and never sets an outcome. The adapter detects either convention; within a single run, the first signal it sees wins.
36+
37+
The reducer originally keyed interrupts on `on_interrupt` alone, which meant a Strands run finalized as a success with a dangling approval call and an undefined `interrupt()`. That was an adapter defect, and it is fixed.
38+
39+
## Resume uses top-level entries
40+
41+
Strands reads resume data from the protocol-standard top-level `resume` array, one entry per interrupt, keyed by `interruptId`:
42+
43+
```json
44+
{
45+
"resume": [
46+
{ "interruptId": "...", "status": "accepted", "payload": { } }
47+
]
48+
}
49+
```
50+
51+
Application code does not assemble that. You call the neutral `submit({ resume })`, and the adapter derives the wire shape from how the interrupt arrived. The same call against a Mastra backend produces `forwardedProps.command.interruptEvent` instead, and against the LangGraph bridge produces `forwardedProps.command.resume`.
52+
53+
## State is snapshot-only
54+
55+
The bridge emits `STATE_SNAPSHOT` and never `STATE_DELTA`. State reaches the wire only where a tool opts in with a `ToolBehavior` hook:
56+
57+
```python
58+
StrandsAgentConfig(
59+
tool_behaviors={
60+
"check_availability": ToolBehavior(state_from_result=availability_state),
61+
"book_meeting": ToolBehavior(state_from_args=booking_state),
62+
},
63+
)
64+
```
65+
66+
`state_from_result` fires after the tool returns. `state_from_args` fires as the tool call's arguments finish streaming, which is what puts a pending booking into state *before* the interrupt pauses the run.
67+
68+
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.
69+
70+
## Subagents emit nothing the adapter can read
71+
72+
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.
73+
74+
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.
75+
76+
## Next steps
77+
78+
- [Overview](/docs/runtimes/aws-strands/overview) — what the integration supports.
79+
- [Microsoft Agent Framework — How It Connects](/docs/runtimes/microsoft-agent-framework/how-it-connects) — the same outcome convention, a different resume requirement.
80+
- [Choosing an adapter](/docs/choosing-an-adapter) — the full matrix and its cause analysis.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
title: Overview
3+
description: What the AWS Strands integration demonstrates through @threadplane/ag-ui, and where its shared-state support stops short.
4+
---
5+
6+
# AWS Strands Overview
7+
8+
[AWS Strands](https://strandsagents.com) is an open-source Python agent SDK from AWS. Its AG-UI bridge, `ag-ui-strands`, turns a Strands `Agent` into an AG-UI event stream, which is all `@threadplane/ag-ui` needs in order to bind it to `<chat>`.
9+
10+
The Threadplane example is a meeting scheduler. It runs a Strands agent behind FastAPI, streams to an ordinary Angular app, and pauses for human approval before it books anything.
11+
12+
<Callout type="tip" title="See it live">
13+
The hosted example runs at [examples.threadplane.ai/runtimes/aws-strands](https://examples.threadplane.ai/runtimes/aws-strands/). The source is [`cockpit/runtimes/aws-strands`](https://github.com/cacheplane/angular-agent-framework/tree/main/cockpit/runtimes/aws-strands).
14+
</Callout>
15+
16+
## What the integration demonstrates
17+
18+
| Surface | Status | How |
19+
|---|---|---|
20+
| Messages | Supported | Streamed assistant text from a Strands `Agent`. |
21+
| Tool calls | Supported | `check_availability` executes server-side with no pause. |
22+
| Shared state | Partial | Snapshot-only, and opt-in per tool. See below. |
23+
| Interrupts | Supported | `book_meeting` parks in `tool_context.interrupt(...)`. |
24+
| Subagents | Not available | The bridge emits no `ACTIVITY` events at all. |
25+
26+
## Shared state is partial, and the reason matters
27+
28+
The Strands bridge never emits `STATE_DELTA`. Outbound state exists only where a tool opts in through a per-tool `ToolBehavior` hook: the example wires `state_from_result` on `check_availability` and `state_from_args` on `book_meeting`.
29+
30+
Because the adapter applies a `STATE_SNAPSHOT` as a full replacement, every hook has to return the **complete** state object. A hook that returns only the keys it changed clobbers its siblings.
31+
32+
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.
33+
34+
## Subagents are not available
35+
36+
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.
37+
38+
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.
39+
40+
## Model access
41+
42+
Strands' native OpenAI provider is used on a plain `OPENAI_API_KEY`. No AWS credentials are involved anywhere in this example, despite the runtime's name.
43+
44+
## Next steps
45+
46+
- [Quickstart](/docs/runtimes/aws-strands/quickstart) — run the example locally.
47+
- [How It Connects](/docs/runtimes/aws-strands/how-it-connects) — the measured wire conventions.
48+
- [Choosing an adapter](/docs/choosing-an-adapter) — the full runtime matrix and its cause analysis.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
title: Quickstart
3+
description: Run the AWS Strands runtime example locally, backend and Angular app, on ports 5331 and 4331.
4+
---
5+
6+
# AWS Strands Quickstart
7+
8+
This runs the example from a clone of the [monorepo](https://github.com/cacheplane/angular-agent-framework). The backend is a uvicorn process on port 5331; the Angular dev server is on port 4331 and proxies `/agent` to it.
9+
10+
<Callout type="info" title="Prerequisites">
11+
Node.js 20 or newer, Python 3.11 or newer, [`uv`](https://docs.astral.sh/uv/), and an OpenAI API key. No AWS account and no AWS credentials are required.
12+
</Callout>
13+
14+
<Steps>
15+
<Step title="Install workspace dependencies">
16+
17+
```bash
18+
git clone https://github.com/cacheplane/angular-agent-framework.git
19+
cd angular-agent-framework
20+
npm ci
21+
```
22+
23+
</Step>
24+
<Step title="Configure the backend environment">
25+
26+
Copy the example file and fill in a real key.
27+
28+
```bash
29+
cp cockpit/runtimes/aws-strands/python/.env.example \
30+
cockpit/runtimes/aws-strands/python/.env
31+
```
32+
33+
| Variable | Required | Purpose |
34+
|---|---|---|
35+
| `OPENAI_API_KEY` | Yes | Strands' native OpenAI provider. |
36+
| `OPENAI_CHAT_MODEL` | No | Model name. Defaults to `gpt-4o-mini`. |
37+
| `OPENAI_BASE_URL` | No | Redirects the OpenAI client. The end-to-end fixture harness sets this to replay recorded calls. |
38+
| `OTEL_SDK_DISABLED` | No | Already set to `true` in `src/agent.py`. Strands wires OpenTelemetry unconditionally and logs exporter noise without a collector. |
39+
| `OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` | No | Same reason, set to `all` by default. |
40+
41+
</Step>
42+
<Step title="Start both halves">
43+
44+
One command starts the Python backend and the Angular dev server together.
45+
46+
```bash
47+
npx tsx apps/cockpit/scripts/serve-example.ts --capability=rt-strands
48+
```
49+
50+
The script runs `uv sync` in `cockpit/runtimes/aws-strands/python` on the way, so the first start takes longer than later ones.
51+
52+
</Step>
53+
<Step title="Open the app">
54+
55+
Visit `http://localhost:4331`. The backend answers on `http://localhost:5331/agent`, with an unauthenticated health check at `http://localhost:5331/ok`.
56+
57+
</Step>
58+
<Step title="Exercise every surface">
59+
60+
Three prompts cover the measured surfaces in order.
61+
62+
1. *"What is my availability on Thursday?"* — streams a message and calls `check_availability`, which mirrors its result into shared state.
63+
2. *"Book the 2pm slot to talk about the roadmap."* — calls `book_meeting`, which parks in an interrupt and renders an approval card.
64+
3. Approve or decline the card — the run resumes and the agent confirms the outcome.
65+
66+
</Step>
67+
</Steps>
68+
69+
## About the bridge pin
70+
71+
`pyproject.toml` pins `ag-ui-strands` to a git reference of the [`ag-ui-protocol/ag-ui`](https://github.com/ag-ui-protocol/ag-ui) repository, subdirectory `integrations/aws-strands/python`, through `[tool.uv.sources]`. The exported requirements file carries the matching `git+https://...#subdirectory=...` line.
72+
73+
The published PyPI release, `ag-ui-strands` 0.3.0, is stale: it predates the interrupt and resume contract this example depends on, and it crashes on multi-agent routes. Installing from PyPI instead of the pin will not reproduce the behavior documented here.
74+
75+
## Next steps
76+
77+
- [How It Connects](/docs/runtimes/aws-strands/how-it-connects) — the wire conventions this example relies on.
78+
- [Overview](/docs/runtimes/aws-strands/overview) — what the integration does and does not support.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
title: Introduction
3+
description: Measured AG-UI runtime support for AWS Strands, Microsoft Agent Framework, and Mastra behind one Angular adapter.
4+
---
5+
6+
# Introduction
7+
8+
`@threadplane/ag-ui` is protocol-first: it consumes the [AG-UI](https://github.com/ag-ui-protocol/ag-ui) event vocabulary rather than any one runtime's SDK. That makes "any AG-UI backend plugs in" a claim that can be tested instead of asserted.
9+
10+
This section documents what happened when it was tested. On 2026-08-31 the adapter was run against three runtimes that have nothing to do with LangGraph, in two languages, with no adapter changes for messages, tool calls, or state. Each runtime has a standalone Angular example, a real backend, and a committed transcript of its wire traffic.
11+
12+
<Callout type="info" title="What this section is not">
13+
These pages document runtimes as *backends measured against the adapter*. They are not a substitute for each vendor's own documentation, and Threadplane does not maintain any of the upstream AG-UI bridges described here.
14+
</Callout>
15+
16+
## The runtimes
17+
18+
<CardGroup>
19+
<Card title="AWS Strands" href="/docs/runtimes/aws-strands/overview">
20+
Python. Messages, tool calls, and interrupts work. Shared state is snapshot-only and opt-in per tool.
21+
</Card>
22+
<Card title="Microsoft Agent Framework" href="/docs/runtimes/microsoft-agent-framework/overview">
23+
Python. Messages, tool calls, state, and interrupts all work. Azure OpenAI by default.
24+
</Card>
25+
<Card title="Mastra" href="/docs/runtimes/mastra/overview">
26+
TypeScript. Messages, tool calls, state, and interrupts all work, against a hand-written Node hosting service.
27+
</Card>
28+
</CardGroup>
29+
30+
## Measured support
31+
32+
| Runtime | Messages | Tool calls | State | Interrupts | Subagents |
33+
|---|---|---|---|---|---|
34+
| **LangGraph** (via the AG-UI bridge) | Yes | Yes | Yes | Yes | Yes |
35+
| **AWS Strands** (Python) | Yes | Yes | Partial | Yes | No |
36+
| **Microsoft Agent Framework** (Python) | Yes | Yes | Yes | Yes | No |
37+
| **Mastra** (TypeScript) | Yes | Yes | Yes | Yes | No |
38+
39+
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).
40+
41+
## What is the same everywhere
42+
43+
The Angular side does not change between these three runtimes. Each example uses the same provider call and the same component body:
44+
45+
```ts
46+
// app.config.ts
47+
import { ApplicationConfig } from '@angular/core';
48+
import { provideAgent } from '@threadplane/ag-ui';
49+
import { provideChat } from '@threadplane/chat';
50+
51+
export const appConfig: ApplicationConfig = {
52+
providers: [
53+
provideAgent({ url: '/agent' }),
54+
provideChat({}),
55+
],
56+
};
57+
```
58+
59+
```ts
60+
import { Component } from '@angular/core';
61+
import { ChatComponent } from '@threadplane/chat';
62+
import { injectAgent } from '@threadplane/ag-ui';
63+
64+
@Component({
65+
selector: 'app-root',
66+
imports: [ChatComponent],
67+
template: `<chat [agent]="agent" />`,
68+
})
69+
export class App {
70+
protected readonly agent = injectAgent();
71+
}
72+
```
73+
74+
What changes is the backend, its hosting lane, and the wire conventions it happens to use. The **How It Connects** page for each runtime records those conventions as they were measured.
75+
76+
## What is different everywhere
77+
78+
Three differences turned up repeatedly, and each runtime page returns to them.
79+
80+
**Interrupts arrive by two different conventions.** AWS Strands and Microsoft Agent Framework signal an interrupt only through the protocol-standard `RUN_FINISHED` outcome. The LangGraph bridge signals it only through a `CUSTOM` event named `on_interrupt`. Mastra emits both. The adapter accepts either, and within a single run the first signal wins.
81+
82+
**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.
83+
84+
**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.
85+
86+
## Further reading
87+
88+
- [Choosing an adapter](/docs/choosing-an-adapter) — the full measured matrix, cause-by-cause.
89+
- [AG-UI adapter introduction](/docs/ag-ui/getting-started/introduction) — the adapter these runtimes bind through.
90+
- [What changes when the runtime changes](/blog/what-changes-when-the-runtime-changes) — the argument for measuring portability.
91+
- [We measured the runtime swap](/blog/we-measured-the-runtime-swap) — the results write-up.
92+
- [`libs/ag-ui/fixtures/runtime-transcripts/`](https://github.com/cacheplane/angular-agent-framework/tree/main/libs/ag-ui/fixtures/runtime-transcripts) — the captured Server-Sent Events, verbatim from the wire.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
title: How It Connects
3+
description: Measured AG-UI wire behavior for Mastra: both interrupt conventions, an interruptEvent resume shape, and JSON-Patch state deltas.
4+
---
5+
6+
# How Mastra Connects
7+
8+
This page records the AG-UI wire behavior measured for Mastra on 2026-08-31. It describes what the runtime emitted, not what the protocol permits in general. The captured Server-Sent Events are committed at [`libs/ag-ui/fixtures/runtime-transcripts/`](https://github.com/cacheplane/angular-agent-framework/tree/main/libs/ag-ui/fixtures/runtime-transcripts) and replayed by the adapter test suite on every run.
9+
10+
## Transport is Threadplane's, not upstream's
11+
12+
`@ag-ui/mastra` ships an in-process `MastraAgent` bridge and a CopilotKit runtime mount, and no plain AG-UI HTTP endpoint. The service in [`deployments/ag-ui-mastra`](https://github.com/cacheplane/angular-agent-framework/tree/main/deployments/ag-ui-mastra) supplies the missing endpoint.
13+
14+
| Route | Method | Auth | Notes |
15+
|---|---|---|---|
16+
| `/ok` | `GET` | None | Health check. |
17+
| `/agent/mastra` | `POST` | `X-Internal-Token` | `RunAgentInput` JSON in, Server-Sent Events out. |
18+
19+
The service refuses to boot without `AG_UI_INTERNAL_TOKEN`, rejects any non-health route without a matching header, and maps an error from the underlying observable to a `RUN_ERROR` frame rather than dropping the socket. Every other route returns `401 {"detail":"unauthorized"}`.
20+
21+
On the Angular side this is still an ordinary `provideAgent({ url: '/agent' })`. The token is injected by the dev proxy in front of the app, never by the browser.
22+
23+
## Interrupts arrive by both conventions
24+
25+
Mastra is the only measured runtime that signals an interrupt twice: it emits a `CUSTOM` event named `on_interrupt` **and** finishes the run with the protocol-standard interrupt outcome.
26+
27+
AWS Strands and Microsoft Agent Framework emit only the outcome. The LangGraph bridge emits only `on_interrupt`. The adapter accepts either, and within a single run the first signal it sees wins, so a runtime that emits both is handled without special-casing.
28+
29+
## Resume uses interruptEvent
30+
31+
Mastra reads resume data from `forwardedProps.command.interruptEvent`, carrying a tool-call id and a run id:
32+
33+
```json
34+
{
35+
"forwardedProps": {
36+
"command": {
37+
"interruptEvent": { "toolCallId": "...", "runId": "..." }
38+
}
39+
}
40+
}
41+
```
42+
43+
That is a third distinct shape. Strands and Microsoft Agent Framework read a top-level `resume` array; the LangGraph bridge reads `forwardedProps.command.resume`. Application code passes one neutral `submit({ resume })` and the adapter derives the wire shape from how the interrupt arrived.
44+
45+
## Suspend and resume require persistent storage
46+
47+
`reserve_campsite` calls Mastra's `suspend()` on its first invocation and reads `resumeData` on the second. Mastra writes the suspended-run snapshot to LibSQL file storage, and resume loads it back.
48+
49+
Because those two invocations are separate HTTP requests, an in-memory store cannot round-trip them. On a deployment with an ephemeral filesystem, every redeploy orphans pending interrupts. This is a property of the runtime, not of the adapter, and it is the single most consequential operational difference between Mastra and the two Python runtimes.
50+
51+
## State is working memory, with real deltas
52+
53+
Shared state is a Mastra working-memory object under a Zod schema. The bridge emits `STATE_SNAPSHOT` followed by real JSON-Patch `STATE_DELTA` events as the model revises it, so the adapter applies ordinary deltas with no reassembly.
54+
55+
That places Mastra alongside Microsoft Agent Framework and apart from AWS Strands, whose bridge emits snapshots only.
56+
57+
## Subagents emit nothing the adapter can read
58+
59+
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.
60+
61+
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.
62+
63+
## Next steps
64+
65+
- [Overview](/docs/runtimes/mastra/overview) — what the integration supports.
66+
- [Quickstart](/docs/runtimes/mastra/quickstart) — run the example and the service locally.
67+
- [Choosing an adapter](/docs/choosing-an-adapter) — the full matrix and its cause analysis.

0 commit comments

Comments
 (0)