From 8734c4ccf87250caf0b24fa574430bcdc5473b2f Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 31 Aug 2026 19:40:58 -0700 Subject: [PATCH 1/2] docs(website): add an agent runtimes docs section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../runtimes/aws-strands/how-it-connects.mdx | 80 ++++++++++++++++ .../docs/runtimes/aws-strands/overview.mdx | 48 ++++++++++ .../docs/runtimes/aws-strands/quickstart.mdx | 78 +++++++++++++++ .../runtimes/getting-started/introduction.mdx | 92 ++++++++++++++++++ .../docs/runtimes/mastra/how-it-connects.mdx | 67 +++++++++++++ .../content/docs/runtimes/mastra/overview.mdx | 52 ++++++++++ .../docs/runtimes/mastra/quickstart.mdx | 94 +++++++++++++++++++ .../how-it-connects.mdx | 67 +++++++++++++ .../microsoft-agent-framework/overview.mdx | 56 +++++++++++ .../microsoft-agent-framework/quickstart.mdx | 76 +++++++++++++++ apps/website/src/app/docs/page.tsx | 4 + .../src/components/docs/LibraryMark.tsx | 14 ++- apps/website/src/lib/docs-config.ts | 48 +++++++++- 13 files changed, 774 insertions(+), 2 deletions(-) create mode 100644 apps/website/content/docs/runtimes/aws-strands/how-it-connects.mdx create mode 100644 apps/website/content/docs/runtimes/aws-strands/overview.mdx create mode 100644 apps/website/content/docs/runtimes/aws-strands/quickstart.mdx create mode 100644 apps/website/content/docs/runtimes/getting-started/introduction.mdx create mode 100644 apps/website/content/docs/runtimes/mastra/how-it-connects.mdx create mode 100644 apps/website/content/docs/runtimes/mastra/overview.mdx create mode 100644 apps/website/content/docs/runtimes/mastra/quickstart.mdx create mode 100644 apps/website/content/docs/runtimes/microsoft-agent-framework/how-it-connects.mdx create mode 100644 apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx create mode 100644 apps/website/content/docs/runtimes/microsoft-agent-framework/quickstart.mdx 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 new file mode 100644 index 000000000..286d5d27c --- /dev/null +++ b/apps/website/content/docs/runtimes/aws-strands/how-it-connects.mdx @@ -0,0 +1,80 @@ +--- +title: How It Connects +description: The AG-UI wire conventions measured for AWS Strands: outcome interrupts, top-level resume entries, and snapshot-only state. +--- + +# How AWS Strands Connects + +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. + +## Transport + +The example serves a single AG-UI endpoint from FastAPI. + +| Route | Method | Notes | +|---|---|---| +| `/agent` | `POST` | `RunAgentInput` JSON in, Server-Sent Events out. | +| `/ok` | `GET` | Unauthenticated health check. | + +On the Angular side that is an ordinary `provideAgent({ url: '/agent' })`. Nothing about the adapter configuration is Strands-specific. + +## Interrupts use the outcome convention + +Strands signals an interrupt through the protocol-standard run outcome and never through a `CUSTOM` event: + +```json +{ + "type": "RUN_FINISHED", + "outcome": { + "type": "interrupt", + "interrupts": [{ "interruptId": "...", "value": { } }] + } +} +``` + +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. + +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. + +## Resume uses top-level entries + +Strands reads resume data from the protocol-standard top-level `resume` array, one entry per interrupt, keyed by `interruptId`: + +```json +{ + "resume": [ + { "interruptId": "...", "status": "accepted", "payload": { } } + ] +} +``` + +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`. + +## State is snapshot-only + +The bridge emits `STATE_SNAPSHOT` and never `STATE_DELTA`. State reaches the wire only where a tool opts in with a `ToolBehavior` hook: + +```python +StrandsAgentConfig( + tool_behaviors={ + "check_availability": ToolBehavior(state_from_result=availability_state), + "book_meeting": ToolBehavior(state_from_args=booking_state), + }, +) +``` + +`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. + +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 + +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. + +## Next steps + +- [Overview](/docs/runtimes/aws-strands/overview) — what the integration supports. +- [Microsoft Agent Framework — How It Connects](/docs/runtimes/microsoft-agent-framework/how-it-connects) — the same outcome convention, a different resume requirement. +- [Choosing an adapter](/docs/choosing-an-adapter) — the full matrix and its cause analysis. diff --git a/apps/website/content/docs/runtimes/aws-strands/overview.mdx b/apps/website/content/docs/runtimes/aws-strands/overview.mdx new file mode 100644 index 000000000..7018c3502 --- /dev/null +++ b/apps/website/content/docs/runtimes/aws-strands/overview.mdx @@ -0,0 +1,48 @@ +--- +title: Overview +description: What the AWS Strands integration demonstrates through @threadplane/ag-ui, and where its shared-state support stops short. +--- + +# AWS Strands Overview + +[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 ``. + +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. + + +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). + + +## What the integration demonstrates + +| Surface | Status | How | +|---|---|---| +| Messages | Supported | Streamed assistant text from a Strands `Agent`. | +| 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. | + +## Shared state is partial, and the reason matters + +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`. + +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. + +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 + +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. + +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. + +## Model access + +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. + +## Next steps + +- [Quickstart](/docs/runtimes/aws-strands/quickstart) — run the example locally. +- [How It Connects](/docs/runtimes/aws-strands/how-it-connects) — the measured wire conventions. +- [Choosing an adapter](/docs/choosing-an-adapter) — the full runtime matrix and its cause analysis. diff --git a/apps/website/content/docs/runtimes/aws-strands/quickstart.mdx b/apps/website/content/docs/runtimes/aws-strands/quickstart.mdx new file mode 100644 index 000000000..aa3fbd6a2 --- /dev/null +++ b/apps/website/content/docs/runtimes/aws-strands/quickstart.mdx @@ -0,0 +1,78 @@ +--- +title: Quickstart +description: Run the AWS Strands runtime example locally, backend and Angular app, on ports 5331 and 4331. +--- + +# AWS Strands Quickstart + +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. + + +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. + + + + + +```bash +git clone https://github.com/cacheplane/angular-agent-framework.git +cd angular-agent-framework +npm ci +``` + + + + +Copy the example file and fill in a real key. + +```bash +cp cockpit/runtimes/aws-strands/python/.env.example \ + cockpit/runtimes/aws-strands/python/.env +``` + +| Variable | Required | Purpose | +|---|---|---| +| `OPENAI_API_KEY` | Yes | Strands' native OpenAI provider. | +| `OPENAI_CHAT_MODEL` | No | Model name. Defaults to `gpt-4o-mini`. | +| `OPENAI_BASE_URL` | No | Redirects the OpenAI client. The end-to-end fixture harness sets this to replay recorded calls. | +| `OTEL_SDK_DISABLED` | No | Already set to `true` in `src/agent.py`. Strands wires OpenTelemetry unconditionally and logs exporter noise without a collector. | +| `OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` | No | Same reason, set to `all` by default. | + + + + +One command starts the Python backend and the Angular dev server together. + +```bash +npx tsx apps/cockpit/scripts/serve-example.ts --capability=rt-strands +``` + +The script runs `uv sync` in `cockpit/runtimes/aws-strands/python` on the way, so the first start takes longer than later ones. + + + + +Visit `http://localhost:4331`. The backend answers on `http://localhost:5331/agent`, with an unauthenticated health check at `http://localhost:5331/ok`. + + + + +Three prompts cover the measured surfaces in order. + +1. *"What is my availability on Thursday?"* — streams a message and calls `check_availability`, which mirrors its result into shared state. +2. *"Book the 2pm slot to talk about the roadmap."* — calls `book_meeting`, which parks in an interrupt and renders an approval card. +3. Approve or decline the card — the run resumes and the agent confirms the outcome. + + + + +## About the bridge pin + +`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. + +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. + +## Next steps + +- [How It Connects](/docs/runtimes/aws-strands/how-it-connects) — the wire conventions this example relies on. +- [Overview](/docs/runtimes/aws-strands/overview) — what the integration does and does not support. diff --git a/apps/website/content/docs/runtimes/getting-started/introduction.mdx b/apps/website/content/docs/runtimes/getting-started/introduction.mdx new file mode 100644 index 000000000..91e98d18a --- /dev/null +++ b/apps/website/content/docs/runtimes/getting-started/introduction.mdx @@ -0,0 +1,92 @@ +--- +title: Introduction +description: Measured AG-UI runtime support for AWS Strands, Microsoft Agent Framework, and Mastra behind one Angular adapter. +--- + +# Introduction + +`@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. + +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. + + +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. + + +## The runtimes + + + +Python. Messages, tool calls, and interrupts work. Shared state is snapshot-only and opt-in per tool. + + +Python. Messages, tool calls, state, and interrupts all work. Azure OpenAI by default. + + +TypeScript. Messages, tool calls, state, and interrupts all work, against a hand-written Node hosting service. + + + +## Measured support + +| 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 | + +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). + +## What is the same everywhere + +The Angular side does not change between these three runtimes. Each example uses the same provider call and the same component body: + +```ts +// app.config.ts +import { ApplicationConfig } from '@angular/core'; +import { provideAgent } from '@threadplane/ag-ui'; +import { provideChat } from '@threadplane/chat'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideAgent({ url: '/agent' }), + provideChat({}), + ], +}; +``` + +```ts +import { Component } from '@angular/core'; +import { ChatComponent } from '@threadplane/chat'; +import { injectAgent } from '@threadplane/ag-ui'; + +@Component({ + selector: 'app-root', + imports: [ChatComponent], + template: ``, +}) +export class App { + protected readonly agent = injectAgent(); +} +``` + +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. + +## What is different everywhere + +Three differences turned up repeatedly, and each runtime page returns to them. + +**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. + +**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. + +## Further reading + +- [Choosing an adapter](/docs/choosing-an-adapter) — the full measured matrix, cause-by-cause. +- [AG-UI adapter introduction](/docs/ag-ui/getting-started/introduction) — the adapter these runtimes bind through. +- [What changes when the runtime changes](/blog/what-changes-when-the-runtime-changes) — the argument for measuring portability. +- [We measured the runtime swap](/blog/we-measured-the-runtime-swap) — the results write-up. +- [`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. diff --git a/apps/website/content/docs/runtimes/mastra/how-it-connects.mdx b/apps/website/content/docs/runtimes/mastra/how-it-connects.mdx new file mode 100644 index 000000000..bab64d51a --- /dev/null +++ b/apps/website/content/docs/runtimes/mastra/how-it-connects.mdx @@ -0,0 +1,67 @@ +--- +title: How It Connects +description: Measured AG-UI wire behavior for Mastra: both interrupt conventions, an interruptEvent resume shape, and JSON-Patch state deltas. +--- + +# How Mastra Connects + +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. + +## Transport is Threadplane's, not upstream's + +`@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. + +| Route | Method | Auth | Notes | +|---|---|---|---| +| `/ok` | `GET` | None | Health check. | +| `/agent/mastra` | `POST` | `X-Internal-Token` | `RunAgentInput` JSON in, Server-Sent Events out. | + +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"}`. + +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. + +## Interrupts arrive by both conventions + +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. + +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. + +## Resume uses interruptEvent + +Mastra reads resume data from `forwardedProps.command.interruptEvent`, carrying a tool-call id and a run id: + +```json +{ + "forwardedProps": { + "command": { + "interruptEvent": { "toolCallId": "...", "runId": "..." } + } + } +} +``` + +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. + +## Suspend and resume require persistent storage + +`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. + +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. + +## State is working memory, with real deltas + +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. + +That places Mastra alongside Microsoft Agent Framework and apart from AWS Strands, whose bridge emits snapshots only. + +## Subagents emit nothing the adapter can read + +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. + +## Next steps + +- [Overview](/docs/runtimes/mastra/overview) — what the integration supports. +- [Quickstart](/docs/runtimes/mastra/quickstart) — run the example and the service locally. +- [Choosing an adapter](/docs/choosing-an-adapter) — the full matrix and its cause analysis. diff --git a/apps/website/content/docs/runtimes/mastra/overview.mdx b/apps/website/content/docs/runtimes/mastra/overview.mdx new file mode 100644 index 000000000..161d557c9 --- /dev/null +++ b/apps/website/content/docs/runtimes/mastra/overview.mdx @@ -0,0 +1,52 @@ +--- +title: Overview +description: What the Mastra integration demonstrates through @threadplane/ag-ui, and why it needs a hand-written Node hosting service. +--- + +# Mastra Overview + +[Mastra](https://mastra.ai) is a TypeScript agent framework. It is the only non-Python runtime in the measured set, and the only one whose upstream AG-UI integration ships no HTTP endpoint at all. + +The Threadplane example is a camping trip planner. It streams messages, calls a backend tool, keeps a packing list in shared state through Mastra's working memory, and suspends a run for human approval before reserving a campsite. + + +Unlike the two Python runtimes, Mastra is not served by the shared FastAPI deployment. Its backend is a separate Node service with its own hosting lane, so the [hosted example page](https://examples.threadplane.ai/runtimes/mastra/) may not have a reachable backend behind it. The [Quickstart](/docs/runtimes/mastra/quickstart) is the supported path. + + +## What the integration demonstrates + +| Surface | Status | How | +|---|---|---| +| Messages | Supported | Streamed assistant text over `TEXT_MESSAGE_CHUNK`. | +| 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. | + +## Upstream ships no HTTP endpoint + +`@ag-ui/mastra` provides an in-process `MastraAgent` bridge and a CopilotKit runtime mount. It does not provide a plain AG-UI HTTP endpoint, which is what `@threadplane/ag-ui` connects to. + +Threadplane therefore maintains a small hosting service, [`deployments/ag-ui-mastra`](https://github.com/cacheplane/angular-agent-framework/tree/main/deployments/ag-ui-mastra). It accepts `POST /agent/`, calls `MastraAgent.run(input)`, and writes one Server-Sent Events frame per AG-UI event. It is deliberately hand-written rather than generated: the Python generator targets one aggregated FastAPI process, and Mastra is a different language on a different hosting lane. + +This is the honest shape of the Mastra integration. The adapter needed no changes, but somebody has to serve the events, and upstream does not. + +## Persistence is a hard requirement + +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 + +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. + +As with the other two runtimes, this is an upstream modeling difference rather than an adapter defect. + +## How the Mastra row was measured + +Its cells come from a real Mastra server driven with live model calls. The raw Server-Sent Events were captured off the wire and are committed and replayed like every other runtime's, including the interrupt and resume round trip driven through the real `@ag-ui/client`. + +## Next steps + +- [Quickstart](/docs/runtimes/mastra/quickstart) — run the example locally. +- [How It Connects](/docs/runtimes/mastra/how-it-connects) — the measured wire conventions. +- [Choosing an adapter](/docs/choosing-an-adapter) — the full runtime matrix and its cause analysis. diff --git a/apps/website/content/docs/runtimes/mastra/quickstart.mdx b/apps/website/content/docs/runtimes/mastra/quickstart.mdx new file mode 100644 index 000000000..ca93338e6 --- /dev/null +++ b/apps/website/content/docs/runtimes/mastra/quickstart.mdx @@ -0,0 +1,94 @@ +--- +title: Quickstart +description: Run the Mastra runtime example locally against the Node AG-UI hosting service on port 5332, with the Angular app on 4332. +--- + +# Mastra Quickstart + +This runs the example from a clone of the [monorepo](https://github.com/cacheplane/angular-agent-framework). The backend is **not** Python: it is the Node service in `deployments/ag-ui-mastra`, listening on port 5332. The Angular dev server is on port 4332. + + +Node.js 20 or newer and an OpenAI API key. No Python toolchain is involved in this example. + + + +`serve-example.ts` auto-starts Python backends only. For Mastra it starts the Angular side, and you start the Node service yourself. + + + + + +```bash +git clone https://github.com/cacheplane/angular-agent-framework.git +cd angular-agent-framework +npm ci +``` + + + + +The service has its own dependency tree. + +```bash +cd deployments/ag-ui-mastra +npm ci +AG_UI_INTERNAL_TOKEN=dev-local-token \ + OPENAI_API_KEY=sk-... \ + PORT=5332 node server.mjs +``` + +| Variable | Required | Purpose | +|---|---|---| +| `AG_UI_INTERNAL_TOKEN` | Yes | Every route except `/ok` requires a matching `X-Internal-Token` header. The service refuses to boot without this. | +| `OPENAI_API_KEY` | Yes | Resolved by Mastra's model router. `OPENAI_BASE_URL` is honored, which is how the fixture harness replays recorded calls. | +| `PORT` | Yes, here | Defaults to `8321`. The example's dev proxy targets `5332`, so set it. | +| `AG_UI_MASTRA_DB_PATH` | No | LibSQL file path. Defaults to `data/mastra.db` beside the service. It must be persistent, because suspended-run snapshots live there. | + + +Export only the variables you need. A stray `AG_UI_INTERNAL_TOKEN` that disagrees with the dev proxy surfaces as a 401 that reads like an OpenAI authentication failure. + + + + + +```bash +npx tsx apps/cockpit/scripts/serve-example.ts --capability=rt-mastra +``` + +Or run the target directly: + +```bash +npx nx run cockpit-runtimes-mastra-angular:serve:cockpit --port 4332 +``` + +The example's `proxy.conf.mjs` rewrites `/agent` to `http://localhost:5332/agent/mastra` and injects `X-Internal-Token: dev-local-token`. Override that header value with the `AG_UI_INTERNAL_TOKEN` environment variable if you started the service with a different token. + + + + +Visit `http://localhost:4332`. The service exposes an unauthenticated health check at `http://localhost:5332/ok`; the run endpoint is `http://localhost:5332/agent/mastra`. + + + + +1. *"Start a packing list with a tent and two sleeping bags."* — writes the list into working memory, which reaches the frontend as shared state. +2. *"What are the trail conditions at Point Reyes?"* — calls `check_conditions`. +3. *"Reserve the Sky Camp site for two nights."* — calls `reserve_campsite`, which suspends the run and renders an approval card. +4. Approve or decline the card — the run resumes from the persisted snapshot and the agent confirms the outcome. + + + + +## Running the service's own tests + +```bash +cd deployments/ag-ui-mastra +npm test +``` + +These assert the Server-Sent Events grammar of every surface against the captured 2026-08-31 transcripts, and drive the interrupt-to-resume round trip through the real `@ag-ui/client` — the same client the Angular adapter wraps. The model is a scripted mock, so no network and no key are required. + +## Next steps + +- [How It Connects](/docs/runtimes/mastra/how-it-connects) — the wire conventions this example relies on. +- [Overview](/docs/runtimes/mastra/overview) — what the integration does and does not support. 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 new file mode 100644 index 000000000..7d5dd4c27 --- /dev/null +++ b/apps/website/content/docs/runtimes/microsoft-agent-framework/how-it-connects.mdx @@ -0,0 +1,67 @@ +--- +title: How It Connects +description: Measured AG-UI wire behavior for Microsoft Agent Framework: outcome interrupts, resume entries for every pending interrupt, and real state deltas. +--- + +# How Microsoft Agent Framework Connects + +This page records the AG-UI wire behavior measured for the Microsoft Agent Framework 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. + +## Transport + +The example serves a single AG-UI endpoint from FastAPI. + +| Route | Method | Notes | +|---|---|---| +| `/agent` | `POST` | `RunAgentInput` JSON in, Server-Sent Events out. | +| `/ok` | `GET` | Unauthenticated health check. | + +On the Angular side that is an ordinary `provideAgent({ url: '/agent' })`. Nothing about the adapter configuration is Microsoft-specific. + +## Interrupts use the outcome convention + +A tool declares `approval_mode="always_require"`, and the bridge finishes the run with the protocol-standard outcome: + +```json +{ + "type": "RUN_FINISHED", + "outcome": { + "type": "interrupt", + "interrupts": [{ "interruptId": "...", "value": { } }] + } +} +``` + +This matches AWS Strands and differs from the LangGraph bridge, which signals interrupts only through a `CUSTOM` event named `on_interrupt`. The adapter detects either convention; within a single run, the first signal it sees wins. + +## Resume must address every pending interrupt + +Like Strands, this runtime reads the protocol-standard top-level `resume` array of `{ interruptId, status, payload }` entries. Unlike Strands, it expects an entry for **every** pending interrupt, not only the one the user just answered. + +Application code does not assemble that. You call the neutral `submit({ resume })`, and the adapter derives the wire shape, including the entries for interrupts still outstanding. + +## State streams predictively + +`predict_state_config` maps a tool argument onto a state key: + +```python +predict_state_config={ + "expense": {"tool": "submit_expense", "tool_argument": "expense"}, +} +``` + +The bridge emits `STATE_SNAPSHOT` followed by real `STATE_DELTA` events as the argument is generated, so the frontend sees the expense fill in before `submit_expense` has been called or approved. + +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 + +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. + +## Next steps + +- [Overview](/docs/runtimes/microsoft-agent-framework/overview) — what the integration supports. +- [AWS Strands — How It Connects](/docs/runtimes/aws-strands/how-it-connects) — the same outcome convention, snapshot-only state. +- [Choosing an adapter](/docs/choosing-an-adapter) — the full matrix and its cause analysis. diff --git a/apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx b/apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx new file mode 100644 index 000000000..46c2e158a --- /dev/null +++ b/apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx @@ -0,0 +1,56 @@ +--- +title: Overview +description: What the Microsoft Agent Framework integration demonstrates through @threadplane/ag-ui, including predictive state streaming. +--- + +# Microsoft Agent Framework Overview + +[Microsoft Agent Framework](https://github.com/microsoft/agent-framework) is Microsoft's Python and .NET agent SDK. Its AG-UI bridge turns an `Agent` into an AG-UI event stream, which is all `@threadplane/ag-ui` needs in order to bind it to ``. + +The Threadplane example is an expense assistant. It looks up policy through a server-side tool, streams a proposed expense into frontend state while the model is still writing it, and requires human approval before submitting. + + +The hosted example runs at [examples.threadplane.ai/runtimes/microsoft-agent-framework](https://examples.threadplane.ai/runtimes/microsoft-agent-framework/). The source is [`cockpit/runtimes/microsoft-agent-framework`](https://github.com/cacheplane/angular-agent-framework/tree/main/cockpit/runtimes/microsoft-agent-framework). + + +## What the integration demonstrates + +| Surface | Status | How | +|---|---|---| +| Messages | Supported | Streamed assistant text from `Agent` in `agent-framework-core`. | +| 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. | + +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. + +## Predictive state is the interesting part + +`predict_state_config` maps a tool argument onto a state key: + +```python +predict_state_config={ + "expense": {"tool": "submit_expense", "tool_argument": "expense"}, +} +``` + +The bridge then streams the `expense` argument of `submit_expense` into frontend state as the model generates it, over `STATE_SNAPSHOT` and `STATE_DELTA`. The user watches the expense fill in before the tool has been called, let alone approved. + +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 + +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. + +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. + +## Model access + +Azure OpenAI is the default path. When `AZURE_OPENAI_ENDPOINT` is set, the agent routes through Azure OpenAI with key authentication. When it is absent, the agent falls back to the plain OpenAI client on `OPENAI_API_KEY`. + +## Next steps + +- [Quickstart](/docs/runtimes/microsoft-agent-framework/quickstart) — run the example locally. +- [How It Connects](/docs/runtimes/microsoft-agent-framework/how-it-connects) — the measured wire conventions. +- [Choosing an adapter](/docs/choosing-an-adapter) — the full runtime matrix and its cause analysis. diff --git a/apps/website/content/docs/runtimes/microsoft-agent-framework/quickstart.mdx b/apps/website/content/docs/runtimes/microsoft-agent-framework/quickstart.mdx new file mode 100644 index 000000000..965bc7575 --- /dev/null +++ b/apps/website/content/docs/runtimes/microsoft-agent-framework/quickstart.mdx @@ -0,0 +1,76 @@ +--- +title: Quickstart +description: Run the Microsoft Agent Framework runtime example locally on ports 5330 and 4330, with Azure OpenAI or plain OpenAI. +--- + +# Microsoft Agent Framework Quickstart + +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 5330; the Angular dev server is on port 4330 and proxies `/agent` to it. + + +Node.js 20 or newer, Python 3.11 or newer, [`uv`](https://docs.astral.sh/uv/), and either an Azure OpenAI resource or an OpenAI API key. + + + + + +```bash +git clone https://github.com/cacheplane/angular-agent-framework.git +cd angular-agent-framework +npm ci +``` + + + + +Copy the example file and fill in whichever model path you are using. + +```bash +cp cockpit/runtimes/microsoft-agent-framework/python/.env.example \ + cockpit/runtimes/microsoft-agent-framework/python/.env +``` + +Azure OpenAI is the default path. Setting `AZURE_OPENAI_ENDPOINT` selects it; leaving it unset selects the OpenAI fallback. + +| Variable | Required | Purpose | +|---|---|---| +| `AZURE_OPENAI_ENDPOINT` | For Azure | Resource endpoint. Its presence is what selects the Azure path. | +| `AZURE_OPENAI_API_KEY` | For Azure | Key authentication against that resource. | +| `AZURE_OPENAI_MODEL` | For Azure | Deployment name of a chat model, not a public model id. | +| `AZURE_OPENAI_API_VERSION` | No | Defaults to `2024-12-01-preview`. | +| `OPENAI_API_KEY` | For the fallback | Used only when `AZURE_OPENAI_ENDPOINT` is unset. | +| `OPENAI_CHAT_MODEL` | No | Model name for the fallback path. | +| `OPENAI_BASE_URL` | No | Redirects the OpenAI client. The end-to-end fixture harness sets this to replay recorded calls. | + + +Azure addresses models by the deployment name you chose in your resource, which is frequently not the public model id. A valid key paired with a public model id fails as a missing deployment. + + + + + +```bash +npx tsx apps/cockpit/scripts/serve-example.ts --capability=rt-maf +``` + +The script runs `uv sync` in `cockpit/runtimes/microsoft-agent-framework/python` on the way, so the first start takes longer than later ones. + + + + +Visit `http://localhost:4330`. The backend answers on `http://localhost:5330/agent`, with an unauthenticated health check at `http://localhost:5330/ok`. + + + + +1. *"What is the policy on client dinners?"* — streams a message and calls `lookup_expense_policy`. +2. *"Submit a 96 dollar client dinner from last Tuesday."* — the `expense` argument streams into shared state while the model writes it, then `submit_expense` pauses for approval. +3. Approve or decline the card — the run resumes and the agent confirms the outcome. + + + + +## Next steps + +- [How It Connects](/docs/runtimes/microsoft-agent-framework/how-it-connects) — the wire conventions this example relies on. +- [Overview](/docs/runtimes/microsoft-agent-framework/overview) — what the integration does and does not support. diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index 78967b46e..b36ebbf8e 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -207,6 +207,10 @@ export default function DocsLandingPage() { Chat quickstart → + {' '}Running a non-LangGraph backend?{' '} + + Agent runtimes → +

