Skip to content
Draft
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
3 changes: 3 additions & 0 deletions python-agentcore-durable-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.mypy_cache/
.ruff_cache/
agentcore/cdk/
61 changes: 61 additions & 0 deletions python-agentcore-durable-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Durable Strands agent on Amazon Bedrock AgentCore

This sample supports the Temporal guide
[Build a durable agent on Amazon Bedrock AgentCore](https://docs.temporal.io/guides/durable-agent-on-agentcore).

The application keeps one Strands conversation in a long-running Temporal Workflow. Model and AgentCore Code
Interpreter calls run as Activities. The local Worker runs continuously. The AgentCore Runtime handler runs the same
Workflow and Activity code on serverless Worker compute and retires after an idle period.

## Run locally

Install dependencies:

```bash
uv sync
```

Start a local Temporal development server:

```bash
temporal server start-dev
```

In another terminal, start the Worker with AWS credentials that can call Bedrock and AgentCore Code Interpreter:

```bash
uv run python local_worker.py
```

In a third terminal, start a conversation:

```bash
uv run python chat.py
```

Enter `/finish` to close the Workflow.

## Test

The test uses a scripted model and does not call AWS:

```bash
uv run pytest
```

It processes the first prompt on one Worker, stops that Worker, processes a follow-up on a new Worker, and verifies that
the second model call receives the first turn's messages.

## Deploy

Install the AgentCore CLI, then generate its CDK project:

```bash
npm install -g @aws/agentcore
./bootstrap-agentcore-project.sh
```

Replace the placeholders in `agentcore/aws-targets.json` and `agentcore/agentcore.json`. Do not commit a Temporal Cloud
API key. Then follow the
[AgentCore Serverless Worker deployment guide](https://docs.temporal.io/production-deployment/worker-deployments/serverless-workers/agentcore)
to deploy the Runtime, create its invocation role, and configure the Worker Deployment Version.
23 changes: 23 additions & 0 deletions python-agentcore-durable-agent/activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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


# @@@SNIPSTART python-agentcore-durable-agent-code-activity
@activity.defn
def execute_code(
code: str, language: LanguageType = LanguageType.PYTHON
) -> dict[str, Any]:
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)
)


# @@@SNIPEND
81 changes: 81 additions & 0 deletions python-agentcore-durable-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": "TemporalDurableAgent",
"version": 1,
"managedBy": "CDK",
"tags": {
"agentcore:project-name": "TemporalDurableAgent"
},
"runtimes": [
{
"name": "temporal_durable_agent_worker",
"description": "Durable Strands agent on a Temporal Serverless 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": "durable-agent"
},
{
"name": "TEMPORAL_DEPLOYMENT_NAME",
"value": "durable-agent-agentcore"
},
{
"name": "TEMPORAL_BUILD_ID",
"value": "1.0.0"
},
{
"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 python-agentcore-durable-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 for the Runtime",
"account": "000000000000",
"region": "us-west-2"
}
]
120 changes: 120 additions & 0 deletions python-agentcore-durable-agent/agentcore_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import asyncio
import os
from concurrent.futures import ThreadPoolExecutor
from datetime import timedelta

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from temporalio.client import Client
from temporalio.common import VersioningBehavior
from temporalio.contrib.strands import StrandsPlugin
from temporalio.worker import (
ActivityInboundInterceptor,
ExecuteActivityInput,
Interceptor,
Worker,
WorkerDeploymentConfig,
WorkerDeploymentVersion,
)

from activities import execute_code
from workflows import BUILD_ID, DEPLOYMENT_NAME, TASK_QUEUE, DurableAgentWorkflow

app = BedrockAgentCoreApp()
log = app.logger

DEBOUNCE = float(os.environ.get("AGENTCORE_DEBOUNCE_SECONDS", "60"))
DRAIN = timedelta(minutes=2)


def required_env(key: str) -> str:
value = os.environ.get(key)
if not value:
raise RuntimeError(f"{key} is required")
return value


# @@@SNIPSTART python-agentcore-durable-agent-activity-tracker
class ActivityTracker(Interceptor):
def __init__(self) -> None:
self.inflight = 0
self.changed = asyncio.Event()

def intercept_activity(
self, next: ActivityInboundInterceptor
) -> ActivityInboundInterceptor:
return _TrackedActivity(next, self)

async def wait_until_idle(self, idle_seconds: float) -> None:
while True:
self.changed.clear()
try:
await asyncio.wait_for(self.changed.wait(), timeout=idle_seconds)
except asyncio.TimeoutError:
if self.inflight == 0:
return


