Skip to content

Commit b38f44d

Browse files
committed
feat: add strands-browseruse-multiagent pattern with AgentCore Browser tool
- Add strands-browseruse-multiagent pattern with browser-use + Claude for autonomous browser control, plus Gateway MCP tools and Code Interpreter - Add shared browser tools core (tools/browser/) for AgentCore Browser session lifecycle management - Stream browser actions in real-time via on_step_start/on_step_end hooks with asyncio.Queue for immediate event delivery - Add IAM permissions for AgentCore Browser in CDK backend stack - Browser session persists across tool calls (cookies, login state, tabs)
1 parent d45cfdb commit b38f44d

11 files changed

Lines changed: 861 additions & 0 deletions

File tree

infra-cdk/lib/backend-stack.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,29 @@ export class BackendStack extends cdk.NestedStack {
309309
})
310310
)
311311

312+
// Add Browser Tool permissions (for AgentCore Browser + OS-level actions)
313+
agentRole.addToPolicy(
314+
new iam.PolicyStatement({
315+
sid: "BrowserToolAccess",
316+
effect: iam.Effect.ALLOW,
317+
actions: [
318+
"bedrock-agentcore:StartBrowserSession",
319+
"bedrock-agentcore:StopBrowserSession",
320+
"bedrock-agentcore:GetBrowserSession",
321+
"bedrock-agentcore:ListBrowserSessions",
322+
"bedrock-agentcore:UpdateBrowserStream",
323+
"bedrock-agentcore:ConnectBrowserAutomationStream",
324+
"bedrock-agentcore:ConnectBrowserLiveViewStream",
325+
"bedrock-agentcore:InvokeBrowser",
326+
],
327+
resources: [
328+
`arn:aws:bedrock-agentcore:${this.region}:aws:browser/*`,
329+
`arn:aws:bedrock-agentcore:${this.region}:${this.account}:browser/*`,
330+
`arn:aws:bedrock-agentcore:${this.region}:${this.account}:browser-custom/*`,
331+
],
332+
})
333+
)
334+
312335
// Add OAuth2 Credential Provider access for AgentCore Runtime
313336
// The @requires_access_token decorator performs a two-stage process:
314337
// 1. GetOauth2CredentialProvider - Looks up provider metadata (ARN, vendor config, grant types)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
5+
6+
WORKDIR /app
7+
8+
# Configure UV for container environment
9+
ENV UV_SYSTEM_PYTHON=1 \
10+
UV_COMPILE_BYTECODE=1 \
11+
DOCKER_CONTAINER=1 \
12+
OTEL_PYTHON_LOG_CORRELATION=true \
13+
PYTHONUNBUFFERED=1
14+
15+
# Copy pyproject.toml and shared packages for installation
16+
COPY pyproject.toml .
17+
COPY gateway/ gateway/
18+
COPY tools/ agentcore_tools/
19+
20+
# Copy and install agent-specific requirements first
21+
COPY patterns/strands-browseruse-multiagent/requirements.txt requirements.txt
22+
RUN uv pip install --no-cache -r requirements.txt && \
23+
uv pip install --no-cache aws-opentelemetry-distro==0.16.0
24+
25+
# Install FAST package with only core dependencies (no dev/agent optional deps)
26+
RUN uv pip install --no-cache -e . --no-deps && \
27+
uv pip install --no-cache requests>=2.31.0
28+
29+
# Create non-root user
30+
RUN useradd -m -u 1000 bedrock_agentcore
31+
USER bedrock_agentcore
32+
33+
EXPOSE 8080
34+
35+
# Copy agent code and tools
36+
COPY patterns/strands-browseruse-multiagent/agent.py .
37+
COPY patterns/strands-browseruse-multiagent/tools/ tools/
38+
COPY patterns/utils/ utils/
39+
40+
# Healthcheck
41+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
42+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/ping', timeout=2)" || exit 1
43+
44+
# Start agent with OpenTelemetry instrumentation
45+
CMD ["opentelemetry-instrument", "python", "-m", "agent"]
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Strands Single Agent Pattern
2+
3+
This pattern uses the [Strands Agents](https://github.com/strands-agents/strands-agents) framework to build a single agent with Gateway tool access, Code Interpreter, and AgentCore Memory for conversation history.
4+
5+
## Features
6+
7+
- **Token-Level Streaming**: True token-by-token streaming via `agent.stream_async()`
8+
- **AgentCore Memory**: Conversation history persisted across requests via `AgentCoreMemorySessionManager`, with optional long-term memory for cross-session fact recall
9+
- **Code Interpreter**: Secure Python execution via `StrandsCodeInterpreterTools`
10+
- **Gateway Integration**: Access Lambda-based tools through AgentCore Gateway (MCP protocol with OAuth2 auth)
11+
- **Secure Identity**: User identity extracted from validated JWT token (`RequestContext`), not from payload
12+
13+
## Architecture
14+
15+
```
16+
User Request
17+
|
18+
BedrockAgentCoreApp (basic_agent.py)
19+
|
20+
Strands Agent (Sonnet model via BedrockModel)
21+
|
22+
+-- AgentCore Memory (conversation history)
23+
| AgentCoreMemorySessionManager
24+
|
25+
+-- Code Interpreter
26+
| StrandsCodeInterpreterTools (execute_python_securely)
27+
|
28+
+-- Gateway MCP Client (streamable HTTP)
29+
Lambda-based tools via AgentCore Gateway
30+
```
31+
32+
## File Structure
33+
34+
```
35+
patterns/strands-single-agent/
36+
├── basic_agent.py # Main entrypoint (BedrockAgentCoreApp)
37+
├── strands_code_interpreter.py # Strands @tool wrapper for Code Interpreter
38+
├── tools/
39+
│ └── strands_execute_python.py # Strands-specific tool implementation
40+
├── requirements.txt # Pinned dependencies
41+
└── Dockerfile # Container build (Python 3.13)
42+
```
43+
44+
## Available Tools
45+
46+
| Tool | Source | Description |
47+
|------|--------|-------------|
48+
| `execute_python_securely` | Code Interpreter | Execute Python code in a secure sandbox |
49+
| Gateway tools | AgentCore Gateway | Lambda-based tools discovered via MCP |
50+
51+
## Model
52+
53+
- **Agent**: `us.anthropic.claude-sonnet-4-5-20250929-v1:0` (Sonnet via Bedrock)
54+
55+
## Streaming Events
56+
57+
The agent yields SSE `data: {json}` lines via `agent.stream_async()`. The frontend parser at `frontend/src/lib/agentcore-client/parsers/strands.ts` handles these event types:
58+
59+
| Event | Format | Description |
60+
|-------|--------|-------------|
61+
| Text | `{"data": "text"}` | Token-level text content |
62+
| Tool use start | `{"current_tool_use": {...}, "delta": {"toolUse": {"input": ""}}}` | Tool invocation begins |
63+
| Tool use delta | `{"current_tool_use": {...}, "delta": {"toolUse": {"input": "..."}}}` | Streaming tool input |
64+
| Tool result | `{"message": {"role": "user", "content": [{"toolResult": {...}}]}}` | Tool execution result |
65+
| Result | `{"result": {"stop_reason": "end_turn"}}` | Agent finished |
66+
| Lifecycle | `{"init_event_loop": true}` / `{"start_event_loop": true}` | Agent lifecycle events |
67+
68+
## Memory Integration
69+
70+
This pattern uses **AgentCore Memory** for conversation persistence and optional long-term recall:
71+
72+
**Short-term memory** (always active):
73+
1. `MEMORY_ID` environment variable provides the memory resource ID
74+
2. `AgentCoreMemoryConfig` is initialized with `memory_id`, `session_id`, and `actor_id` (user ID)
75+
3. `AgentCoreMemorySessionManager` handles storing/retrieving conversation history
76+
4. Memory is tied to the `runtimeSessionId` from the client
77+
78+
**Long-term memory** (opt-in via `use_long_term_memory: true` in `config.yaml`):
79+
1. The CDK stack passes `USE_LONG_TERM_MEMORY=true` to the agent runtime
80+
2. The agent configures a `RetrievalConfig` for the `/facts/{actorId}` namespace
81+
3. AgentCore extracts facts from conversations asynchronously and stores them per user (keyed by Cognito `userId`)
82+
4. On each turn, relevant facts are retrieved and injected into the agent context, enabling cross-session personalization
83+
5. Additional costs apply: $0.75/1,000 records stored + $0.50/1,000 retrieval calls
84+
85+
See [Memory Integration Guide](../../docs/MEMORY_INTEGRATION.md) for full details.
86+
87+
## Security
88+
89+
- **User identity**: Extracted from the validated JWT token via `RequestContext`, not from the payload body
90+
- **STACK_NAME validation**: Validated for alphanumeric format before use in SSM parameter paths
91+
- **Payload validation**: Required fields (`prompt`, `runtimeSessionId`) validated before processing
92+
- **Gateway auth**: OAuth2 client credentials flow via Cognito for machine-to-machine authentication
93+
94+
## Deployment
95+
96+
```bash
97+
cd infra-cdk
98+
# Set pattern in config.yaml:
99+
# backend:
100+
# pattern: strands-single-agent
101+
# deployment_type: docker # or zip
102+
cdk deploy
103+
```
104+
105+
Both Docker and ZIP deployment types are supported.
106+
107+
## Dependencies
108+
109+
```
110+
strands-agents==1.24.0
111+
mcp==1.26.0
112+
bedrock-agentcore[strands-agents]==1.2.0
113+
PyJWT[crypto]>=2.10.1
114+
```
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Strands agent with Gateway MCP tools, Memory, Code Interpreter, and Browser."""
2+
3+
import json
4+
import logging
5+
import os
6+
7+
from bedrock_agentcore.memory.integrations.strands.config import (
8+
AgentCoreMemoryConfig,
9+
RetrievalConfig,
10+
)
11+
from bedrock_agentcore.memory.integrations.strands.session_manager import (
12+
AgentCoreMemorySessionManager,
13+
)
14+
from bedrock_agentcore.runtime import BedrockAgentCoreApp, RequestContext
15+
from strands import Agent
16+
from strands.models import BedrockModel
17+
from tools.gateway import create_gateway_mcp_client
18+
from utils.auth import extract_user_id_from_context
19+
20+
from tools.browser import StrandsBrowserTools
21+
from tools.code_interpreter import StrandsCodeInterpreterTools
22+
23+
logger = logging.getLogger(__name__)
24+
25+
app = BedrockAgentCoreApp()
26+
27+
SYSTEM_PROMPT = (
28+
"You are a helpful assistant with access to tools via the Gateway, "
29+
"Code Interpreter, and Browser. "
30+
"When asked about your tools, list them and explain what they do."
31+
)
32+
33+
34+
def _create_session_manager(
35+
user_id: str, session_id: str
36+
) -> AgentCoreMemorySessionManager:
37+
"""Create an AgentCore memory session manager, optionally with long-term semantic retrieval.
38+
39+
When the USE_LONG_TERM_MEMORY environment variable is "true", configures retrieval
40+
from the /facts/{actorId} namespace so the agent recalls facts across sessions.
41+
When false (default), only short-term memory (conversation history) is active,
42+
avoiding the additional storage and retrieval costs of long-term memory.
43+
44+
Args:
45+
user_id: Unique identifier for the user (actor), extracted from the JWT sub claim.
46+
session_id: Unique identifier for the current conversation session.
47+
48+
Returns:
49+
An AgentCoreMemorySessionManager bound to the user and session.
50+
"""
51+
memory_id = os.environ.get("MEMORY_ID")
52+
if not memory_id:
53+
raise ValueError("MEMORY_ID environment variable is required")
54+
55+
use_ltm = os.environ.get("USE_LONG_TERM_MEMORY", "false").lower() == "true"
56+
57+
top_k = int(os.environ.get("LTM_TOP_K", "10"))
58+
relevance_score = float(os.environ.get("LTM_RELEVANCE_SCORE", "0.3"))
59+
60+
# Only pass retrieval_config when LTM is explicitly enabled.
61+
# Omitting it means the session manager uses short-term memory only,
62+
# which avoids the $0.50/1,000 retrieval and $0.75/1,000 storage costs.
63+
retrieval_config = (
64+
{
65+
"/facts/{actorId}": RetrievalConfig(
66+
top_k=top_k,
67+
relevance_score=relevance_score,
68+
)
69+
}
70+
if use_ltm
71+
else None
72+
)
73+
74+
config = AgentCoreMemoryConfig(
75+
memory_id=memory_id,
76+
session_id=session_id,
77+
actor_id=user_id,
78+
retrieval_config=retrieval_config,
79+
)
80+
return AgentCoreMemorySessionManager(
81+
agentcore_memory_config=config,
82+
region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
83+
)
84+
85+
86+
def create_strands_agent(user_id: str, session_id: str) -> Agent:
87+
"""Create a Strands agent with Gateway tools, memory, Code Interpreter, and Browser."""
88+
89+
bedrock_model = BedrockModel(
90+
model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0", temperature=0.1
91+
)
92+
93+
session_manager = _create_session_manager(user_id, session_id)
94+
95+
region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
96+
code_tools = StrandsCodeInterpreterTools(region)
97+
browser_tools = StrandsBrowserTools(region)
98+
99+
gateway_client = create_gateway_mcp_client()
100+
101+
return Agent(
102+
name="strands_agent",
103+
system_prompt=SYSTEM_PROMPT,
104+
tools=[
105+
gateway_client,
106+
code_tools.execute_python_securely,
107+
browser_tools.browser,
108+
],
109+
model=bedrock_model,
110+
session_manager=session_manager,
111+
trace_attributes={"user.id": user_id, "session.id": session_id},
112+
)
113+
114+
115+
@app.entrypoint
116+
async def invocations(payload, context: RequestContext):
117+
"""Main entrypoint — called by AgentCore Runtime on each request.
118+
119+
Extracts user ID from the validated JWT token (not the payload body)
120+
to prevent impersonation via prompt injection.
121+
"""
122+
user_query = payload.get("prompt")
123+
session_id = payload.get("runtimeSessionId")
124+
125+
if not all([user_query, session_id]):
126+
yield {
127+
"status": "error",
128+
"error": "Missing required fields: prompt or runtimeSessionId",
129+
}
130+
return
131+
132+
try:
133+
user_id = extract_user_id_from_context(context)
134+
agent = create_strands_agent(user_id, session_id)
135+
136+
async for event in agent.stream_async(user_query):
137+
yield json.loads(json.dumps(dict(event), default=str))
138+
139+
except Exception as e:
140+
logger.exception("Agent run failed")
141+
yield {"status": "error", "error": str(e)}
142+
143+
144+
if __name__ == "__main__":
145+
app.run()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Strands agent with browser-use for browser automation
2+
strands-agents==1.35.0
3+
strands-agents-tools==0.4.1
4+
bedrock-agentcore==1.4.7
5+
mcp==1.26.0
6+
PyJWT[crypto]==2.12.1
7+
# browser-use + Claude for browser automation
8+
browser-use>=0.12.0
9+
nest-asyncio>=1.5.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)