diff --git a/apps/website/src/components/docs/LibraryMark.tsx b/apps/website/src/components/docs/LibraryMark.tsx index 339fad941..c80a47f37 100644 --- a/apps/website/src/components/docs/LibraryMark.tsx +++ b/apps/website/src/components/docs/LibraryMark.tsx @@ -1,6 +1,6 @@ import type { LibraryId } from '../../lib/docs-config'; -type GlyphKey = 'chat' | 'middleware' | 'pulse'; +type GlyphKey = 'chat' | 'middleware' | 'pulse' | 'layers'; type MarkEntry = | { kind: 'logo'; src: string } @@ -14,6 +14,7 @@ const MARKS: Record = { chat: { kind: 'glyph', glyph: 'chat' }, middleware: { kind: 'glyph', glyph: 'middleware' }, telemetry: { kind: 'glyph', glyph: 'pulse' }, + runtimes: { kind: 'glyph', glyph: 'layers' }, }; function ChatGlyph({ s }: { s: number }) { @@ -41,10 +42,21 @@ function PulseGlyph({ s }: { s: number }) { ); } +function LayersGlyph({ s }: { s: number }) { + return ( + + ); +} + const GLYPHS: Record React.JSX.Element> = { chat: ChatGlyph, middleware: MiddlewareGlyph, pulse: PulseGlyph, + layers: LayersGlyph, }; interface Props { diff --git a/apps/website/src/lib/docs-config.ts b/apps/website/src/lib/docs-config.ts index dd7886b0a..d802630ab 100644 --- a/apps/website/src/lib/docs-config.ts +++ b/apps/website/src/lib/docs-config.ts @@ -5,7 +5,8 @@ export type LibraryId = | 'ag-ui' | 'a2ui' | 'middleware' - | 'telemetry'; + | 'telemetry' + | 'runtimes'; export interface DocsPage { title: string; @@ -426,6 +427,51 @@ export const docsConfig: DocsLibrary[] = [ }, ], }, + { + id: 'runtimes', + title: 'Runtimes', + description: 'Measured AG-UI runtime integrations behind @threadplane/ag-ui', + sections: [ + { + title: 'Getting Started', + id: 'getting-started', + color: 'blue', + pages: [ + { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, + ], + }, + { + title: 'AWS Strands', + id: 'aws-strands', + color: 'blue', + pages: [ + { title: 'Overview', slug: 'overview', section: 'aws-strands' }, + { title: 'Quickstart', slug: 'quickstart', section: 'aws-strands' }, + { title: 'How It Connects', slug: 'how-it-connects', section: 'aws-strands' }, + ], + }, + { + title: 'Microsoft Agent Framework', + id: 'microsoft-agent-framework', + color: 'red', + pages: [ + { title: 'Overview', slug: 'overview', section: 'microsoft-agent-framework' }, + { title: 'Quickstart', slug: 'quickstart', section: 'microsoft-agent-framework' }, + { title: 'How It Connects', slug: 'how-it-connects', section: 'microsoft-agent-framework' }, + ], + }, + { + title: 'Mastra', + id: 'mastra', + color: 'blue', + pages: [ + { title: 'Overview', slug: 'overview', section: 'mastra' }, + { title: 'Quickstart', slug: 'quickstart', section: 'mastra' }, + { title: 'How It Connects', slug: 'how-it-connects', section: 'mastra' }, + ], + }, + ], + }, ]; export function getLibraryConfig(libraryId: string): DocsLibrary | undefined { From fbe43ee971b522fd7a9eeeba449595111e3e84b7 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 31 Aug 2026 19:49:30 -0700 Subject: [PATCH 2/2] docs(website): present the Mastra hosted demo as live The Mastra Railway service and AG_UI_MASTRA_URL are now provisioned, and POST examples.threadplane.ai/runtimes/mastra/agent returns a real RUN_STARTED -> TEXT_MESSAGE_CHUNK -> RUN_FINISHED stream. Drop the hosted-backend caveat and the local-quickstart-as-workaround framing, and match the See-it-live callout the Strands and MAF overviews use. Also correct the now-false 'not yet running in the hosted demo deployment' note on the runtime matrix. Co-Authored-By: Claude Opus 5 --- apps/website/content/docs/choosing-an-adapter/index.mdx | 4 ++-- apps/website/content/docs/runtimes/mastra/overview.mdx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/website/content/docs/choosing-an-adapter/index.mdx b/apps/website/content/docs/choosing-an-adapter/index.mdx index 9165988e3..2e17a328f 100644 --- a/apps/website/content/docs/choosing-an-adapter/index.mdx +++ b/apps/website/content/docs/choosing-an-adapter/index.mdx @@ -123,9 +123,9 @@ AWS Strands and Microsoft Agent Framework both read the protocol-standard top-le The LangGraph bridge reads `forwardedProps.command.resume`. You pass one neutral `submit({ resume })`, and the adapter derives the wire shape from how the interrupt arrived. -**The Mastra row was measured, but not in the hosted demo.** +**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 yet running in the hosted demo deployment. +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. diff --git a/apps/website/content/docs/runtimes/mastra/overview.mdx b/apps/website/content/docs/runtimes/mastra/overview.mdx index 161d557c9..21ade5f11 100644 --- a/apps/website/content/docs/runtimes/mastra/overview.mdx +++ b/apps/website/content/docs/runtimes/mastra/overview.mdx @@ -9,8 +9,8 @@ description: What the Mastra integration demonstrates through @threadplane/ag-ui The Threadplane example is a camping trip planner. It streams messages, calls a backend tool, keeps a packing list in shared state through Mastra's working memory, and suspends a run for human approval before reserving a campsite. - -Unlike the two Python runtimes, Mastra is not served by the shared FastAPI deployment. Its backend is a separate Node service with its own hosting lane, so the [hosted example page](https://examples.threadplane.ai/runtimes/mastra/) may not have a reachable backend behind it. The [Quickstart](/docs/runtimes/mastra/quickstart) is the supported path. + +The hosted example runs at [examples.threadplane.ai/runtimes/mastra](https://examples.threadplane.ai/runtimes/mastra/). The Angular source is [`cockpit/runtimes/mastra`](https://github.com/cacheplane/angular-agent-framework/tree/main/cockpit/runtimes/mastra), and its backend is [`deployments/ag-ui-mastra`](https://github.com/cacheplane/angular-agent-framework/tree/main/deployments/ag-ui-mastra) rather than the FastAPI deployment the two Python runtimes share. ## What the integration demonstrates