Skip to content

Commit 07c3c6a

Browse files
author
Rana Umar Majeed
committed
aws-strands integration
1 parent 498925d commit 07c3c6a

29 files changed

+5078
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
share/python-wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
MANIFEST
28+
29+
# PyInstaller
30+
*.manifest
31+
*.spec
32+
33+
# Installer logs
34+
pip-log.txt
35+
pip-delete-this-directory.txt
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.nox/
41+
.coverage
42+
.coverage.*
43+
.cache
44+
nosetests.xml
45+
coverage.xml
46+
*.cover
47+
*.py,cover
48+
.hypothesis/
49+
.pytest_cache/
50+
cover/
51+
52+
# pyenv
53+
# For a library or package, you might want to ignore these files since the code is
54+
# intended to run in multiple environments; otherwise, check them in:
55+
# .python-version
56+
57+
# pipenv
58+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
59+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
60+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
61+
# install all needed dependencies.
62+
# Pipfile.lock
63+
64+
# UV
65+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
66+
# This is especially recommended for binary packages to ensure reproducibility, and is more
67+
# commonly ignored for libraries.
68+
# uv.lock
69+
70+
# poetry
71+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
72+
# This is especially recommended for binary packages to ensure reproducibility, and is more
73+
# commonly ignored for libraries.
74+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
75+
# poetry.lock
76+
# poetry.toml
77+
78+
# pdm
79+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
80+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
81+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
82+
# pdm.lock
83+
# pdm.toml
84+
.pdm-python
85+
.pdm-build/
86+
87+
# pixi
88+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
89+
# pixi.lock
90+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
91+
# in the .venv directory. It is recommended not to include this directory in version control.
92+
.pixi
93+
94+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
95+
__pypackages__/
96+
97+
# Environments
98+
.env
99+
.envrc
100+
.venv
101+
env/
102+
venv/
103+
ENV/
104+
env.bak/
105+
venv.bak/
106+
107+
# Spyder project settings
108+
.spyderproject
109+
.spyproject
110+
111+
# Rope project settings
112+
.ropeproject
113+
114+
# mypy
115+
.mypy_cache/
116+
.dmypy.json
117+
dmypy.json
118+
119+
# Ruff
120+
.ruff_cache/
121+
122+
# PyPI configuration file
123+
.pypirc
124+
125+
# IDE
126+
.vscode/
127+
.idea/
128+
*.swp
129+
*.swo
130+
*~
131+
132+
# OS
133+
.DS_Store
134+
Thumbs.db
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# AWS Strands Integration for AG-UI
2+
3+
This package exposes a lightweight wrapper that lets any `strands.Agent` speak the AG-UI protocol. It mirrors the developer experience of the other integrations: give us a Strands agent instance, plug it into `StrandsAgent`, and wire it to FastAPI via `create_strands_app` (or `add_strands_fastapi_endpoint`).
4+
5+
## Prerequisites
6+
7+
- Python 3.10+
8+
- `poetry` (recommended) or `pip`
9+
- A Strands-compatible model key (e.g., `GOOGLE_API_KEY` for Gemini)
10+
11+
## Quick Start
12+
13+
14+
The `examples/server/__main__.py` module mounts all demo routes behind a single FastAPI app. Run:
15+
16+
```bash
17+
cd integrations/aws-strands/python/examples
18+
poetry install
19+
poetry run python -m server
20+
```
21+
22+
It exposes:
23+
24+
| Route | Description |
25+
| --- | --- |
26+
| `/agentic-chat` | Frontend tool demo |
27+
| `/backend-tool-rendering` | Backend tool rendering demo |
28+
| `/shared-state` | Shared recipe state |
29+
| `/agentic-generative-ui` | Agentic UI with PredictState |
30+
31+
This is the easiest way to test multiple flows locally. Each route still follows the pattern described below (Strands agent → wrapper → FastAPI).
32+
33+
## Architecture Overview
34+
35+
The integration has three main layers:
36+
37+
- **StrandsAgent** – wraps `strands.Agent.stream_async`. It translates Strands events into AG-UI events (text chunks, tool calls, PredictState, snapshots, etc.).
38+
- **Configuration**`StrandsAgentConfig` + `ToolBehavior` + `PredictStateMapping` let you describe tool-specific quirks declaratively (skip message snapshots, emit state, stream args, send confirm actions, etc.).
39+
- **Transport helpers**`create_strands_app` and `add_strands_fastapi_endpoint` expose the agent via SSE. They are thin shells over the shared `ag_ui.encoder.EventEncoder`.
40+
41+
See [ARCHITECTURE.md](ARCHITECTURE.md) for diagrams and a deeper dive.
42+
43+
44+
## Key Files
45+
46+
| File | Description |
47+
| --- | --- |
48+
| `src/ag_ui_strands/agent.py` | Core wrapper translating Strands streams into AG-UI events |
49+
| `src/ag_ui_strands/config.py` | Config primitives (`StrandsAgentConfig`, `ToolBehavior`, `PredictStateMapping`) |
50+
| `src/ag_ui_strands/endpoint.py` | FastAPI endpoint helper |
51+
| `examples/server/api/*.py` | Ready-to-run demo apps |
52+
53+
## Next Steps
54+
55+
- Wire Strands’ callback handler into the wrapper to expose multi-agent metadata.
56+
- Add an event queue layer (like the ADK middleware) for resumable streams and non-HTTP transports.
57+
- Expand the test suite as new behaviors land.
58+

0 commit comments

Comments
 (0)