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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/website/content/docs/choosing-an-adapter/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
80 changes: 80 additions & 0 deletions apps/website/content/docs/runtimes/aws-strands/how-it-connects.mdx
Original file line number Diff line number Diff line change
@@ -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.
48 changes: 48 additions & 0 deletions apps/website/content/docs/runtimes/aws-strands/overview.mdx
Original file line number Diff line number Diff line change
@@ -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 `<chat>`.

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.

<Callout type="tip" title="See it live">
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).
</Callout>

## 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.
78 changes: 78 additions & 0 deletions apps/website/content/docs/runtimes/aws-strands/quickstart.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Callout type="info" title="Prerequisites">
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.
</Callout>

<Steps>
<Step title="Install workspace dependencies">

```bash
git clone https://github.com/cacheplane/angular-agent-framework.git
cd angular-agent-framework
npm ci
```

</Step>
<Step title="Configure the backend environment">

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. |

</Step>
<Step title="Start both halves">

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.

</Step>
<Step title="Open the app">

Visit `http://localhost:4331`. The backend answers on `http://localhost:5331/agent`, with an unauthenticated health check at `http://localhost:5331/ok`.

</Step>
<Step title="Exercise every surface">

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.

</Step>
</Steps>

## 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.
Original file line number Diff line number Diff line change
@@ -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.

<Callout type="info" title="What this section is not">
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.
</Callout>

## The runtimes

<CardGroup>
<Card title="AWS Strands" href="/docs/runtimes/aws-strands/overview">
Python. Messages, tool calls, and interrupts work. Shared state is snapshot-only and opt-in per tool.
</Card>
<Card title="Microsoft Agent Framework" href="/docs/runtimes/microsoft-agent-framework/overview">
Python. Messages, tool calls, state, and interrupts all work. Azure OpenAI by default.
</Card>
<Card title="Mastra" href="/docs/runtimes/mastra/overview">
TypeScript. Messages, tool calls, state, and interrupts all work, against a hand-written Node hosting service.
</Card>
</CardGroup>

## 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: `<chat [agent]="agent" />`,
})
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.
Loading
Loading