|
| 1 | +# AWS Strands Integration Architecture |
| 2 | + |
| 3 | +This document explains how the AWS Strands integration inside `integrations/aws-strands/` is implemented today. It covers the Python adapter that speaks the AG-UI protocol and the FastAPI transport helpers. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## System Overview |
| 8 | + |
| 9 | +``` |
| 10 | +┌─────────────┐ RunAgentInput ┌──────────────────────────┐ |
| 11 | +│ AG-UI UI │ ────────────────► │ AG-UI HttpAgent (standard) │ |
| 12 | +└─────────────┘ (messages, │ e.g., @ag-ui/client │ |
| 13 | + tools, state) └──────────────────────────┬──────┘ |
| 14 | + │ HTTP(S) POST + SSE |
| 15 | + ▼ |
| 16 | + ┌────────────────────────────┐ |
| 17 | + │ FastAPI endpoint (Python) │ |
| 18 | + │ add_strands_fastapi_endpoint│ |
| 19 | + └─────────────┬──────────────┘ |
| 20 | + │ |
| 21 | + ▼ |
| 22 | + ┌─────────────────────────┐ |
| 23 | + │ StrandsAgent adapter │ |
| 24 | + │ (src/ag_ui_strands/...) │ |
| 25 | + └─────────────┬───────────┘ |
| 26 | + │ |
| 27 | + ▼ |
| 28 | + strands.Agent.stream_async() |
| 29 | +``` |
| 30 | + |
| 31 | +1. The browser (or any AG-UI client) instantiates the standard AG-UI `HttpAgent` (or equivalent) and targets the Strands endpoint URL; there is no Strands-specific SDK on the client. |
| 32 | +2. The client sends a `RunAgentInput` payload that contains the current thread state, previously executed tools, shared UI state, and the latest user message(s). |
| 33 | +3. `add_strands_fastapi_endpoint` (or `create_strands_app`) registers a POST route that deserializes `RunAgentInput`, instantiates an `EventEncoder`, and streams whatever the Python `StrandsAgent` yields. |
| 34 | +4. `StrandsAgent.run` wraps a concrete `strands.Agent` instance, forwards the derived user prompt into `stream_async`, and translates every event into AG-UI protocol events (text deltas, tool invocations, snapshots, etc.). |
| 35 | +5. The encoded stream is delivered back to the client over `text/event-stream` (or JSON chunked mode) and rendered by AG-UI without any Strands-specific code on the frontend. |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## Python Adapter Components |
| 40 | + |
| 41 | +### `StrandsAgent` (`src/ag_ui_strands/agent.py`) |
| 42 | + |
| 43 | +`StrandsAgent` is the heart of the integration. It encapsulates a Strands SDK agent and implements the AG-UI event contract: |
| 44 | + |
| 45 | +- **Lifecycle framing** |
| 46 | + - Emits `RunStartedEvent` before touching Strands. |
| 47 | + - Always emits `RunFinishedEvent` unless an exception occurs, in which case it emits `RunErrorEvent` with `code="STRANDS_ERROR"`. |
| 48 | +- **State priming** |
| 49 | + - If `RunAgentInput.state` is provided, it immediately publishes a `StateSnapshotEvent`, filtering out any `messages` field so the frontend remains the source of truth for the timeline. |
| 50 | + - Optionally rewrites the outgoing user prompt via `StrandsAgentConfig.state_context_builder`. |
| 51 | +- **User message derivation** |
| 52 | + - The adapter inspects `input_data.messages` from newest-to-oldest, picks the most recent `"user"` message, and defaults to `"Hello"` if none exist. |
| 53 | +- **Streaming text** |
| 54 | + - When Strands yields events with a `"data"` field, the adapter opens a new `TextMessageStartEvent` (once per turn), forwards every chunk as `TextMessageContentEvent`, and closes with `TextMessageEndEvent` when the Strands stream completes or is halted. |
| 55 | + - `stop_text_streaming` is toggled when certain tool behaviors demand ending narration as soon as a backend tool result arrives. |
| 56 | +- **Tool call fan-out** |
| 57 | + - Strands emits tool usage metadata via `event["current_tool_use"]`. The adapter: |
| 58 | + - Records `tool_use_id`, arguments, and normalized JSON for replay. |
| 59 | + - Emits optional `StateSnapshotEvent` via `ToolBehavior.state_from_args`. |
| 60 | + - Translates declarative `PredictStateMapping` entries into a `CustomEvent(name="PredictState")`. |
| 61 | + - Streams arguments through an optional async generator (`args_streamer`) so large payloads can be revealed progressively. |
| 62 | + - Emits `ToolCallStartEvent`, zero or more `ToolCallArgsEvent`, and `ToolCallEndEvent`. |
| 63 | + - Automatically halts streaming when the call corresponds to a frontend-only tool (identified by matching `RunAgentInput.tools`) unless the configured behavior flips `continue_after_frontend_call`. |
| 64 | +- **Tool result handling** |
| 65 | + - Strands encodes tool results inside `"message"` events whose role is `"user"` and whose contents include `toolResult`. The adapter: |
| 66 | + - Parses the blob into Python objects, tolerating single quotes or malformed JSON. |
| 67 | + - Reconstructs a short-lived pair of `AssistantMessage` (carrying the `tool_calls` array) and `ToolMessage`, then publishes a `MessagesSnapshotEvent` so the AG-UI timeline includes the function call and result (unless a pending backend tool result already exists or `skip_messages_snapshot` is set). |
| 68 | + - Executes `ToolBehavior.state_from_result` to hydrate shared state and `custom_result_handler` to emit additional AG-UI events (e.g., simulated progress via `StateDeltaEvent` in the generative UI example). |
| 69 | + - Honors `stop_streaming_after_result` by closing any active text message and halting the Strands stream early. |
| 70 | +- **Frontend tool awareness** |
| 71 | + - `input_data.tools` supplies the frontend tool registry. Their names are used to (a) avoid double-invoking tool results that were literally produced by the UI, and (b) stop the Strands run after the LLM has issued a UI-only instruction. |
| 72 | + |
| 73 | +### Configuration Layer (`src/ag_ui_strands/config.py`) |
| 74 | + |
| 75 | +`StrandsAgentConfig` allows each tool to define bespoke behavior without editing the adapter: |
| 76 | + |
| 77 | +| Primitive | Purpose | |
| 78 | +| --- | --- | |
| 79 | +| `tool_behaviors: Dict[str, ToolBehavior]` | Per-tool overrides keyed by the Strands tool name. | |
| 80 | +| `state_context_builder` | Callable that enriches the outgoing prompt with the current shared state (useful for reiterating plan steps, recipes, etc.). | |
| 81 | + |
| 82 | +`ToolBehavior` captures how the adapter should react: |
| 83 | + |
| 84 | +- `skip_messages_snapshot`: Prevents helper messages from being appended when the UI is already in sync. |
| 85 | +- `continue_after_frontend_call`: Keeps the stream alive after emitting a frontend tool call. |
| 86 | +- `stop_streaming_after_result`: Cuts off text streaming when the backend produced a decisive result. |
| 87 | +- `predict_state`: Iterable of `PredictStateMapping` objects that inform the UI how to project tool arguments into shared state before results arrive. |
| 88 | +- `args_streamer`: Async generator that controls how tool arguments are leaked into the transcript (e.g., chunk large JSON payloads). |
| 89 | +- `state_from_args` / `state_from_result`: Hooks that build `StateSnapshotEvent`s from tool inputs or outputs, enabling instant UI updates. |
| 90 | +- `custom_result_handler`: Async iterator that can emit arbitrary AG-UI events (state deltas, confirmation messages, etc.). |
| 91 | + |
| 92 | +Helper utilities: |
| 93 | + |
| 94 | +- `ToolCallContext` / `ToolResultContext` expose the `RunAgentInput`, tool identifiers, arguments, and parsed results to hook functions. |
| 95 | +- `maybe_await` awaits either coroutines or plain values, simplifying user-defined hooks. |
| 96 | +- `normalize_predict_state` ensures the adapter can iterate predictably over mappings. |
| 97 | + |
| 98 | +### Transport Helpers (`src/ag_ui_strands/endpoint.py` & `utils.py`) |
| 99 | + |
| 100 | +The transport layer is intentionally lightweight: |
| 101 | + |
| 102 | +- `add_strands_fastapi_endpoint(app, agent, path)` registers a POST route that: |
| 103 | + - Accepts a `RunAgentInput` body. |
| 104 | + - Instantiates `EventEncoder` using the requester’s `Accept` header to choose between SSE (`text/event-stream`) and newline-delimited JSON. |
| 105 | + - Streams whatever `StrandsAgent.run` yields, automatically encoding every AG-UI event. |
| 106 | + - Sends a `RunErrorEvent` with `code="ENCODING_ERROR"` if serialization fails mid-stream. |
| 107 | +- `create_strands_app(agent, path="/")` bootstraps a FastAPI application, adds permissive CORS middleware (allowing any origin/method/header so AG-UI localhost builds can connect), and mounts the agent route. |
| 108 | + |
| 109 | +### Packaging Surface (`src/ag_ui_strands/__init__.py`) |
| 110 | + |
| 111 | +The package exposes only what downstream callers need: |
| 112 | + |
| 113 | +``` |
| 114 | +StrandsAgent |
| 115 | +create_strands_app / add_strands_fastapi_endpoint |
| 116 | +StrandsAgentConfig / ToolBehavior / ToolCallContext / ToolResultContext / PredictStateMapping |
| 117 | +``` |
| 118 | + |
| 119 | +This mirrors other AG-UI integrations (Agno, LangGraph, etc.), so documentation and examples can follow the same mental model. |
| 120 | + |
| 121 | +--- |
| 122 | + |
| 123 | +## Example Entry Points (`python/examples/server/api/*.py`) |
| 124 | + |
| 125 | +The repository includes four runnable FastAPI apps that showcase different features. Each example builds a Strands SDK agent, wraps it with `StrandsAgent`, and exposes it via `create_strands_app`: |
| 126 | + |
| 127 | +| Module | Focus | Relevant Configuration | |
| 128 | +| --- | --- | --- | |
| 129 | +| `agentic_chat.py` | Baseline text generation with a frontend-only `change_background` tool. | No custom config; demonstrates automatic text streaming and frontend tool short-circuiting. | |
| 130 | +| `backend_tool_rendering.py` | Backend-executed tools (`render_chart`, `get_weather`). | Shows how tool results become `MessagesSnapshotEvent`s and can be rendered directly in the UI. | |
| 131 | +| `shared_state.py` | Collaborative recipe editor that streams server-side state. | Uses `state_context_builder`, `state_from_args`, and `state_from_result` to keep the UI’s recipe object synchronized. | |
| 132 | +| `agentic_generative_ui.py` | Predictive and reactive state updates for generative UI surfaces. | Demonstrates `PredictStateMapping`, `custom_result_handler` emitting `StateDeltaEvent`s, and the `stop_streaming_after_result` flag. | |
| 133 | + |
| 134 | +These examples double as integration tests: they exercise every built-in hook so regressions surface quickly during manual QA. |
| 135 | + |
| 136 | +--- |
| 137 | + |
| 138 | +## Event Semantics Recap |
| 139 | + |
| 140 | +| Strands Signal | Adapter Reaction | AG-UI Consumer Impact | |
| 141 | +| --- | --- | --- | |
| 142 | +| `stream_async` yields `{"data": ...}` | Emit text start/content/end | Updates conversational transcript incrementally. | |
| 143 | +| `current_tool_use` announced | Emit tool call events, optional PredictState/state snapshots | Shows tool invocation cards and, when configured, optimistic UI updates. | |
| 144 | +| `toolResult` packaged within `message.content[].toolResult` | Publish timeline snapshot, tool result hooks, optional halt | Renders backend tool outputs and state changes without additional frontend logic. | |
| 145 | +| Stream sends `complete` or adapter decides to halt | Close text envelope (if needed) and emit `RunFinishedEvent` | Signals the UI that the run ended; frontends may start follow-up runs or show idle states. | |
| 146 | +| Exceptions anywhere in the stack | Emit `RunErrorEvent` with the exception message | Frontend surfaces the failure and can offer retries. | |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +## Deployment & Runtime Characteristics |
| 151 | + |
| 152 | +- **HTTP/SSE transport**: The adapter currently supports only HTTP POST requests plus streaming responses. Longer-lived transports (WebSockets, queues) are not part of the implemented surface. |
| 153 | +- **Stateless server layer**: Every request is independent. All persistent context flows through `RunAgentInput.state` and `messages`, which the AG-UI runtime maintains. |
| 154 | +- **Model compatibility**: The examples use `strands.models.gemini.GeminiModel`, but `StrandsAgent` works with any `strands.Agent` configured with compatible tools and prompts because it only relies on `stream_async`. |
| 155 | +- **Error isolation**: Failures inside tool hooks (`state_from_args`, etc.) are swallowed so the main run can continue. Only uncaught exceptions in the core loop trigger `RunErrorEvent`. |
| 156 | + |
| 157 | +--- |
| 158 | + |
| 159 | +## Summary |
| 160 | + |
| 161 | +The AWS Strands integration adapts the Strands SDK to the AG-UI protocol by: |
| 162 | + |
| 163 | +1. Wrapping `strands.Agent.stream_async` with `StrandsAgent`, which understands AG-UI events, tool semantics, and shared-state conventions. |
| 164 | +2. Exposing a trivial FastAPI transport layer that handles encoding and CORS while remaining stateless. |
| 165 | +3. Letting any existing AG-UI HTTP client connect directly to the endpoint—no Strands-specific frontend package is required. |
| 166 | + |
| 167 | +All current behavior lives in `integrations/aws-strands/python/src/ag_ui_strands`. There are no hidden services or background workers; what is described above is the complete, production-ready implementation that powers today’s Strands integration. |
| 168 | + |
| 169 | + |
0 commit comments