diff --git a/apps/website/content/docs/runtimes/aws-strands/overview.mdx b/apps/website/content/docs/runtimes/aws-strands/overview.mdx index dfa094cb7..c568f9631 100644 --- a/apps/website/content/docs/runtimes/aws-strands/overview.mdx +++ b/apps/website/content/docs/runtimes/aws-strands/overview.mdx @@ -1,23 +1,102 @@ --- title: Overview -description: What the AWS Strands integration demonstrates through @threadplane/ag-ui, and where its shared-state support stops short. +description: How the AWS Strands example books meetings over AG-UI, and why its shared-state support is measured as partial. --- # 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 ``. +[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 running example is a meeting scheduler that looks up open slots, pauses for human approval before it books anything, and delegates research to a specialist, and this page walks the files that make it work. The Angular component it renders is the same UI code the LangGraph-backed examples use; only the provider and the backend behind it differ. -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. +## What the demo does - -The hosted example runs the AWS Strands integration end to end. +The Run tab shows the prebuilt `` composition in front of a Strands agent served over AG-UI, with a side panel titled "Shared state — schedule" to the right of the transcript. Two welcome suggestions set it up: "Book the Q3 roadmap review" asks for a Tuesday meeting with the platform team, and "Book a design critique" runs the same two steps for the web team on Thursday. - - Run the example - View source - +The agent calls `check_availability` for the requested weekday, and that day and its open slots appear in the side panel. Then it calls `book_meeting`, the run stops, and a modal card titled "Booking approval required" shows the topic and the chosen slot above two buttons, Cancel and Approve. Approve resumes the run and the agent confirms the booking in one sentence; Cancel resumes it with a rejection and nothing is booked. Either way the panel keeps showing the booking as it was snapshotted before the pause; the final booked or declined status lives only in the agent's confirmation sentence, because no hook emits state after the interrupt resolves. + +Delegation is the third thing to try. Type a research request instead, for example "Find a slot for Ada and Grace next week — research their availability first", and the agent calls `research_availability`. That call renders as a subagent card carrying the specialist's own answer, streamed token by token inside the card rather than into the parent bubble. + +## How it is built + +Four files carry the example: a Python module holding the Strands agent and its tools, a FastAPI server that mounts it, an application config that registers the agent, and a component that renders the state panel and the approval card. Open the Code tab to read them in place. `subagent_emitter.py`, a fifth file in the same backend directory and not shown in the Code tab, holds the translation from the specialist's stream into the protocol's subagent events. + +### An ordinary backend tool + +`check_availability` is a plain Strands `@tool`. It executes server-side and never pauses, so on the wire it is a tool call with a result and nothing else. The function under it is the state hook that mirrors the lookup into shared state, registered against the tool further down. + + + +The hook reads the tool result from `context.result_data` and tolerates the string form, because a result that has round-tripped through JSON arrives as text. + +### Pausing for a human decision + +`book_meeting` is a context tool: Strands hands it a `ToolContext`, and `tool_context.interrupt(...)` parks the tool mid-execution. The first argument is the name the client sees on the pending interrupt, and `reason` is the payload the approval card renders. + + + +When the human answers, the call returns the decision and the rest of the function runs to completion in the resumed run. + + +AWS Strands signals an interrupt only through the protocol-standard `RUN_FINISHED` outcome, `{ type: 'interrupt', interrupts: [...] }`, never through the LangGraph bridge's `CUSTOM` event named `on_interrupt`. It reads the decision back from the protocol-standard top-level `resume` array, one `{ interruptId, status, payload }` entry per interrupt. The adapter accepts either signal and sends the shape the runtime reads, so nothing in the component changes when the backend does. +### Delegating to a specialist + +`research_availability` is an async-generator tool. It re-yields every event the specialist emits, keeps the text deltas as it passes them along, and yields the joined text last, because Strands takes the last yielded value as the tool result. Each yielded value crosses the bridge as a `tool_stream_event`, which is the seam the subagent emitter listens on. + + + +The specialist itself is an ordinary Strands `Agent` with its own system prompt and no tools of its own. + + + +### Registering the per-tool behaviors + +Everything that makes this example more than streamed text is registered in one place. `StrandsAgentConfig.tool_behaviors` maps a tool name to a `ToolBehavior`: `state_from_result` for the availability lookup, `state_from_args` for the booking, and `tool_stream_event_handler` for the delegation tool. Registering a stream handler for a tool gives that handler the whole child stream. + + + +The orchestrator binds all three tools and the `StrandsAgent` wrapper is what the server mounts. + +### Serving the agent over AG-UI + +The backend is a FastAPI application. `add_strands_fastapi_endpoint` from the `ag-ui-strands` package mounts the wrapped agent at a path that speaks the AG-UI event stream. + + + +### Providing the agent + +`provideAgent()` registers the agent once for the whole application, and `provideChat({})` registers the configuration the `` composition reads, here left at its defaults. The example passes a factory because it resolves its endpoint at runtime from the host that serves the demo. Your own application does not need the factory. + + + +Pass the URL of your AG-UI endpoint directly: + +```typescript +provideAgent({ + url: 'https://your-backend.example.com/agent', +}); +``` + +### Reading shared state in the component + +`injectAgent()` returns the adapter's agent, and `agent.state()` is the Signal the snapshots land on. The component narrows that object into two computed Signals, treating a half-filled entry as absent so the panel does not render an empty row. + + + +The panel itself is plain Angular template code reading those two Signals, with a placeholder line for the empty case. + +### The approval card + +`` opens as a modal whenever the agent has a pending interrupt, and the `#body` template names what is being approved. This one renders the topic and the slot. + + + +The class supplies that body from the pending interrupt and maps the card's two buttons onto resume payloads. The adapter stores the interrupt outcome as `{ interrupts: [...], runId }`; each Strands entry carries the tool name under `reason` and the tool's own payload under `metadata.reason`. + + + +`submit({ resume })` is the only interrupt-specific call in the component, and it is the same call every other AG-UI example makes. + ## What the integration demonstrates | Surface | Status | How | @@ -30,24 +109,43 @@ The hosted example runs the AWS Strands integration end to end. ## 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`. +The Strands bridge never emits `STATE_DELTA`. Outbound state exists only where a tool opts in through a per-tool `ToolBehavior` hook, which is why the example registers `state_from_result` on `check_availability` and `state_from_args` on `book_meeting` and gets nothing from `research_availability`. + +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, so the example keeps one module-level object and composes the whole thing on every emission. -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. + + +The booking hook shows what that costs in practice. It fires on the tool-call arguments, before the interrupt pauses the run, so the approval card can render a pending booking from shared state — and it still has to return both keys, not just the one it touched. + + 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. ## How subagents surface -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. +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`. Natively the bridge forwards only the inner tool-call lifecycle, so a delegated run reaches the browser as one opaque result string with no child text at all. -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. +The handler registered on `research_availability` closes that gap. `subagent_emitter.py` translates the specialist's stream into `SUBAGENT_STARTED`, attributed `TEXT_MESSAGE_*` deltas carrying `subagentRunId`, and `SUBAGENT_FINISHED`, deriving every identifier from the tool call identifier the bridge already put on the wire. The adapter routes those attributed events into a subagent entry instead of the parent transcript, which is why the card streams the specialist's tokens live. -## Model access +The wire capture behind that matrix cell is committed beside the backend at [`docs/wire-capture-subagents.md`](https://github.com/cacheplane/angular-agent-framework/blob/main/cockpit/runtimes/aws-strands/python/docs/wire-capture-subagents.md), before and after the emitter. Multi-agent routes crash the stale published wheel, which is one reason the example pins the bridge to a git reference instead. -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 +## Model access -- [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. +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. `OPENAI_BASE_URL` is honored when it is set, which is how the end-to-end harness replays recorded model calls against this backend. + +## What's Next + + + + The measured AG-UI wire behavior for this runtime. + + + The same matrix with the cause analysis behind each partial cell. + + + How the adapter attributes a delegated run to the tool call that spawned it. + + + Run this backend locally and point an Angular application at it. + + diff --git a/apps/website/content/docs/runtimes/mastra/overview.mdx b/apps/website/content/docs/runtimes/mastra/overview.mdx index 0129c064d..40a1a68d1 100644 --- a/apps/website/content/docs/runtimes/mastra/overview.mdx +++ b/apps/website/content/docs/runtimes/mastra/overview.mdx @@ -1,23 +1,120 @@ --- title: Overview -description: What the Mastra integration demonstrates through @threadplane/ag-ui, and why it needs a hand-written Node hosting service. +description: How the Mastra example is built, from the Node service that serves AG-UI events to the Angular component that renders them. --- # 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. +[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, so the backend here is a small hosting service written by hand. The running example is a camping trip planner, and this page walks the four files the Code tab carries: two backend files under `deployments/ag-ui-mastra`, an application config, and a component. [Quickstart](/docs/runtimes/mastra/quickstart) runs the same example locally. -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. +## What the demo does - -The hosted example runs the Mastra integration end to end. 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. +The Run tab shows the prebuilt `` composition beside a panel that mirrors the agent's packing list. Three welcome suggestions set it up: "Start a packing list" seeds a titled list with a tent and two sleeping bags and the panel fills in as the agent writes it, "Check trail conditions" makes the agent call a backend tool, and "Reserve the campsite" asks for two nights at North Pines. - - Run Mastra example - View source - +The reservation is the interesting one. It suspends the run rather than booking anything, and an approval card appears with the campsite, the number of nights, and the total. Approve resumes the agent with a confirmation number, and Cancel resumes it with a refusal. + +Ask for a weather forecast and the agent delegates to a second agent instead of answering itself. The child's answer streams into a subagent card in the transcript while it is still being written. + +## How it is built + +Four files carry the example. Two are the backend: `agents.mjs` defines the Mastra agents, and `server.mjs` serves them over HTTP. The other two are the Angular side: a config that registers the agent, and a component that adds the state panel and the approval card around ``. Open the Code tab to read them in place. + +### A backend tool + +`createTool` describes a tool with Zod schemas for its input and output and an `execute` body that runs on the server. `check_conditions` returns a fixed forecast, so the demo behaves the same on every run. + + + +Nothing in this tool is protocol-aware; the bridge turns the call into `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END`, and `TOOL_CALL_RESULT` on the wire. + +### Pausing a run for approval + +A tool becomes a human-in-the-loop step by adding a `suspendSchema` and a `resumeSchema` to that same shape. The first call arrives with no resume data, so the body calls `suspend()` with the payload the approval card renders. The resumed call arrives with the operator's decision and either books the site or reports the refusal. + + + +The suspended run is written to storage, which is why the database path further down has to be durable. + +### The child agent + +A sub-agent is an ordinary Mastra `Agent` with its own instructions. Its `description` is what the parent model reads when it decides whether to delegate. + + + +### The trip agent and its working memory + +The parent agent collects everything: the instructions, the two tools, the child agent under `agents`, and a `Memory` whose working memory is a typed Zod schema. That schema is the shared state the frontend reads. Registering the child under `agents` is the entire delegation wiring in the agent definition. + + + +`MODEL` is the plain string `openai/gpt-4o-mini`, which Mastra's model router resolves from the standard `OPENAI_API_KEY` and `OPENAI_BASE_URL` variables with no provider SDK wiring. + + +Mastra has no separate state object to publish. The packing list is working memory. The bridge streams it as a `STATE_SNAPSHOT` plus real JSON-Patch `STATE_DELTA` events while the agent edits the list, and the adapter applies those patches to the state signal the component reads. +### The route contract + +`server.mjs` is a plain `node:http` server. Its contract mirrors the Python lane's: `GET /ok` is unauthenticated for health checks, every other route requires the `X-Internal-Token` header, and topics are served at `POST /agent/`, resolved against the Mastra instance by name. + + + +The service refuses to boot at all without `AG_UI_INTERNAL_TOKEN`, rather than serve an unauthenticated model proxy. + +### One event, one Server-Sent Events frame + +The whole encoding is a single function. An AG-UI event becomes one `data:` frame. + + + +That is the wire shape `@ag-ui/client` parses, and the same shape the FastAPI bridges emit for the Python runtimes. + +### Running the bridge + +Each request builds a fresh `MastraAgent`, because the bridge carries per-run state, and scopes Mastra memory to the conversation by keying `resourceId` on the inbound thread identifier. `bridge.run(input)` returns an Observable of AG-UI events, and every event it emits is written as a frame. An Observable error becomes a `RUN_ERROR` frame rather than a dropped socket, so the client finalizes the run instead of hanging. + +A per-run `createSubagentInjector` sits in front of the frame writer with two methods: `injector.chunk` reads the raw stream-tee chunks and emits the eager subagent events, and `injector.eventsFor` reads the bridge's own AG-UI events and drops its later, redundant delegation copies. + + + +The agent handed to the bridge is wrapped first, which is what makes the subagent card stream live; the section below explains why that wrapper exists. + +### The agent provider + +`provideAgent()` registers the agent once for the whole application, and `provideChat({})` registers the configuration the `` composition reads, here left at its defaults. Neither is Mastra-specific: this is the same config every AG-UI example uses. The example passes a factory because it resolves its endpoint at runtime from the host that serves the demo. + + + +Your own application passes the URL directly: + +```typescript +provideAgent({ + url: 'https://your-backend.example.com/agent', +}); +``` + +### Reading shared state in the component + +The component reads the packing list off `agent.state()`, the runtime-neutral signal the adapter maintains from the state events. Working memory arrives under its schema key, so the component narrows that key and treats a list without a title as absent. + + + +The panel then renders that signal like any other computed value, with an empty state before the first snapshot. + + + +### The approval card + +`` renders whenever the agent has a pending interrupt and reports the operator's choice through its `action` output. The example projects a body template into it so the card shows the campsite, the nights, and the total rather than a generic prompt. + + + +The class supplies that payload and maps the two actions onto resume calls. + + + +`agent.interrupt()` carries the parsed `CUSTOM on_interrupt` payload as its `value`, and because that payload carries a tool-call id, `submit({ resume })` goes back out as `forwardedProps.command = { resume, interruptEvent: { toolCallId, runId } }` — the shape the Mastra bridge reads to reopen the suspended run. + ## What the integration demonstrates | Surface | Status | How | @@ -26,34 +123,47 @@ The hosted example runs the Mastra integration end to end. Its backend is [`depl | 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 | Supported | An in-tree stream tee observes the parent stream ahead of the bridge and emits the delegation tool call eagerly, `SUBAGENT_*` lifecycle, and the child's token deltas under the subagent identity. | +| Subagents | Supported (streaming) | An in-tree stream tee observes the parent stream ahead of the bridge and emits the delegation tool call eagerly, `SUBAGENT_*` lifecycle, and the child's token deltas under the subagent identity. | ## Upstream ships no HTTP endpoint `@ag-ui/mastra` provides an in-process `MastraAgent` bridge and a mount for its own chat frontend runtime. 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. +Threadplane therefore maintains the hosting service walked above, `deployments/ag-ui-mastra`. 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. +Mastra persists memory and suspended-run snapshots to LibSQL file storage, which is the `dbUrl` passed into `createMastra`. 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. ## How subagents surface Mastra registers a child agent as a delegation tool named `agent-`, and while the child runs its every chunk is forwarded on the parent stream as a public `tool-output` chunk. The runtime's bridge drops those chunks and withholds the delegation tool call until the child resolves, so on its own the wire would carry only the child's final text, after a silent gap. -The hosting service therefore wraps the agent in a small stream tee (`deployments/ag-ui-mastra/streaming-tee.mjs`) that observes each chunk before the bridge processes it, and a per-run injector (`deployments/ag-ui-mastra/subagent-emitter.mjs`) maps them to the protocol: the delegation `tool-call` chunk becomes an eager `TOOL_CALL_START`, `TOOL_CALL_ARGS`, and `TOOL_CALL_END` plus `SUBAGENT_STARTED`; each child text delta becomes a `TEXT_MESSAGE_CONTENT` attributed to the subagent; and the delegation result becomes `SUBAGENT_FINISHED` or `SUBAGENT_ERROR`. The bridge's own copy of the delegation tool call, flushed at the result, is dropped so the wire carries it once. +The hosting service therefore wraps the agent in a small stream tee, `streaming-tee.mjs`, that observes each chunk before the bridge processes it, and a per-run injector, `subagent-emitter.mjs`, maps them to the protocol: the delegation `tool-call` chunk becomes an eager `TOOL_CALL_START`, `TOOL_CALL_ARGS`, and `TOOL_CALL_END` plus `SUBAGENT_STARTED`; each child text delta becomes a `TEXT_MESSAGE_CONTENT` attributed to the subagent; and the delegation result becomes `SUBAGENT_FINISHED` or `SUBAGENT_ERROR`. The bridge's own copy of the delegation tool call, flushed at the result, is dropped so the wire carries it once. Both files sit next to `server.mjs` rather than in the Code tab. -The bridge itself is unmodified; the tee touches only the public agent members the bridge reads. The child's tokens reach the card while it is still running, which is what flips this cell to Supported. 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). +The bridge itself is unmodified; the tee touches only the public agent members the bridge reads. The child's tokens reach the card while it is still running, which is what flips this cell to Supported. The component contains no subagent code at all, because the card is rendered from the projection the adapter maintains. ## 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. +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`. [How it connects](/docs/runtimes/mastra/how-it-connects) records the resulting wire conventions. + +The subagent capture behind the Subagents row is committed beside the example as [`angular/docs/wire-capture-subagents.md`](https://github.com/cacheplane/angular-agent-framework/blob/main/cockpit/runtimes/mastra/angular/docs/wire-capture-subagents.md). + +## What's Next + + + + The measured support table and what stays the same across runtimes. + + + The full matrix and the cause analysis behind every cell. + + + How attributed child runs become cards in the transcript. + + + Run this backend locally and point an Angular application at it. + + 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 0c10269ef..fca702d95 100644 --- a/apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx +++ b/apps/website/content/docs/runtimes/microsoft-agent-framework/overview.mdx @@ -1,23 +1,104 @@ --- title: Overview -description: What the Microsoft Agent Framework integration demonstrates through @threadplane/ag-ui, including predictive state streaming. +description: How the Microsoft Agent Framework example is built, from the approval tool and predictive state on the Python side to the Angular component that renders both. --- # 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 ``. +[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 running example is an expense assistant, and this page walks the four files that make it work. -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. +## What the demo does - -The hosted example runs the Microsoft Agent Framework integration end to end. +The Run tab shows the prebuilt `` composition in front of an expense approval copilot served over AG-UI, with a shared-state panel beside it. Two starter suggestions are offered: "File a team dinner expense" and "File a monitor purchase". Pick either one and the agent researches the reimbursement policy, delegating part of that work to a specialist agent and calling a server-side policy tool, then drafts the expense: vendor, category, amount, and memo appear in the side panel while the model is still writing the tool call, before the tool has been invoked. - - Run the example - View source - +The draft never submits itself. `submit_expense` requires human approval, so the run pauses and a modal approval card shows the amount, vendor, category, and memo with Cancel and Approve buttons. Approve it and the agent confirms the submission in one sentence; cancel it and the agent acknowledges that nothing was filed. + +## How it is built + +Four files carry the example: a Python module holding the agent and its tools, a FastAPI server that mounts it, an application config that registers the agent, and a component that renders the chat, the state panel, and the approval card. A fifth file next to the agent, `src/subagent_emitter.py`, holds the delegation events; it is discussed below but is not one of the four. + +### The policy tool + +Tools are plain functions decorated with `@tool`. This one is the ordinary case: it executes server-side, returns a string, and never pauses the run. + + + +The decorator's `name` and `description` are the model-facing contract, which is why the categories live in the module-level policy table and the `Expense.category` field description rather than only in the system prompt. + +### The tool that requires approval + +The second tool takes a single pydantic argument, an `Expense` with `vendor`, `category`, `amount_usd`, and `memo` fields, and it carries one extra decorator argument: `approval_mode="always_require"`. That argument is the entire interrupt configuration. The framework stops before the body runs and asks the caller for a decision, and the bridge reports that pause to the client. + + + + +On the approval-resume path the framework replays the stored tool-call arguments as plain dictionaries rather than re-validating them through pydantic. The body therefore normalizes with `Expense.model_validate` before reading attributes; a body that assumes a model instance raises on the resume turn only. +### Delegating to a specialist + +The delegation tool is `research_policy`. `policy_researcher` is a second `Agent` with its own instructions and no tools of its own, and `research_policy` streams it from inside a tool body. The bridge forwards nothing from an agent running inside a tool body on its own: only the tool's return string reaches the wire, as a tool result. + + +The `delegation_*` helpers close that gap. They live in `src/subagent_emitter.py`, they build typed `ag_ui.core` events, and they enqueue them onto a queue that a run wrapper merges into the bridge's own event stream, so `SUBAGENT_STARTED`, the specialist's `TEXT_MESSAGE_*` deltas stamped with a `subagentRunId`, and `SUBAGENT_FINISHED` all reach the client while the bridge generator is still suspended inside the tool. Outside that wrapped run the helpers are no-ops, which keeps direct agent runs free of side effects. + + + + +The wire capture behind the design is committed next to the module as [`python/docs/wire-capture-subagents.md`](https://github.com/cacheplane/angular-agent-framework/blob/main/cockpit/runtimes/microsoft-agent-framework/python/docs/wire-capture-subagents.md). + +### Choosing the model client + +Azure OpenAI is the default path, and the choice is made by one environment variable. + + + +Passing `azure_endpoint` explicitly is the constructor's strongest Azure signal, so Azure wins whenever `AZURE_OPENAI_ENDPOINT` is set, even when `OPENAI_API_KEY` is also present. Without it the plain OpenAI client is constructed with an explicit key, because the constructor otherwise falls back to Azure environment resolution and raises at import time. + +### Exposing the agent over AG-UI + +`AgentFrameworkAgent` wraps the framework agent and is the AG-UI surface. Two of its arguments carry the shared-state behavior: `state_schema` declares the shared-state keys the agent exposes; the bridge opens the run with them in the first `STATE_SNAPSHOT`. `predict_state_config` maps a tool argument onto one of those keys. + + + +With that mapping in place the bridge streams the `expense` argument of `submit_expense` into frontend state as the model generates it, over `STATE_SNAPSHOT` and `STATE_DELTA`, rather than waiting for the tool call to complete. + +### Serving the agent + +The backend is a FastAPI application, and `add_agent_framework_fastapi_endpoint` mounts the agent at a path that speaks the AG-UI event stream. + + + +The agent handed to the endpoint is the wrapped one, because the wrapper is the seam where the delegation events are merged into the stream the endpoint consumes. + +### The agent provider + +`provideAgent()` registers the agent once for the whole application, and `provideChat({})` registers the configuration the `` composition reads, here left at its defaults. Nothing in either provider is specific to this runtime. The example passes a factory because it resolves its endpoint at runtime from the host that serves the demo. + + + +Your own application does not need the factory. Pass the URL of your AG-UI endpoint directly: + +```typescript +provideAgent({ + url: 'https://your-backend.example.com/agent', +}); +``` + +### Rendering the streamed expense + +The adapter applies every `STATE_SNAPSHOT` and `STATE_DELTA` to one store and projects it as `agent.state()`. The component reads the `expense` key from that signal and holds the draft back until it has a vendor, which suppresses the panel until the vendor key exists, so it does not flash a half-written draft while the arguments stream. The panel itself is ordinary Angular template code over that signal. + + + +### Approving the tool call + +`` renders the pending approval as a modal dialog and emits `'approve'` or `'cancel'`. Its body template is yours to write, and this one reads the pending tool call out of `agent.interrupt()`, whose `value` the adapter sets to `{ interrupts: [...], runId }`, and each entry's `metadata.agent_framework.function_call` carries the tool name and its parsed arguments. + + + +Both branches call the same neutral `agent.submit({ resume })`, and the adapter derives the wire shape from how the interrupt arrived. + ## What the integration demonstrates | Surface | Status | How | @@ -28,34 +109,33 @@ The hosted example runs the Microsoft Agent Framework integration end to end. | Interrupts | Supported | `submit_expense` declares `approval_mode="always_require"`. | | 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: all five surfaces are green. - -## 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. +Together with Mastra this is the most complete third-party row in the measured matrix: all five surfaces are green. -Unlike AWS Strands, this runtime emits real deltas, so state does not have to be reassembled in full on every update. +## Why the state updates are deltas -## How subagents surface +Predictive state is the interesting part of this runtime. The bridge does not wait for the tool call to finish before telling the client about it: it emits real `STATE_DELTA` patches as the argument grows, and the adapter applies each patch to the state it already holds. The user watches the expense fill in before the tool has been called, let alone approved. -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. +That is not true of every runtime. On AWS Strands shared state is snapshot-only and opt-in per tool, so every update replaces the whole state object and each hook has to return it complete. Here the adapter reassembles nothing. -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). +## How the interrupt travels -## Model access +Microsoft Agent Framework signals an interrupt only through the protocol-standard `RUN_FINISHED` outcome, `{ type: 'interrupt', interrupts: [...] }`. It never uses the LangGraph bridge's `CUSTOM` event named `on_interrupt`. The adapter detects either convention, and within a single run the first signal wins. -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`. +The resume payload is shaped the same way. Because the interrupt arrived as an outcome, `submit({ resume })` goes out as the protocol-standard top-level `resume` array, one `{ interruptId, status, payload }` entry per pending interrupt — and this bridge expects an entry for every pending interrupt, not only the one the user answered. You pass one neutral resume value and the adapter builds that array. -## Next steps +## What's Next -- [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. + + + Run this example locally on ports 5330 and 4330, with Azure OpenAI or plain OpenAI. + + + The measured AG-UI wire behavior: outcome interrupts, resume entries, and real state deltas. + + + The full matrix with the cause of every gap, including the two adapter defects it found. + + + How the adapter attributes delegated runs and turns them into cards. + + diff --git a/apps/website/src/lib/docs-example-code.spec.ts b/apps/website/src/lib/docs-example-code.spec.ts index 3fcdac560..251c3e47f 100644 --- a/apps/website/src/lib/docs-example-code.spec.ts +++ b/apps/website/src/lib/docs-example-code.spec.ts @@ -34,9 +34,6 @@ const PENDING_PAGES = new Set([ '/docs/chat/guides/generative-ui', '/docs/chat/guides/theming', '/docs/chat/guides/thread-routing', - '/docs/runtimes/aws-strands/overview', - '/docs/runtimes/mastra/overview', - '/docs/runtimes/microsoft-agent-framework/overview', ]); const findWorkspaceRoot = (): string => { diff --git a/cockpit/runtimes/aws-strands/angular/src/app/aws-strands.component.ts b/cockpit/runtimes/aws-strands/angular/src/app/aws-strands.component.ts index cd1d993fc..b4e86a6f8 100644 --- a/cockpit/runtimes/aws-strands/angular/src/app/aws-strands.component.ts +++ b/cockpit/runtimes/aws-strands/angular/src/app/aws-strands.component.ts @@ -100,6 +100,7 @@ interface Booking { } + + `, @@ -235,6 +237,7 @@ export class AwsStrandsComponent { protected readonly agent = injectAgent(); + // region shared-state /** Shared state snapshotted from the backend (STATE_SNAPSHOT only). */ private readonly sharedState = computed(() => { // Example apps compile lib source with strict:false — cast at the read site. @@ -250,7 +253,9 @@ export class AwsStrandsComponent { const b = this.sharedState()?.booking; return b && b.topic !== undefined ? b : undefined; }); + // endregion + // region approval-wiring /** * The pending approval request from the protocol-standard interrupt * outcome. The reducer stores it as `{ interrupts: [...], runId }`; each @@ -282,4 +287,5 @@ export class AwsStrandsComponent { void this.agent.submit({ resume: { approved: false } }); } } + // endregion } diff --git a/cockpit/runtimes/aws-strands/python/docs/guide.md b/cockpit/runtimes/aws-strands/python/docs/guide.md deleted file mode 100644 index 49aceed0b..000000000 --- a/cockpit/runtimes/aws-strands/python/docs/guide.md +++ /dev/null @@ -1,46 +0,0 @@ -# Runtimes — AWS Strands - -Second entry on the one-capability-many-runtimes axis -(`cockpit/runtimes//`): the same neutral `Agent` contract and the -same `@threadplane/chat` UI primitives as every other AG-UI example, over a -backend that is genuinely not LangGraph. - -## What it demonstrates - -| Surface | How | -| --- | --- | -| Messages | Streamed assistant text from a Strands `Agent`. | -| Tool calls | `check_availability` executes server-side, no pause. | -| Shared state | SNAPSHOT-only, honestly: the Strands bridge never emits STATE_DELTA, and outbound state exists only where a tool opts in via per-tool `ToolBehavior` hooks — `state_from_result` on `check_availability`, `state_from_args` on `book_meeting`. Because snapshots replace the whole state object, every hook returns the COMPLETE state (a partial return would clobber sibling keys). | -| Interrupts | `book_meeting` parks in `tool_context.interrupt(...)`; the bridge finishes the run with the protocol-standard `RUN_FINISHED.outcome = { type: 'interrupt', interrupts: [...] }` and resumes from the client's top-level `resume` entries keyed by `interruptId`. | -| Subagents | Not demonstrated — the bridge routes delegation through CUSTOM MultiAgentHandoff + STEP_* with zero ACTIVITY events (measured red upstream in the 2026-08-31 runtime matrix). Multi-agent routes also crash the stale PyPI wheel (below). | - -The interrupt path never uses the LangGraph bridge's `CUSTOM on_interrupt` -convention; it is the outcome-provenance path added to the reducer and -resume builder in #888/#889/#891. - -## The bridge pin - -PyPI `ag-ui-strands` 0.3.0 is stale: it crashes on multi-agent routes -(`'function' object has no attribute 'model'`, agent.py:927) and predates -the interrupt/resume contract. `pyproject.toml` therefore pins the bridge -to a git ref of `ag-ui-protocol/ag-ui` (subdirectory -`integrations/aws-strands/python`) via `[tool.uv.sources]`, and the -exported requirements carry a `git+https://...#subdirectory=...` line. - -## Model client - -Strands' native OpenAI provider on plain `OPENAI_API_KEY` — no AWS -credentials involved. `OPENAI_BASE_URL` is honored, which is how the -aimock e2e harness intercepts model calls. `OTEL_SDK_DISABLED=true` is -setdefaulted in `src/agent.py` to silence Strands' collector-less OTEL -exporter noise. See `.env.example`. - -## Running locally - -```sh -npx tsx scripts/examples/serve-example.ts --capability=rt-strands -``` - -Angular dev server on :4331, uvicorn backend on :5331 (`/agent`, health at -`/ok`). diff --git a/cockpit/runtimes/aws-strands/python/src/agent.py b/cockpit/runtimes/aws-strands/python/src/agent.py index e6ab07b68..d83cc9188 100644 --- a/cockpit/runtimes/aws-strands/python/src/agent.py +++ b/cockpit/runtimes/aws-strands/python/src/agent.py @@ -59,6 +59,7 @@ "friday": ["10:30"], } +# region demo-state # Per-process demo state. The Strands bridge is SNAPSHOT-only: every # outbound state emission replaces the whole frontend state object, so each # ToolBehavior hook below composes and returns this COMPLETE object rather @@ -70,8 +71,10 @@ def _complete_state() -> dict: return {"availability": _state["availability"], "booking": _state["booking"]} +# endregion +# region availability-tool @tool def check_availability(day: str) -> dict: """Look up the open meeting slots for a weekday. @@ -98,8 +101,10 @@ async def availability_state(context) -> dict | None: return None _state["availability"] = {"day": result.get("day"), "slots": result.get("slots", [])} return _complete_state() +# endregion +# region book-meeting @tool(context=True) def book_meeting(topic: str, slot: str, tool_context: ToolContext) -> str: """Book a meeting after a human approves it. @@ -123,8 +128,10 @@ def book_meeting(topic: str, slot: str, tool_context: ToolContext) -> str: if not approved: return f"The human declined. Meeting NOT booked: {topic}" return f"Meeting booked for {slot}: {topic}" +# endregion +# region booking-state async def booking_state(context) -> dict | None: """state_from_args hook: mirror the pending booking into state as the tool-call arguments finish streaming (before the interrupt pauses the @@ -143,6 +150,7 @@ async def booking_state(context) -> dict | None: "status": "pending", } return _complete_state() +# endregion _RESEARCHER_INSTRUCTIONS = ( @@ -152,6 +160,7 @@ async def booking_state(context) -> dict | None: ) +# region delegation-tool @tool async def research_availability(attendees: str, date_range: str): """Delegate availability research for the given attendees to a specialist. @@ -178,6 +187,7 @@ async def research_availability(attendees: str, date_range: str): raise # Strands takes the LAST yielded value as the tool result. yield "".join(chunks) +# endregion _INSTRUCTIONS = """You are a meeting scheduling copilot. @@ -202,6 +212,7 @@ async def research_availability(attendees: str, date_range: str): """ +# region model def build_model() -> OpenAIModel: """Strands' native OpenAI provider — plain OPENAI_API_KEY, no AWS creds. @@ -214,8 +225,10 @@ def build_model() -> OpenAIModel: if base_url: client_args["base_url"] = base_url return OpenAIModel(client_args=client_args, model_id=os.environ.get("OPENAI_CHAT_MODEL", "gpt-4o-mini")) +# endregion +# region specialist # Tool-less specialist the orchestrator delegates availability research to # via the `research_availability` async-generator tool above. Its streamed # events cross the bridge as tool_stream_events and are translated into @@ -226,7 +239,9 @@ def build_model() -> OpenAIModel: name="availability_researcher", tools=[], ) +# endregion +# region agent-config agent = StrandsAgent( agent=Agent( model=build_model(), @@ -245,3 +260,4 @@ def build_model() -> OpenAIModel: }, ), ) +# endregion diff --git a/cockpit/runtimes/mastra/angular/docs/guide.md b/cockpit/runtimes/mastra/angular/docs/guide.md deleted file mode 100644 index 3c4a662ac..000000000 --- a/cockpit/runtimes/mastra/angular/docs/guide.md +++ /dev/null @@ -1,68 +0,0 @@ -# Runtimes — Mastra - -Third entry on the one-capability-many-runtimes axis -(`cockpit/runtimes//`): the same neutral `Agent` contract and the -same `@threadplane/chat` UI primitives as every other AG-UI example, over a -backend that is genuinely not LangGraph — and, uniquely on this axis, not -Python either. - -## What it demonstrates - -| Surface | How | -| --- | --- | -| Messages | Streamed assistant text (`TEXT_MESSAGE_CHUNK`) from a Mastra `Agent` via the `@ag-ui/mastra` bridge. | -| Tool calls | `check_conditions` executes server-side, no pause. | -| Shared state | Working memory bridged honestly: the agent's `packing_list` working-memory schema streams as a `STATE_SNAPSHOT` plus real JSON-Patch `STATE_DELTA` events while the agent updates the list (measured in the spike's 04a capture) — the only runtime on this axis that emits deltas. | -| Interrupts | `reserve_campsite` suspends the run via its `suspendSchema`/`resumeSchema` pair; the bridge emits a `CUSTOM on_interrupt` payload (`{ toolCallId, toolName, suspendPayload, runId }`) followed by the protocol-standard `RUN_FINISHED.outcome = { type: 'interrupt', interrupts: [...] }`. The adapter resumes with the Mastra wire shape `forwardedProps.command = { resume, interruptEvent: { toolCallId, runId } }`. | -| Subagents | Not demonstrated — upstream reserves `ACTIVITY_*` events for background tasks, so delegation has no per-subagent stream (measured red in the 2026-08-31 runtime matrix). | - -Unlike the Python-lane runtimes, the interrupt path here does use a `CUSTOM -on_interrupt` event — it is the one convention the Mastra bridge shares with -the LangGraph bridge — but the run still finishes with the outcome-provenance -`RUN_FINISHED` shape added in #888/#889/#891, so the reducer treats all three -runtimes identically. - -Suspend/resume REQUIRES persistent storage: Mastra writes suspended-run -snapshots to LibSQL file storage and resume loads them back, so an in-memory -store would orphan every pending approval across HTTP requests. - -## The hosting service (Node lane) - -Upstream `@ag-ui/mastra` ships no plain AG-UI HTTP endpoint — only the -in-process `MastraAgent` bridge and a mount for its own chat frontend -runtime. The backend is therefore the hand-written Node service -`deployments/ag-ui-mastra/`: -`server.mjs` subscribes to `MastraAgent.run(input)` (the raw AG-UI event -Observable) and encodes each event as one SSE `data:` frame — exactly what -`@ag-ui/client`'s `HttpAgent` consumes. It mirrors the Python lane's -behavior contract (`GET /ok` unauthenticated, `X-Internal-Token` on every -other route, topics at `POST /agent/`, Observable errors mapped to a -`RUN_ERROR` frame). The agent itself lives in `agents.mjs`, next to the -shim, because there is no per-example Python module to stage into a -generated deployment. This is why `cockpit/runtimes/mastra/` has no -`python/` directory and its assets live in the `angular` lane. - -## Model client - -Mastra's model router resolves the plain string `openai/gpt-4o-mini` on -`OPENAI_API_KEY` — no provider SDK wiring. `OPENAI_BASE_URL` is honored, -which is how the aimock e2e harness intercepts model calls without a code -fork. - -## Running locally - -```sh -npx tsx scripts/examples/serve-example.ts --capability=rt-mastra -``` - -Angular dev server on :4332. The serve script only auto-starts Python -backends, so start the Node service manually (see -`deployments/ag-ui-mastra/README.md`): - -```sh -cd deployments/ag-ui-mastra && npm ci -AG_UI_INTERNAL_TOKEN=dev-local-token OPENAI_API_KEY=sk-... PORT=5332 node server.mjs -``` - -The example's dev proxy rewrites `/agent` to -`http://localhost:5332/agent/mastra` and injects the dev token. diff --git a/cockpit/runtimes/mastra/angular/src/app/mastra.component.ts b/cockpit/runtimes/mastra/angular/src/app/mastra.component.ts index f2ea68a83..d43749f19 100644 --- a/cockpit/runtimes/mastra/angular/src/app/mastra.component.ts +++ b/cockpit/runtimes/mastra/angular/src/app/mastra.component.ts @@ -88,6 +88,7 @@ interface PackingList { + + + + `, @@ -235,6 +239,7 @@ export class MastraComponent { protected readonly agent = injectAgent(); + // #region packing-list-state /** Mastra working memory, bridged into AG-UI shared state. */ protected readonly packingList = computed(() => { // Example apps compile lib source with strict:false — cast at the read site. @@ -242,7 +247,9 @@ export class MastraComponent { const list = state?.packing_list; return list && list.title ? list : undefined; }); + // #endregion + // #region approval-actions /** * The pending Mastra suspend. The reducer stores the parsed CUSTOM * `on_interrupt` payload: `{ type: 'mastra_suspend', toolCallId, toolName, @@ -272,4 +279,5 @@ export class MastraComponent { void this.agent.submit({ resume: { approved: false } }); } } + // #endregion } diff --git a/cockpit/runtimes/microsoft-agent-framework/angular/src/app/microsoft-agent-framework.component.ts b/cockpit/runtimes/microsoft-agent-framework/angular/src/app/microsoft-agent-framework.component.ts index 68bcfd0e7..820d70cba 100644 --- a/cockpit/runtimes/microsoft-agent-framework/angular/src/app/microsoft-agent-framework.component.ts +++ b/cockpit/runtimes/microsoft-agent-framework/angular/src/app/microsoft-agent-framework.component.ts @@ -71,6 +71,7 @@ interface ExpenseDraft { + + { // Example apps compile lib source with strict:false — cast at the read site. @@ -220,7 +223,9 @@ export class MicrosoftAgentFrameworkComponent { const e = state?.expense; return e && e.vendor !== undefined ? e : undefined; }); + // #endregion + // #region approval-wiring /** * The pending approval request from the protocol-standard interrupt * outcome. The reducer stores it as `{ interrupts: [...], runId }`; each @@ -253,4 +258,5 @@ export class MicrosoftAgentFrameworkComponent { void this.agent.submit({ resume: { approved: false } }); } } + // #endregion } diff --git a/cockpit/runtimes/microsoft-agent-framework/python/docs/guide.md b/cockpit/runtimes/microsoft-agent-framework/python/docs/guide.md deleted file mode 100644 index fbab59bd8..000000000 --- a/cockpit/runtimes/microsoft-agent-framework/python/docs/guide.md +++ /dev/null @@ -1,38 +0,0 @@ -# Runtimes — Microsoft Agent Framework - -This example is the first entry on the one-capability-many-runtimes axis -(`cockpit/runtimes//`): the same neutral `Agent` contract and the -same `@threadplane/chat` UI primitives as every other AG-UI example, over a -backend that is genuinely not LangGraph. - -## What it demonstrates - -| Surface | How | -| --- | --- | -| Messages | Streamed assistant text from `Agent` (agent-framework-core). | -| Tool calls | `lookup_expense_policy` executes server-side, no pause. | -| Shared state | `predict_state_config` streams the `submit_expense` `expense` argument into frontend state (STATE_SNAPSHOT / STATE_DELTA) while the model is still generating it. | -| Interrupts | `submit_expense` has `approval_mode="always_require"`; the bridge finishes the run with the protocol-standard `RUN_FINISHED.outcome = { type: 'interrupt', interrupts: [...] }` and resumes from the client's top-level `resume` entries. | -| Subagents | Not demonstrated — the bridge emits no per-subagent ACTIVITY stream (measured red upstream in the 2026-08-31 runtime matrix). | - -The interrupt path never uses the LangGraph bridge's `CUSTOM on_interrupt` -convention; it is the outcome-provenance path added to the reducer and -resume builder in #888/#889/#891. - -## Model client - -Azure OpenAI is the default: set `AZURE_OPENAI_ENDPOINT`, -`AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_MODEL` (deployment name), and -optionally `AZURE_OPENAI_API_VERSION`. When `AZURE_OPENAI_ENDPOINT` is -absent, the agent falls back to the plain OpenAI client on -`OPENAI_API_KEY` (honoring `OPENAI_BASE_URL`, which is how the aimock e2e -harness intercepts model calls). See `.env.example`. - -## Running locally - -```sh -npx tsx scripts/examples/serve-example.ts --capability=rt-maf -``` - -Angular dev server on :4330, uvicorn backend on :5330 (`/agent`, health at -`/ok`). diff --git a/cockpit/runtimes/microsoft-agent-framework/python/src/agent.py b/cockpit/runtimes/microsoft-agent-framework/python/src/agent.py index 9e95a24c2..b24c060c0 100644 --- a/cockpit/runtimes/microsoft-agent-framework/python/src/agent.py +++ b/cockpit/runtimes/microsoft-agent-framework/python/src/agent.py @@ -55,6 +55,7 @@ class Expense(BaseModel): memo: str = Field(..., description="One-line justification, including attendee count for meals.") +# region policy-tool @tool( name="lookup_expense_policy", description="Look up the reimbursement policy for an expense category.", @@ -75,8 +76,10 @@ def lookup_expense_policy(category: str) -> str: f"Policy for {category}: limit ${policy['limit_usd']} per expense, " f"receipts required over ${policy['receipt_required_over_usd']}. {policy['notes']}" ) +# endregion +# region approval-tool @tool( name="submit_expense", description="Submit an expense report entry for reimbursement. Requires human approval.", @@ -99,6 +102,7 @@ def submit_expense(expense: Expense) -> str: f"Expense recorded: ${entry.amount_usd:.2f} to {entry.vendor} " f"({entry.category}) — queued for reimbursement." ) +# endregion _INSTRUCTIONS = """You are an expense approval copilot. @@ -124,6 +128,7 @@ def submit_expense(expense: Expense) -> str: """ +# region model-client def build_chat_client() -> OpenAIChatCompletionClient: """Azure OpenAI by default; plain OpenAI when Azure env is absent. @@ -146,8 +151,10 @@ def build_chat_client() -> OpenAIChatCompletionClient: model=os.environ.get("OPENAI_CHAT_MODEL", "gpt-4o-mini"), api_key=os.environ.get("OPENAI_API_KEY", "unset-openai-api-key"), ) +# endregion +# region delegation policy_researcher = Agent( name="policy_researcher", instructions=( @@ -197,8 +204,10 @@ async def research_policy(category: str, amount: float) -> str: raise subagent_emitter.delegation_finished(tid) return "".join(parts) +# endregion +# region bridge agent = AgentFrameworkAgent( agent=Agent( name="expense_approval_copilot", @@ -216,3 +225,4 @@ async def research_policy(category: str, amount: float) -> str: }, require_confirmation=False, ) +# endregion diff --git a/deployments/ag-ui-dev/deps/aws_strands/docs/guide.md b/deployments/ag-ui-dev/deps/aws_strands/docs/guide.md deleted file mode 100644 index 49aceed0b..000000000 --- a/deployments/ag-ui-dev/deps/aws_strands/docs/guide.md +++ /dev/null @@ -1,46 +0,0 @@ -# Runtimes — AWS Strands - -Second entry on the one-capability-many-runtimes axis -(`cockpit/runtimes//`): the same neutral `Agent` contract and the -same `@threadplane/chat` UI primitives as every other AG-UI example, over a -backend that is genuinely not LangGraph. - -## What it demonstrates - -| Surface | How | -| --- | --- | -| Messages | Streamed assistant text from a Strands `Agent`. | -| Tool calls | `check_availability` executes server-side, no pause. | -| Shared state | SNAPSHOT-only, honestly: the Strands bridge never emits STATE_DELTA, and outbound state exists only where a tool opts in via per-tool `ToolBehavior` hooks — `state_from_result` on `check_availability`, `state_from_args` on `book_meeting`. Because snapshots replace the whole state object, every hook returns the COMPLETE state (a partial return would clobber sibling keys). | -| Interrupts | `book_meeting` parks in `tool_context.interrupt(...)`; the bridge finishes the run with the protocol-standard `RUN_FINISHED.outcome = { type: 'interrupt', interrupts: [...] }` and resumes from the client's top-level `resume` entries keyed by `interruptId`. | -| Subagents | Not demonstrated — the bridge routes delegation through CUSTOM MultiAgentHandoff + STEP_* with zero ACTIVITY events (measured red upstream in the 2026-08-31 runtime matrix). Multi-agent routes also crash the stale PyPI wheel (below). | - -The interrupt path never uses the LangGraph bridge's `CUSTOM on_interrupt` -convention; it is the outcome-provenance path added to the reducer and -resume builder in #888/#889/#891. - -## The bridge pin - -PyPI `ag-ui-strands` 0.3.0 is stale: it crashes on multi-agent routes -(`'function' object has no attribute 'model'`, agent.py:927) and predates -the interrupt/resume contract. `pyproject.toml` therefore pins the bridge -to a git ref of `ag-ui-protocol/ag-ui` (subdirectory -`integrations/aws-strands/python`) via `[tool.uv.sources]`, and the -exported requirements carry a `git+https://...#subdirectory=...` line. - -## Model client - -Strands' native OpenAI provider on plain `OPENAI_API_KEY` — no AWS -credentials involved. `OPENAI_BASE_URL` is honored, which is how the -aimock e2e harness intercepts model calls. `OTEL_SDK_DISABLED=true` is -setdefaulted in `src/agent.py` to silence Strands' collector-less OTEL -exporter noise. See `.env.example`. - -## Running locally - -```sh -npx tsx scripts/examples/serve-example.ts --capability=rt-strands -``` - -Angular dev server on :4331, uvicorn backend on :5331 (`/agent`, health at -`/ok`). diff --git a/deployments/ag-ui-dev/deps/aws_strands/src/agent.py b/deployments/ag-ui-dev/deps/aws_strands/src/agent.py index e6ab07b68..d83cc9188 100644 --- a/deployments/ag-ui-dev/deps/aws_strands/src/agent.py +++ b/deployments/ag-ui-dev/deps/aws_strands/src/agent.py @@ -59,6 +59,7 @@ "friday": ["10:30"], } +# region demo-state # Per-process demo state. The Strands bridge is SNAPSHOT-only: every # outbound state emission replaces the whole frontend state object, so each # ToolBehavior hook below composes and returns this COMPLETE object rather @@ -70,8 +71,10 @@ def _complete_state() -> dict: return {"availability": _state["availability"], "booking": _state["booking"]} +# endregion +# region availability-tool @tool def check_availability(day: str) -> dict: """Look up the open meeting slots for a weekday. @@ -98,8 +101,10 @@ async def availability_state(context) -> dict | None: return None _state["availability"] = {"day": result.get("day"), "slots": result.get("slots", [])} return _complete_state() +# endregion +# region book-meeting @tool(context=True) def book_meeting(topic: str, slot: str, tool_context: ToolContext) -> str: """Book a meeting after a human approves it. @@ -123,8 +128,10 @@ def book_meeting(topic: str, slot: str, tool_context: ToolContext) -> str: if not approved: return f"The human declined. Meeting NOT booked: {topic}" return f"Meeting booked for {slot}: {topic}" +# endregion +# region booking-state async def booking_state(context) -> dict | None: """state_from_args hook: mirror the pending booking into state as the tool-call arguments finish streaming (before the interrupt pauses the @@ -143,6 +150,7 @@ async def booking_state(context) -> dict | None: "status": "pending", } return _complete_state() +# endregion _RESEARCHER_INSTRUCTIONS = ( @@ -152,6 +160,7 @@ async def booking_state(context) -> dict | None: ) +# region delegation-tool @tool async def research_availability(attendees: str, date_range: str): """Delegate availability research for the given attendees to a specialist. @@ -178,6 +187,7 @@ async def research_availability(attendees: str, date_range: str): raise # Strands takes the LAST yielded value as the tool result. yield "".join(chunks) +# endregion _INSTRUCTIONS = """You are a meeting scheduling copilot. @@ -202,6 +212,7 @@ async def research_availability(attendees: str, date_range: str): """ +# region model def build_model() -> OpenAIModel: """Strands' native OpenAI provider — plain OPENAI_API_KEY, no AWS creds. @@ -214,8 +225,10 @@ def build_model() -> OpenAIModel: if base_url: client_args["base_url"] = base_url return OpenAIModel(client_args=client_args, model_id=os.environ.get("OPENAI_CHAT_MODEL", "gpt-4o-mini")) +# endregion +# region specialist # Tool-less specialist the orchestrator delegates availability research to # via the `research_availability` async-generator tool above. Its streamed # events cross the bridge as tool_stream_events and are translated into @@ -226,7 +239,9 @@ def build_model() -> OpenAIModel: name="availability_researcher", tools=[], ) +# endregion +# region agent-config agent = StrandsAgent( agent=Agent( model=build_model(), @@ -245,3 +260,4 @@ def build_model() -> OpenAIModel: }, ), ) +# endregion diff --git a/deployments/ag-ui-dev/deps/microsoft_agent_framework/docs/guide.md b/deployments/ag-ui-dev/deps/microsoft_agent_framework/docs/guide.md deleted file mode 100644 index fbab59bd8..000000000 --- a/deployments/ag-ui-dev/deps/microsoft_agent_framework/docs/guide.md +++ /dev/null @@ -1,38 +0,0 @@ -# Runtimes — Microsoft Agent Framework - -This example is the first entry on the one-capability-many-runtimes axis -(`cockpit/runtimes//`): the same neutral `Agent` contract and the -same `@threadplane/chat` UI primitives as every other AG-UI example, over a -backend that is genuinely not LangGraph. - -## What it demonstrates - -| Surface | How | -| --- | --- | -| Messages | Streamed assistant text from `Agent` (agent-framework-core). | -| Tool calls | `lookup_expense_policy` executes server-side, no pause. | -| Shared state | `predict_state_config` streams the `submit_expense` `expense` argument into frontend state (STATE_SNAPSHOT / STATE_DELTA) while the model is still generating it. | -| Interrupts | `submit_expense` has `approval_mode="always_require"`; the bridge finishes the run with the protocol-standard `RUN_FINISHED.outcome = { type: 'interrupt', interrupts: [...] }` and resumes from the client's top-level `resume` entries. | -| Subagents | Not demonstrated — the bridge emits no per-subagent ACTIVITY stream (measured red upstream in the 2026-08-31 runtime matrix). | - -The interrupt path never uses the LangGraph bridge's `CUSTOM on_interrupt` -convention; it is the outcome-provenance path added to the reducer and -resume builder in #888/#889/#891. - -## Model client - -Azure OpenAI is the default: set `AZURE_OPENAI_ENDPOINT`, -`AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_MODEL` (deployment name), and -optionally `AZURE_OPENAI_API_VERSION`. When `AZURE_OPENAI_ENDPOINT` is -absent, the agent falls back to the plain OpenAI client on -`OPENAI_API_KEY` (honoring `OPENAI_BASE_URL`, which is how the aimock e2e -harness intercepts model calls). See `.env.example`. - -## Running locally - -```sh -npx tsx scripts/examples/serve-example.ts --capability=rt-maf -``` - -Angular dev server on :4330, uvicorn backend on :5330 (`/agent`, health at -`/ok`). diff --git a/deployments/ag-ui-dev/deps/microsoft_agent_framework/src/agent.py b/deployments/ag-ui-dev/deps/microsoft_agent_framework/src/agent.py index 9e95a24c2..b24c060c0 100644 --- a/deployments/ag-ui-dev/deps/microsoft_agent_framework/src/agent.py +++ b/deployments/ag-ui-dev/deps/microsoft_agent_framework/src/agent.py @@ -55,6 +55,7 @@ class Expense(BaseModel): memo: str = Field(..., description="One-line justification, including attendee count for meals.") +# region policy-tool @tool( name="lookup_expense_policy", description="Look up the reimbursement policy for an expense category.", @@ -75,8 +76,10 @@ def lookup_expense_policy(category: str) -> str: f"Policy for {category}: limit ${policy['limit_usd']} per expense, " f"receipts required over ${policy['receipt_required_over_usd']}. {policy['notes']}" ) +# endregion +# region approval-tool @tool( name="submit_expense", description="Submit an expense report entry for reimbursement. Requires human approval.", @@ -99,6 +102,7 @@ def submit_expense(expense: Expense) -> str: f"Expense recorded: ${entry.amount_usd:.2f} to {entry.vendor} " f"({entry.category}) — queued for reimbursement." ) +# endregion _INSTRUCTIONS = """You are an expense approval copilot. @@ -124,6 +128,7 @@ def submit_expense(expense: Expense) -> str: """ +# region model-client def build_chat_client() -> OpenAIChatCompletionClient: """Azure OpenAI by default; plain OpenAI when Azure env is absent. @@ -146,8 +151,10 @@ def build_chat_client() -> OpenAIChatCompletionClient: model=os.environ.get("OPENAI_CHAT_MODEL", "gpt-4o-mini"), api_key=os.environ.get("OPENAI_API_KEY", "unset-openai-api-key"), ) +# endregion +# region delegation policy_researcher = Agent( name="policy_researcher", instructions=( @@ -197,8 +204,10 @@ async def research_policy(category: str, amount: float) -> str: raise subagent_emitter.delegation_finished(tid) return "".join(parts) +# endregion +# region bridge agent = AgentFrameworkAgent( agent=Agent( name="expense_approval_copilot", @@ -216,3 +225,4 @@ async def research_policy(category: str, amount: float) -> str: }, require_confirmation=False, ) +# endregion diff --git a/deployments/ag-ui-mastra/agents.mjs b/deployments/ag-ui-mastra/agents.mjs index e48da1401..bf7b680cf 100644 --- a/deployments/ag-ui-mastra/agents.mjs +++ b/deployments/ag-ui-mastra/agents.mjs @@ -26,6 +26,7 @@ const MODEL = 'openai/gpt-4o-mini'; const NIGHTLY_RATE_USD = 45; +// #region check-conditions-tool /** Deterministic backend tool — no external calls, stable for fixtures. */ const checkConditionsTool = createTool({ id: 'check_conditions', @@ -44,7 +45,9 @@ const checkConditionsTool = createTool({ low_c: 4, }), }); +// #endregion +// #region reserve-campsite-tool /** * Human-in-the-loop tool. First call suspends the run (persisted to LibSQL — * suspend/resume REQUIRES persistent storage, a spike finding); the frontend @@ -83,6 +86,7 @@ const reserveCampsiteTool = createTool({ return `Reservation for ${inputData.site} was declined by the user. Nothing was booked.`; }, }); +// #endregion /** * Build the Mastra instance for this service. @@ -95,6 +99,7 @@ const reserveCampsiteTool = createTool({ export function createMastra(dbUrl) { const store = (id) => new LibSQLStore({ id, url: dbUrl }); + // #region weather-forecaster /** * Sub-agent (spike: wire-capture-subagents.md). Registered on the * supervisor via `agents:`; Mastra surfaces it as a backend tool named @@ -110,7 +115,9 @@ export function createMastra(dbUrl) { 'You are a weather forecaster. Given a campsite and dates, give a 3-bullet forecast summary. Be concise.', model: MODEL, }); + // #endregion + // #region trip-agent const tripAgent = new Agent({ id: 'mastra', name: 'mastra', @@ -143,6 +150,7 @@ Always answer in one short sentence.`, }, }), }); + // #endregion return new Mastra({ agents: { mastra: tripAgent }, diff --git a/deployments/ag-ui-mastra/server.mjs b/deployments/ag-ui-mastra/server.mjs index 191e32ec9..118d16284 100644 --- a/deployments/ag-ui-mastra/server.mjs +++ b/deployments/ag-ui-mastra/server.mjs @@ -52,15 +52,18 @@ function json(res, status, body) { res.end(JSON.stringify(body)); } +// #region sse-frame /** One SSE frame per AG-UI event — the wire shape @ag-ui/client parses. */ function sseFrame(event) { return `data: ${JSON.stringify(event)}\n\n`; } +// #endregion export function createAgUiServer() { return http.createServer(async (req, res) => { const path = (req.url ?? '').split('?')[0]; + // #region route-contract if (req.method === 'GET' && path === '/ok') { json(res, 200, { ok: true }); return; @@ -88,6 +91,7 @@ export function createAgUiServer() { json(res, 404, { detail: `no such topic: ${topic}` }); return; } + // #endregion let body = ''; for await (const chunk of req) body += chunk; @@ -105,6 +109,7 @@ export function createAgUiServer() { connection: 'keep-alive', }); + // #region run-and-stream // One injector per run with two inputs, both writing through the same // SSE frame writer: // - `chunk()`: raw Mastra fullStream chunks observed through the stream @@ -147,6 +152,7 @@ export function createAgUiServer() { }); req.on('close', () => sub.unsubscribe()); + // #endregion }); } diff --git a/docs/superpowers/plans/2026-09-06-docs-example-first-products.md b/docs/superpowers/plans/2026-09-06-docs-example-first-products.md index bd3ca0310..8199d5984 100644 --- a/docs/superpowers/plans/2026-09-06-docs-example-first-products.md +++ b/docs/superpowers/plans/2026-09-06-docs-example-first-products.md @@ -15,7 +15,7 @@ - Frontmatter `description:` under 180 characters, one sentence. - Every API named in a fence is verified against `libs/*/src` before it is written; every hand-written fence is valid in its language. - `server.py` files (ag-ui) are 13–19 lines: include whole when the page discusses the wire; otherwise mention by name. -- Region markers are comments only; inline Angular templates take ``; run `npx nx build ` after marking a template. `deployments/ag-ui-dev/deps//**` is TRACKED and generated from `cockpit/ag-ui/**`: after marking an ag-ui source run `npx tsx scripts/generate-ag-ui-deployment-config.ts` and stage ONLY `deployments/ag-ui-dev/deps//`. Other products produce no tracked deployments diff (confirm with `git status --short deployments`). +- Region markers are comments only; inline Angular templates take ``; run `npx nx build ` after marking a template. `deployments/ag-ui-dev/deps//**` is TRACKED and generated from `cockpit/ag-ui/**`: after marking an ag-ui source run `npx tsx scripts/generate-ag-ui-deployment-config.ts` and stage ONLY `deployments/ag-ui-dev/deps//`. The runtimes examples (aws-strands, microsoft-agent-framework) are ALSO mirrored into `deployments/ag-ui-dev/deps//`; render, deep-agents, langgraph and chat produce no tracked deployments diff (confirm with `git status --short deployments`). - Local prod build: `rm -rf apps/website/.next dist/apps/website/.next && GROWTH_FORM_POLICY=growth_v1 npx nx build website`; output in `dist/apps/website/.next`. - Unit runs: `npx nx test website --skip-nx-cache` from the root (the retirement spec is cwd-coupled).