class _TrackedActivity(ActivityInboundInterceptor):
def __init__(
self, next: ActivityInboundInterceptor, tracker: ActivityTracker
) -> None:
super().__init__(next)
self._tracker = tracker

async def execute_activity(self, input: ExecuteActivityInput):
self._tracker.inflight += 1
self._tracker.changed.set()
try:
return await self.next.execute_activity(input)
finally:
self._tracker.inflight -= 1
self._tracker.changed.set()


# @@@SNIPEND


# @@@SNIPSTART python-agentcore-durable-agent-runtime-handler
@app.entrypoint
@app.async_task
async def invoke(payload: dict) -> dict:
client = await Client.connect(
required_env("TEMPORAL_ADDRESS"),
namespace=required_env("TEMPORAL_NAMESPACE"),
api_key=required_env("TEMPORAL_API_KEY"),
tls=True,
plugins=[StrandsPlugin()],
)
tracker = ActivityTracker()

with ThreadPoolExecutor(max_workers=4) as activity_executor:
worker = Worker(
client,
task_queue=os.environ.get("TEMPORAL_TASK_QUEUE", TASK_QUEUE),
workflows=[DurableAgentWorkflow],
activities=[execute_code],
activity_executor=activity_executor,
interceptors=[tracker],
deployment_config=WorkerDeploymentConfig(
version=WorkerDeploymentVersion(
deployment_name=os.environ.get(
"TEMPORAL_DEPLOYMENT_NAME", DEPLOYMENT_NAME
),
build_id=os.environ.get("TEMPORAL_BUILD_ID", BUILD_ID),
),
use_worker_versioning=True,
default_versioning_behavior=VersioningBehavior.PINNED,
),
graceful_shutdown_timeout=DRAIN,
)
async with worker:
await tracker.wait_until_idle(DEBOUNCE)

return {"message": "Worker drained"}


# @@@SNIPEND


if __name__ == "__main__":
app.run()
30 changes: 30 additions & 0 deletions python-agentcore-durable-agent/bootstrap-agentcore-project.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail

SAMPLE_DIR="$(cd "$(dirname "$0")" && pwd)"
CDK_DIR="$SAMPLE_DIR/agentcore/cdk"

if [ -d "$CDK_DIR" ]; then
echo "AgentCore CDK project already exists at $CDK_DIR"
exit 0
fi

for tool in agentcore uv; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "'$tool' is required but is not installed." >&2
exit 1
fi
done

TEMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TEMP_DIR"' EXIT

agentcore create \
--project-name TemporalDurableAgent \
--no-agent \
--output-dir "$TEMP_DIR" \
--skip-git \
--skip-python-setup

mv "$TEMP_DIR/TemporalDurableAgent/agentcore/cdk" "$CDK_DIR"
echo "Created AgentCore CDK project at $CDK_DIR"
34 changes: 34 additions & 0 deletions python-agentcore-durable-agent/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import asyncio
import uuid

from temporalio.client import Client
from temporalio.contrib.strands import StrandsPlugin

from workflows import TASK_QUEUE, DurableAgentWorkflow


# @@@SNIPSTART python-agentcore-durable-agent-chat-client
async def main() -> None:
client = await Client.connect(
"localhost:7233",
plugins=[StrandsPlugin()],
)
handle = await client.start_workflow(
DurableAgentWorkflow.run,
id=f"durable-agent-{uuid.uuid4()}",
task_queue=TASK_QUEUE,
)

while prompt := input("You: "):
if prompt == "/finish":
await handle.signal(DurableAgentWorkflow.finish)
return
answer = await handle.execute_update(DurableAgentWorkflow.ask, prompt)
print(f"Agent: {answer}")


# @@@SNIPEND


if __name__ == "__main__":
asyncio.run(main())
19 changes: 19 additions & 0 deletions python-agentcore-durable-agent/code-interpreter-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BedrockAgentCoreCodeInterpreterAccess",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:StartCodeInterpreterSession",
"bedrock-agentcore:InvokeCodeInterpreter",
"bedrock-agentcore:StopCodeInterpreterSession",
"bedrock-agentcore:GetCodeInterpreterSession",
"bedrock-agentcore:ListCodeInterpreterSessions",
"bedrock-agentcore:GetCodeInterpreter",
"bedrock-agentcore:ListCodeInterpreters"
],
"Resource": "*"
}
]
}
Loading