Skip to content

Commit cae7e6b

Browse files
committed
Introduce AgentCore strands-agent sample
This sample includes a strands agent loop aimed to run on AWS AgentCore, invoked by Temporal serverless workers. This utilizes the AWS AgentCore CLI and related configuration for serverless workers.
1 parent 63672ab commit cae7e6b

16 files changed

Lines changed: 3919 additions & 3 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Some examples require extra dependencies. See each sample's directory for specif
6262
* [activity_worker](activity_worker) - Use Python activities from a workflow in another language.
6363
* [batch_sliding_window](batch_sliding_window) - Batch processing with a sliding window of child workflows.
6464
* [bedrock](bedrock) - Orchestrate a chatbot with Amazon Bedrock.
65+
* [bedrock_agentcore/strands-agent](bedrock_agentcore/strands-agent) - Run a AWS Strands Agent with Temporal Plugin on AgentCore Worker.
6566
* [cloud_export_to_parquet](cloud_export_to_parquet) - Set up schedule workflow to process exported files on an hourly basis
6667
* [context_propagation](context_propagation) - Context propagation through workflows/activities via interceptor.
6768
* [custom_converter](custom_converter) - Use a custom payload converter to handle custom types.

bedrock_agentcore/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Generated by `agentcore create` / `agentcore deploy` (see bin/create-runtime.sh)
2+
# in each sample directory.
3+
**/agentcore/cdk/
4+
**/agentcore/.cli/
5+
**/agentcore/.cache/
6+
**/agentcore/.env.local
7+
**/agentcore/.llm-context/
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Strands Agent on Bedrock AgentCore
2+
3+
This sample demonstrates how to run a [Strands Agents](https://strandsagents.com/) agent as a
4+
Temporal Workflow, inside an [Amazon Bedrock AgentCore
5+
Runtime](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime.html).
6+
7+
It combines three things:
8+
9+
- The [Temporal Strands
10+
plugin](https://docs.temporal.io/develop/python/integrations/strands-agents),
11+
which runs the agent inside a Workflow and turns every model call into a
12+
Temporal Activity -- so model invocations get durable retries, timeouts, and
13+
crash recovery.
14+
- The [AgentCore Code
15+
Interpreter](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/code-interpreter-getting-started.html)
16+
as the agent's tool, so it validates its answers by running Python in a managed
17+
sandbox instead of doing arithmetic in its head. The Temporal plugin enables wrapping the tool with an activity.
18+
- [Temporal Serverless Workers](https://docs.temporal.io/serverless-workers) to launch the AgentCore Runtime when
19+
workflows kick off.
20+
21+
## Prerequisites
22+
23+
- A [Temporal Cloud](https://temporal.io/cloud) namespace (or a self-hosted
24+
Temporal cluster reachable from AgentCore)
25+
- Python 3.10+ and [uv](https://docs.astral.sh/uv/)
26+
- Node.js 20+ -- the [AgentCore CLI](https://github.com/aws/agentcore-cli) is an
27+
npm package: `npm install -g @aws/agentcore`
28+
- AWS CLI configured, and the [AWS
29+
CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) bootstrapped
30+
in the target account/region (`cdk bootstrap`)
31+
- AWS permissions for the AgentCore CLI (S3, IAM, CloudFormation): see [Use the
32+
AgentCore
33+
CLI](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-permissions.html#runtime-permissions-cli)
34+
- Access to an [Amazon
35+
Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access-modify.html)
36+
foundation model in your region -- the agent uses the plugin's default
37+
`BedrockModel()`
38+
39+
40+
Docker is not needed. With the CodeZip build there is no image: the CLI uploads a
41+
zip of this directory and the platform runs it on a managed Python runtime, which
42+
also takes care of AgentCore's ARM64 (Graviton) requirement.
43+
44+
## Files
45+
46+
| File | Description |
47+
|------|-------------|
48+
| `agentcore_worker.py` | Runtime entry point -- a `BedrockAgentCoreApp` that runs the Worker |
49+
| `workflows.py` | `StrandsAgentWorkflow` -- a `TemporalAgent` with the code interpreter tool |
50+
| `activities.py` | `execute_code` -- the AgentCore Code Interpreter, wrapped as a Temporal Activity |
51+
| `starter.py` | Helper program to start a Workflow execution from a local machine |
52+
| `agentcore/agentcore.json` | AgentCore project config -- the runtime definition (entry point, env vars, lifecycle) |
53+
| `agentcore/aws-targets.json` | AWS account and region to deploy into |
54+
| `bin/mk-invoke-role.sh` | Creates the role Temporal Cloud assumes to invoke the runtime |
55+
| `iam-role-for-temporal-agentcore-invoke.yaml` | CloudFormation template for that role |
56+
| `code-interpreter-policy.json` | Code Interpreter permissions, attached to the runtime's execution role via `additionalPolicies` |
57+
| `bin/create-runtime.sh` | Creates/updates the runtime with the AgentCore CLI |
58+
59+
60+
## Determining Busy vs Idle
61+
62+
AgentCore has two levers to control timeouts: idle and max. The idle timeout allows the runtime to exit early when there
63+
is no more work for it to do. The max timeout determines the max amount of time a session is allowed to run. A naive
64+
approach would be to have the Temporal worker spin up and never shutdown on its own. However, the max timeout doesn't
65+
offer graceful shutdown and it would defeat the purpose of AgentCore's idle timer.
66+
67+
This sample, includes a ActivityTracker which intercepts the Worker activity and keeps the worker alive. If the worker sits
68+
idle for AGENTCORE_DEBOUNCE_SECONDS, it will shutdown and enable AgentCore to exit based on idle timer. The idle timer
69+
in this configuration can be very short since there is no temporal worker running when idle.
70+
71+
## Setup
72+
73+
This sample opts to utilize the new AgentCore CLI for creating and updating both the Runtime and Runtime Endpoint.
74+
75+
### 1. Choose the AWS account and region
76+
77+
Edit `agentcore/aws-targets.json` with the account ID and region to deploy into. AgentCore is only available in
78+
[certain regions](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-regions.html), and the region
79+
needs the Bedrock model enabled.
80+
81+
### 2. Configure the Temporal connection
82+
83+
The Worker reads its connection details from the runtime's environment, so edit
84+
the `envVars` on the runtime in `agentcore/agentcore.json`:
85+
86+
| Variable | Description |
87+
|----------|-------------|
88+
| `TEMPORAL_ADDRESS` | Namespace endpoint, e.g. `<ns>.<account>.tmprl.cloud:7233` |
89+
| `TEMPORAL_NAMESPACE` | Namespace, e.g. `<ns>.<account>` |
90+
| `TEMPORAL_API_KEY` | Temporal Cloud API key (TLS is enabled automatically when set) |
91+
| `TEMPORAL_TASK_QUEUE` | Task Queue to poll (defaults to `workflows.TASK_QUEUE`) |
92+
| `TEMPORAL_DEPLOYMENT_NAME` / `TEMPORAL_BUILD_ID` | **Required.** Worker Deployment name and Build ID. The Worker always registers a Worker Deployment Version, and refuses to start without these -- a defaulted build ID would silently strand Workflows on a version nothing is polling |
93+
| `AWS_REGION` | Region for the agent's Bedrock model calls and its Code Interpreter sessions |
94+
| `CODE_INTERPRETER_IDENTIFIER` | Optional; overrides the default `aws.codeinterpreter.v1` sandbox |
95+
| `AGENTCORE_DEBOUNCE_SECONDS` | How long the Worker keeps polling after it goes idle (default `60`) |
96+
97+
Putting the API key in `envVars` keeps this sample short. For production, read it
98+
from secret store in `agentcore_worker.py` instead of shipping it in the
99+
runtime config.
100+
101+
### 3. Create the runtime and its endpoint
102+
103+
```bash
104+
./bin/create-runtime.sh
105+
```
106+
107+
This validates the config, zips this directory, uploads it, and deploys the runtime through CDK. Re-run it to roll out
108+
changes. (`.git`, `.venv`, `__pycache__` and `node_modules` are excluded from the zip.)
109+
110+
`agentcore deploy` also creates the runtime's execution role, so there is nothing to configure for it. The one thing
111+
that role does not grant by default is Code Interpreter access, which this sample needs, so `agentcore.json` points at a
112+
policy file that is attached to it:
113+
114+
```json
115+
"additionalPolicies": ["code-interpreter-policy.json"]
116+
```
117+
118+
It also creates the **runtime endpoint** that Temporal Cloud invokes. The endpoint is declared alongside the runtime in
119+
`agentcore/agentcore.json`. You can find the Endpoint ARN in the CloudFormation Stack Output displayed after running
120+
`create-runtime.sh` or found in the AWS Console.
121+
122+
123+
On the first run this step also generates `agentcore/cdk/`, the CDK app that `agentcore deploy` synthesizes. That
124+
scaffold is generated boilerplate -- it reads `agentcore.json` and `aws-targets.json` at synth time and holds nothing
125+
specific to this sample -- so it is gitignored rather than checked in.
126+
127+
### 4. Create the Temporal Cloud invoke role
128+
129+
With Serverless Workers, Temporal Cloud assumes a role in your account and calls the endpoint when Tasks arrive. Create
130+
that role with the same External ID used to create your serverless worker configuration:
131+
132+
```bash
133+
./bin/mk-invoke-role.sh <stack-name> <external-id> <agent-runtime-arn>
134+
```
135+
136+
That deploys `iam-role-for-temporal-agentcore-invoke.yaml`, which grants `bedrock-agentcore:InvokeAgentRuntime` and
137+
`bedrock-agentcore:GetAgentRuntimeEndpoint`, and trusts Temporal Cloud's principals only under your External ID. Pass
138+
the runtime ARN with a trailing `*` (`...:runtime/temporal_strands_worker-XXXXXXXXXX*`) so the role covers the runtime
139+
*and* its endpoints. Give the stack's `RoleARN` output, and the endpoint, back to Temporal Cloud.
140+
141+
### 5. Set up the Worker Deployment Version
142+
143+
The Worker always registers the Worker Deployment Version named by
144+
`TEMPORAL_DEPLOYMENT_NAME` / `TEMPORAL_BUILD_ID`, and pins Workflows to it. You
145+
must set that version as current, or nothing will be routed to the Worker:
146+
147+
```bash
148+
149+
temporal worker deployment create \
150+
--name <TEMPORAL_DEPLOYMENT_NAME>
151+
152+
temporal worker deployment create-version \
153+
--aws-agentcore-endpoint-arn <RuntimeEndpointARN> \
154+
--aws-agentcore-assume-role-external-id <ExternalId> \
155+
--aws-agentcore-assume-role-arn <InvokeRoleArn> \
156+
--build-id <TEMPORAL_BUILD_ID> \
157+
--deployment-name <TEMPORAL_DEPLOYMENT_NAME>
158+
```
159+
160+
See [Worker
161+
deployments](https://docs.temporal.io/production-deployment/worker-deployments).
162+
163+
### 6. Run the agent
164+
165+
`starter.py` reads the same connection variables the Worker uses, so export
166+
them and run it from this directory:
167+
168+
```bash
169+
export TEMPORAL_ADDRESS=<your-namespace>.<account>.tmprl.cloud:7233
170+
export TEMPORAL_NAMESPACE=<your-namespace>.<account>
171+
export TEMPORAL_API_KEY=<your-api-key>
172+
```
173+
174+
```bash
175+
uv run python starter.py
176+
uv run python starter.py "Calculate the first 10 Fibonacci numbers."
177+
```
178+
179+
Nothing else is needed: Temporal Cloud sees the Tasks on the Task Queue, invokes
180+
the runtime endpoint, the Worker starts inside AgentCore, drains the queue, and
181+
shuts itself down once idle.
182+
183+
Each Workflow execution gets its own sandbox, named after its Workflow ID.
184+
185+
The default prompt asks the agent to verify a claim by running code, so you
186+
should see it open a sandbox and execute Python. In the Workflow history that
187+
shows up as alternating `invoke_model` and `execute_code` Activities:
188+
189+
```bash
190+
temporal workflow show --workflow-id agentcore-strands-workflow-id-1
191+
```
192+
193+
Follow the Worker from the AgentCore side with `agentcore logs --runtime temporal_strands_worker`.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import os
2+
from typing import Any
3+
4+
from strands_tools.code_interpreter import AgentCoreCodeInterpreter
5+
from strands_tools.code_interpreter.models import ExecuteCodeAction, LanguageType
6+
from temporalio import activity
7+
8+
9+
@activity.defn
10+
def execute_code(
11+
code: str, language: LanguageType = LanguageType.PYTHON
12+
) -> dict[str, Any]:
13+
"""Run code in this Sessions's sandbox (workflow ID) and return the Code Interpreter result."""
14+
interpreter = AgentCoreCodeInterpreter(
15+
region=os.environ.get("AWS_REGION", "us-west-2"),
16+
session_name=activity.info().workflow_id,
17+
)
18+
return interpreter.execute_code(
19+
ExecuteCodeAction(type="executeCode", code=code, language=language)
20+
)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"$schema": "https://schema.agentcore.aws.dev/v1/agentcore.json",
3+
"name": "TemporalStrandsAgent",
4+
"version": 1,
5+
"managedBy": "CDK",
6+
"tags": {
7+
"agentcore:project-name": "TemporalStrandsAgent"
8+
},
9+
"runtimes": [
10+
{
11+
"name": "temporal_strands_worker",
12+
"description": "Strands agent on a scale-to-zero Temporal worker",
13+
"build": "CodeZip",
14+
"entrypoint": "agentcore_worker.py",
15+
"codeLocation": ".",
16+
"runtimeVersion": "PYTHON_3_12",
17+
"networkMode": "PUBLIC",
18+
"protocol": "HTTP",
19+
"authorizerType": "AWS_IAM",
20+
"envVars": [
21+
{
22+
"name": "TEMPORAL_ADDRESS",
23+
"value": "<your-namespace>.<account>.tmprl.cloud:7233"
24+
},
25+
{
26+
"name": "TEMPORAL_NAMESPACE",
27+
"value": "<your-namespace>.<account>"
28+
},
29+
{
30+
"name": "TEMPORAL_API_KEY",
31+
"value": "<your-api-key>"
32+
},
33+
{
34+
"name": "TEMPORAL_TASK_QUEUE",
35+
"value": "agentcore-strands-task-queue-python"
36+
},
37+
{
38+
"name": "TEMPORAL_DEPLOYMENT_NAME",
39+
"value": "temporal-strands-agentcore-python"
40+
},
41+
{
42+
"name": "TEMPORAL_BUILD_ID",
43+
"value": "v1"
44+
},
45+
{
46+
"name": "AWS_REGION",
47+
"value": "us-west-2"
48+
},
49+
{
50+
"name": "AGENTCORE_DEBOUNCE_SECONDS",
51+
"value": "60"
52+
}
53+
],
54+
"lifecycleConfiguration": {
55+
"idleRuntimeSessionTimeout": 900,
56+
"maxLifetime": 28800
57+
},
58+
"endpoints": {
59+
"temporal": {
60+
"version": 1,
61+
"description": "Invoked by Temporal Cloud Serverless Workers"
62+
}
63+
},
64+
"additionalPolicies": [
65+
"code-interpreter-policy.json"
66+
]
67+
}
68+
],
69+
"memories": [],
70+
"knowledgeBases": [],
71+
"credentials": [],
72+
"evaluators": [],
73+
"onlineEvalConfigs": [],
74+
"agentCoreGateways": [],
75+
"policyEngines": [],
76+
"configBundles": [],
77+
"abTests": [],
78+
"harnesses": [],
79+
"datasets": [],
80+
"payments": []
81+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
{
3+
"name": "default",
4+
"description": "Replace with the AWS account and region to deploy the runtime into",
5+
"account": "000000000000",
6+
"region": "us-west-2"
7+
}
8+
]

0 commit comments

Comments
 (0)