|
| 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`. |
0 commit comments