Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Some examples require extra dependencies. See each sample's directory for specif
* [activity_worker](activity_worker) - Use Python activities from a workflow in another language.
* [batch_sliding_window](batch_sliding_window) - Batch processing with a sliding window of child workflows.
* [bedrock](bedrock) - Orchestrate a chatbot with Amazon Bedrock.
* [bedrock_agentcore/strands-agent](bedrock_agentcore/strands-agent) - Run a AWS Strands Agent with Temporal Plugin on AgentCore Worker.
* [cloud_export_to_parquet](cloud_export_to_parquet) - Set up schedule workflow to process exported files on an hourly basis
* [context_propagation](context_propagation) - Context propagation through workflows/activities via interceptor.
* [custom_converter](custom_converter) - Use a custom payload converter to handle custom types.
Expand Down
7 changes: 7 additions & 0 deletions bedrock_agentcore/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Generated by `agentcore create` / `agentcore deploy` (see bin/create-runtime.sh)
# in each sample directory.
**/agentcore/cdk/
**/agentcore/.cli/
**/agentcore/.cache/
**/agentcore/.env.local
**/agentcore/.llm-context/
193 changes: 193 additions & 0 deletions bedrock_agentcore/strands-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Strands Agent on Bedrock AgentCore

This sample demonstrates how to run a [Strands Agents](https://strandsagents.com/) agent as a
Temporal Workflow, inside an [Amazon Bedrock AgentCore
Runtime](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime.html).

It combines three things:

- The [Temporal Strands
plugin](https://docs.temporal.io/develop/python/integrations/strands-agents),
which runs the agent inside a Workflow and turns every model call into a
Temporal Activity -- so model invocations get durable retries, timeouts, and
crash recovery.
- The [AgentCore Code
Interpreter](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/code-interpreter-getting-started.html)
as the agent's tool, so it validates its answers by running Python in a managed
sandbox instead of doing arithmetic in its head. The Temporal plugin enables wrapping the tool with an activity.
- [Temporal Serverless Workers](https://docs.temporal.io/serverless-workers) to launch the AgentCore Runtime when
workflows kick off.

## Prerequisites

- A [Temporal Cloud](https://temporal.io/cloud) namespace (or a self-hosted
Temporal cluster reachable from AgentCore)
- Python 3.10+ and [uv](https://docs.astral.sh/uv/)
- Node.js 20+ -- the [AgentCore CLI](https://github.com/aws/agentcore-cli) is an
npm package: `npm install -g @aws/agentcore`
- AWS CLI configured, and the [AWS
CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) bootstrapped
in the target account/region (`cdk bootstrap`)
- AWS permissions for the AgentCore CLI (S3, IAM, CloudFormation): see [Use the
AgentCore
CLI](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-permissions.html#runtime-permissions-cli)
- Access to an [Amazon
Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access-modify.html)
foundation model in your region -- the agent uses the plugin's default
`BedrockModel()`


Docker is not needed. With the CodeZip build there is no image: the CLI uploads a
zip of this directory and the platform runs it on a managed Python runtime, which
also takes care of AgentCore's ARM64 (Graviton) requirement.

## Files

| File | Description |
|------|-------------|
| `agentcore_worker.py` | Runtime entry point -- a `BedrockAgentCoreApp` that runs the Worker |
| `workflows.py` | `StrandsAgentWorkflow` -- a `TemporalAgent` with the code interpreter tool |
| `activities.py` | `execute_code` -- the AgentCore Code Interpreter, wrapped as a Temporal Activity |
| `starter.py` | Helper program to start a Workflow execution from a local machine |
| `agentcore/agentcore.json` | AgentCore project config -- the runtime definition (entry point, env vars, lifecycle) |
| `agentcore/aws-targets.json` | AWS account and region to deploy into |
| `bin/mk-invoke-role.sh` | Creates the role Temporal Cloud assumes to invoke the runtime |
| `iam-role-for-temporal-agentcore-invoke.yaml` | CloudFormation template for that role |
| `code-interpreter-policy.json` | Code Interpreter permissions, attached to the runtime's execution role via `additionalPolicies` |
| `bin/create-runtime.sh` | Creates/updates the runtime with the AgentCore CLI |


## Determining Busy vs Idle

AgentCore has two levers to control timeouts: idle and max. The idle timeout allows the runtime to exit early when there
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
approach would be to have the Temporal worker spin up and never shutdown on its own. However, the max timeout doesn't
offer graceful shutdown and it would defeat the purpose of AgentCore's idle timer.

This sample, includes a ActivityTracker which intercepts the Worker activity and keeps the worker alive. If the worker sits
idle for AGENTCORE_DEBOUNCE_SECONDS, it will shutdown and enable AgentCore to exit based on idle timer. The idle timer
in this configuration can be very short since there is no temporal worker running when idle.

## Setup

This sample opts to utilize the new AgentCore CLI for creating and updating both the Runtime and Runtime Endpoint.

### 1. Choose the AWS account and region

Edit `agentcore/aws-targets.json` with the account ID and region to deploy into. AgentCore is only available in
[certain regions](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-regions.html), and the region
needs the Bedrock model enabled.

### 2. Configure the Temporal connection

The Worker reads its connection details from the runtime's environment, so edit
the `envVars` on the runtime in `agentcore/agentcore.json`:

| Variable | Description |
|----------|-------------|
| `TEMPORAL_ADDRESS` | Namespace endpoint, e.g. `<ns>.<account>.tmprl.cloud:7233` |
| `TEMPORAL_NAMESPACE` | Namespace, e.g. `<ns>.<account>` |
| `TEMPORAL_API_KEY` | Temporal Cloud API key (TLS is enabled automatically when set) |
| `TEMPORAL_TASK_QUEUE` | Task Queue to poll (defaults to `workflows.TASK_QUEUE`) |
| `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 |
| `AWS_REGION` | Region for the agent's Bedrock model calls and its Code Interpreter sessions |
| `CODE_INTERPRETER_IDENTIFIER` | Optional; overrides the default `aws.codeinterpreter.v1` sandbox |
| `AGENTCORE_DEBOUNCE_SECONDS` | How long the Worker keeps polling after it goes idle (default `60`) |

Putting the API key in `envVars` keeps this sample short. For production, read it
from secret store in `agentcore_worker.py` instead of shipping it in the
runtime config.

### 3. Create the runtime and its endpoint

```bash
./bin/create-runtime.sh
```

This validates the config, zips this directory, uploads it, and deploys the runtime through CDK. Re-run it to roll out
changes. (`.git`, `.venv`, `__pycache__` and `node_modules` are excluded from the zip.)

`agentcore deploy` also creates the runtime's execution role, so there is nothing to configure for it. The one thing
that role does not grant by default is Code Interpreter access, which this sample needs, so `agentcore.json` points at a
policy file that is attached to it:

```json
"additionalPolicies": ["code-interpreter-policy.json"]
```

It also creates the **runtime endpoint** that Temporal Cloud invokes. The endpoint is declared alongside the runtime in
`agentcore/agentcore.json`. You can find the Endpoint ARN in the CloudFormation Stack Output displayed after running
`create-runtime.sh` or found in the AWS Console.


On the first run this step also generates `agentcore/cdk/`, the CDK app that `agentcore deploy` synthesizes. That
scaffold is generated boilerplate -- it reads `agentcore.json` and `aws-targets.json` at synth time and holds nothing
specific to this sample -- so it is gitignored rather than checked in.

### 4. Create the Temporal Cloud invoke role

With Serverless Workers, Temporal Cloud assumes a role in your account and calls the endpoint when Tasks arrive. Create
that role with the same External ID used to create your serverless worker configuration:

```bash
./bin/mk-invoke-role.sh <stack-name> <external-id> <agent-runtime-arn>
```

That deploys `iam-role-for-temporal-agentcore-invoke.yaml`, which grants `bedrock-agentcore:InvokeAgentRuntime` and
`bedrock-agentcore:GetAgentRuntimeEndpoint`, and trusts Temporal Cloud's principals only under your External ID. Pass
the runtime ARN with a trailing `*` (`...:runtime/temporal_strands_worker-XXXXXXXXXX*`) so the role covers the runtime
*and* its endpoints. Give the stack's `RoleARN` output, and the endpoint, back to Temporal Cloud.

### 5. Set up the Worker Deployment Version

The Worker always registers the Worker Deployment Version named by
`TEMPORAL_DEPLOYMENT_NAME` / `TEMPORAL_BUILD_ID`, and pins Workflows to it. You
must set that version as current, or nothing will be routed to the Worker:

```bash

temporal worker deployment create \
--name <TEMPORAL_DEPLOYMENT_NAME>

temporal worker deployment create-version \
--aws-agentcore-endpoint-arn <RuntimeEndpointARN> \
--aws-agentcore-assume-role-external-id <ExternalId> \
--aws-agentcore-assume-role-arn <InvokeRoleArn> \
--build-id <TEMPORAL_BUILD_ID> \
--deployment-name <TEMPORAL_DEPLOYMENT_NAME>
```

See [Worker
deployments](https://docs.temporal.io/production-deployment/worker-deployments).

### 6. Run the agent

`starter.py` reads the same connection variables the Worker uses, so export
them and run it from this directory:

```bash
export TEMPORAL_ADDRESS=<your-namespace>.<account>.tmprl.cloud:7233
export TEMPORAL_NAMESPACE=<your-namespace>.<account>
export TEMPORAL_API_KEY=<your-api-key>
```

```bash
uv run python starter.py
uv run python starter.py "Calculate the first 10 Fibonacci numbers."
```

Nothing else is needed: Temporal Cloud sees the Tasks on the Task Queue, invokes
the runtime endpoint, the Worker starts inside AgentCore, drains the queue, and
shuts itself down once idle.

Each Workflow execution gets its own sandbox, named after its Workflow ID.

The default prompt asks the agent to verify a claim by running code, so you
should see it open a sandbox and execute Python. In the Workflow history that
shows up as alternating `invoke_model` and `execute_code` Activities:

```bash
temporal workflow show --workflow-id agentcore-strands-workflow-id-1
```

Follow the Worker from the AgentCore side with `agentcore logs --runtime temporal_strands_worker`.
21 changes: 21 additions & 0 deletions bedrock_agentcore/strands-agent/activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os
from typing import Any

from strands_tools.code_interpreter import AgentCoreCodeInterpreter
from strands_tools.code_interpreter.models import ExecuteCodeAction, LanguageType
from temporalio import activity


# Use AgentCore Code Interpreter to provide a code sandbox and execute LLM generated solution
@activity.defn
def execute_code(
code: str, language: LanguageType = LanguageType.PYTHON
) -> dict[str, Any]:
"""Run code in this Sessions's sandbox (workflow ID) and return the Code Interpreter result."""
interpreter = AgentCoreCodeInterpreter(
region=os.environ.get("AWS_REGION", "us-west-2"),
session_name=activity.info().workflow_id,
)
return interpreter.execute_code(
ExecuteCodeAction(type="executeCode", code=code, language=language)
)
81 changes: 81 additions & 0 deletions bedrock_agentcore/strands-agent/agentcore/agentcore.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"$schema": "https://schema.agentcore.aws.dev/v1/agentcore.json",
"name": "TemporalStrandsAgent",
"version": 1,
"managedBy": "CDK",
"tags": {
"agentcore:project-name": "TemporalStrandsAgent"
},
"runtimes": [
{
"name": "temporal_strands_worker",
"description": "Strands agent on a scale-to-zero Temporal worker",
"build": "CodeZip",
"entrypoint": "agentcore_worker.py",
"codeLocation": ".",
"runtimeVersion": "PYTHON_3_12",
"networkMode": "PUBLIC",
"protocol": "HTTP",
"authorizerType": "AWS_IAM",
"envVars": [
{
"name": "TEMPORAL_ADDRESS",
"value": "<your-namespace>.<account>.tmprl.cloud:7233"
},
{
"name": "TEMPORAL_NAMESPACE",
"value": "<your-namespace>.<account>"
},
{
"name": "TEMPORAL_API_KEY",
"value": "<your-api-key>"
},
{
"name": "TEMPORAL_TASK_QUEUE",
"value": "agentcore-strands-task-queue-python"
},
{
"name": "TEMPORAL_DEPLOYMENT_NAME",
"value": "temporal-strands-agentcore-python"
},
{
"name": "TEMPORAL_BUILD_ID",
"value": "v1"
},
{
"name": "AWS_REGION",
"value": "us-west-2"
},
{
"name": "AGENTCORE_DEBOUNCE_SECONDS",
"value": "60"
}
],
"lifecycleConfiguration": {
"idleRuntimeSessionTimeout": 900,
"maxLifetime": 28800
},
"endpoints": {
"temporal": {
"version": 1,
"description": "Invoked by Temporal Cloud Serverless Workers"
}
},
"additionalPolicies": [
"code-interpreter-policy.json"
]
}
],
"memories": [],
"knowledgeBases": [],
"credentials": [],
"evaluators": [],
"onlineEvalConfigs": [],
"agentCoreGateways": [],
"policyEngines": [],
"configBundles": [],
"abTests": [],
"harnesses": [],
"datasets": [],
"payments": []
}
8 changes: 8 additions & 0 deletions bedrock_agentcore/strands-agent/agentcore/aws-targets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"name": "default",
"description": "Replace with the AWS account and region to deploy the runtime into",
"account": "000000000000",
"region": "us-west-2"
}
]
Loading
Loading