An operating system for agents. Strands is the inner harness — it runs the turn. Temporal is the outer durable harness — it runs the durable process the turns live inside. AWS supplies the silicon.
Strands makes the agent think. Temporal makes the system trustworthy.
This is a working multi-tenant control plane, and a boilerplate you can fork. The four reference agents are examples of what runs on it, not the point of it — the runtime is the deliverable: one generic workflow, manifest-driven agent packages, fair multi-tenant scheduling, safe version routing, and crash-safe side effects.
Adding an agent is a directory, not a code change: manifest.yaml + procedure.sop.md, zero workflow code, zero UI code. See CONTRIBUTING.md.
Everything here exists to keep these working. They are the acceptance test for the whole project.
| # | Claim | How it is shown | Verify |
|---|---|---|---|
| 1 | Fairness protects everyone else | Fairness off → one tenant floods the queue → other tenants' p95 wait collapses. Fairness on → same flood → protected tenants hold steady. | make verify-tenant-priority |
| 2 | Safe version routing | Deploy v2 with sessions mid-reasoning. In-flight sessions stay pinned to v1 and finish cleanly; new sessions start on v2. | make verify-pinning, make verify-versioning |
| 3 | Kill and resume without duplication | Kill the worker at a tool boundary right after a payment. Restart. Sessions resume at the same turn and the payment counter still reads exactly 1. | make verify-kill-resume |
No mocked numbers anywhere: every figure on screen comes from a real Temporal or AWS call.
flowchart LR
subgraph Client
UI["Desktop-OS UI<br/>React + Vite<br/>agent store · process monitor<br/>session terminal · controls"]
end
subgraph Control["Control plane"]
API["FastAPI<br/>the ONLY Temporal client<br/>SSE bridge"]
end
subgraph Temporal["Temporal Cloud"]
TQ["Task queue<br/>Priority + Fairness keys"]
WF["AgentJobWorkflow<br/>PINNED · the only workflow type"]
WS["Workflow Streams<br/>job_events · model_stream"]
ES["External Storage<br/>claim-check"]
end
subgraph Worker["Worker (local today, Lambda in E9)"]
SP["StrandsPlugin<br/>model · tool · MCP calls<br/>become Activities"]
ACT["Activities<br/>payment · dispute · sandbox<br/>memory · guardrail · hosted · swarm"]
end
subgraph AWS
BR["Bedrock<br/>Claude · Nova · Guardrails"]
AC["AgentCore<br/>Gateway (MCP) · Memory<br/>Runtime (hosted) · Code Interpreter"]
DDB[("DynamoDB<br/>registry · tenants · installs<br/>job index · idempotency keys")]
S3[("S3 — large payloads")]
end
MOCK["Mockoon<br/>mocked payment + dispute services"]
UI -->|REST + SSE| API
API -->|start · signal · query| TQ
WS -->|subscribe| API
TQ --> WF --> WS
WF <-->|Activity tasks| SP
SP --> ACT
WF -. large payloads .-> ES <--> S3
SP --> BR
ACT --> BR & AC & DDB & MOCK
API --> DDB
The rules that shape this diagram: the UI never talks to Temporal — the API is the sole client. There is exactly one workflow type. Anything that is not Temporal event history lives in DynamoDB — no second datastore, no Redis, no broker.
sequenceDiagram
actor Op as Operator
participant UI
participant API as FastAPI
participant T as Temporal Cloud
participant W as Worker (Strands)
participant AWS as Bedrock / AgentCore
participant Svc as External service
Op->>UI: submit job for a tenant
UI->>API: POST /api/demo/flood · dos agent test
API->>T: start_workflow(Priority{key, fairness_key, weight})
UI->>API: GET /api/jobs/{id}/events (SSE)
API->>T: subscribe to Workflow Stream
T->>W: workflow task
W->>W: mark_job_started · resolve_agent_package
W->>AWS: recall_tenant_memory (AgentCore Memory)
W-->>UI: job_started · memory_recall
loop agent turn
W->>AWS: invoke_model_streaming (Bedrock)
AWS-->>UI: token · reasoning deltas
opt model proposes a tool
W->>AWS: apply_guardrail (Bedrock Guardrails)
W-->>UI: guardrail verdict (pass / blocked)
opt over approval threshold
W-->>UI: approval_pending
Op->>API: approve / deny
API->>T: signal submit_approval
W-->>UI: approval_resumed
end
W->>Svc: tool activity, idempotency-keyed
end
end
W->>AWS: record_tenant_memory
W-->>UI: job_finished
Why the durability claims hold: every model call, tool call, and MCP call is a Temporal Activity, so a worker crash resumes at the last completed step — and consequential tools are idempotency-keyed, so the side effect happens exactly once (proof 3). A session paused for human approval costs no worker resources and can wait for days.
| Layer | Choice | What it buys |
|---|---|---|
| Orchestration | Temporal Cloud (Python SDK) — never a local server | Durable execution, the outer harness |
| Agent framework | Strands via temporalio.contrib.strands |
Model/tool/MCP calls dispatched as Activities by StrandsPlugin |
| Models | Amazon Bedrock — Claude, Nova | Named in the manifest (bedrock-claude), never a raw model id |
| Tools | AgentCore Gateway (MCP) via TemporalMCPClient |
Tools re-listed per turn, tenant-portable catalog keys |
| Memory | AgentCore Memory, tenant-scoped | Recall across sessions for a tenant |
| Guardrails | Bedrock Guardrails | A deterministic gate between the model proposing a tool and the tool running |
| Hosted agents | AgentCore Runtime | Tier-3 lane: someone else's agent, registered by ARN, no repo access |
| Sandboxes | AgentCore Code Interpreter | Untrusted analysis, invoked from an Activity |
| Large payloads | S3 via Temporal External Storage | Claim-check keeps event history small |
| App database | DynamoDB | Registry, tenants, installs, job index, idempotency keys |
| Frontend / API | React + Vite + Tailwind / FastAPI | Desktop-OS shell; API is the sole Temporal client |
| Mocked services | Mockoon | The payment and dispute services the agents actually call |
| Feature | Used for | Status |
|---|---|---|
| Priority & Fairness | Proof 1 — per-tenant fairness keys and weights | Public Preview, paid Temporal Cloud add-on |
Worker Deployment Versioning (PINNED) |
Proof 2 — in-flight sessions never move | Public Preview |
| Serverless Workers on Lambda | The deployed worker lane (E9) | Public Preview — to be completed |
Workflow Streams (temporalio.contrib.workflow_streams) |
Live token/tool/guardrail events to the UI, no custom event bus | Experimental |
| External Storage (S3 claim-check) | Large tool payloads out of event history | Preview |
temporalio.contrib.strands |
The Strands ↔ Temporal integration itself | Experimental, version-pinned |
| Signals, queries, continue-as-new, retries, idempotency | Human approval, session state, crash-safe side effects | GA |
Preview features are labelled here, in code comments, and in the stage narration. Nothing claims GA that is not GA.
agents/ # agent packages — the extensibility surface
_template/ # copy this to start
invoice_exception/ # tier 2 · tools + MCP + swarm + structured output
incident_triage/ # tier 1 · SOP only, no tools (the flood generator)
dispute_resolution/ # tier 2 · read-only + consequential tools, sandbox
vendorcheck/ # tier 3 · hosted on AgentCore Runtime, manifest only
returns_triage/ # tier 1 · the worked example — two files, no code
app/
workflows/ # AgentJobWorkflow — the ONLY workflow type
activities/ # model, tools, guardrail, sandbox, memory, payment, hosted
registry/ # manifest loader, DynamoDB repository, priority, metrics
api/ # FastAPI, SSE bridge, sole Temporal client
worker.py
cli/ # `dos` CLI — init, validate, test, publish, demo controls
ui/ # React desktop-OS control plane (+ Playwright e2e)
infra/ # SAM templates, Lambda targets, hosted agent source
mocks/ # Mockoon collections
scripts/ # every `make verify-*` entry point
docs/
AWS_SETUP.md # manual AWS steps, append-only — reproduce the environment
DECISIONS.md # ADR log — why things are the way they are
BACKLOG.md # epics → stories → tasks, with what is done and what is not
MULTI_AGENT.md # Swarm inside a job vs agents between jobs
RUNBOOK.md # operating the demo from a laptop — terminals, preflight, failure modes
DEMO_SCRIPT.md # the run of show — to be completed (E8.1)
Prerequisites: Python 3.12 + uv, Node 20+, a Temporal Cloud namespace and API key, an AWS account with Bedrock model access, and mockoon-cli (npm i -g @mockoon/cli).
There is no local Temporal server and no Docker Compose — this runs against Temporal Cloud and real AWS, by design.
git clone <this repo> && cd durable-agent-control-plane
uv sync
cp sample.env .env # fill in Temporal + AWS values- Provision AWS by hand — follow docs/AWS_SETUP.md top to bottom (DynamoDB table, IAM, Bedrock Guardrail, AgentCore Gateway/Memory/Runtime, S3 bucket) and put the resulting ids in
.env. Nothing here creates AWS resources for you, so a fork can reproduce the environment exactly. - Seed tenants and publish agents
uv run dos tenant add acme --name Acme --tier platinum --priority-key 3 --fairness-weight 3.0 make agent-publish ID=invoice-exception uv run dos tenant install acme invoice-exception
- Run it — four terminals
make mockoon # mocked payment + dispute services on :3001 make worker # export AWS_PROFILE as a real env var, see gotchas below make api # :8000 make ui # :5173
- Make the worker's build current — a versioned worker receives no tasks until you do this:
uv run dos demo ramp --version <BUILD_ID from .env> --percent 100
- Run a job
make agent-test ID=invoice-exception PROMPT="Invoice INV-1001 for \$180 from Globex Retail failed PO matching."
Two gotchas that look like silent hangs:
- Start the worker with
AWS_PROFILEexported as a real OS env var (AWS_PROFILE=… make worker)..envalone is not enough — Bedrock's client reads the ambient environment. - After any
BUILD_IDbump, re-run step 4 after a worker has polled with the new id. The version does not exist server-side until then.
The same controls the UI's System Controls pane calls, available from the CLI:
uv run dos demo fairness --off # proof 1 — then flood, and watch p95 collapse
uv run dos demo flood --tenant initech --count 200
uv run dos demo metrics # per-tenant p95 wait, from real job timings
uv run dos demo fairness --on # same flood, protected tenants hold
uv run dos demo ramp --version v2 --percent 100 # proof 2 — new sessions only
uv run dos demo kill-worker --at-tool-boundary # proof 3 — crash after the next payment
uv run dos demo payment-count # the counter that must still read 1Every flood job is a real Bedrock call, so pick --count deliberately.
| Tier | Who | What you write |
|---|---|---|
| 1 | Analyst, no code | manifest.yaml + procedure.sop.md |
| 2 | Developer | Tier 1 + a custom activity tool in app/activities/ |
| 3 | External team | Your own agent on AgentCore Runtime, registered by ARN |
make agent-init ID=returns-triage # scaffold from agents/_template/
make agent-validate ID=returns-triage # model, tools, placeholders, policy
make agent-publish ID=returns-triage # writes a registry row — no redeploy, no restart
make agent-test ID=returns-triage PROMPT="..."A tier-1 author who has never heard of Temporal still inherits fair scheduling, crash recovery mid-turn without duplicated side effects, and safe upgrades while their sessions are in flight.
Worked example: agents/returns_triage/ is a complete tier-1 agent — a manifest and an SOP, no Python at all — authored entirely through the loop above and verified by make verify-returns-triage. Full guide: CONTRIBUTING.md.
There are no unit tests and no mocks here, deliberately. Verification means running the real thing against Temporal Cloud and real AWS, from a repeatable entry point.
make ci # ruff + mypy
make replay # non-determinism guard: replays real recorded histories
make verify # DynamoDB registry round-trip
make verify-agents # all native reference agents, real jobs
make verify-returns-triage # the worked tier-1 example, one job per SOP rule
make verify-payment # proof 3's idempotency mechanism
make verify-kill-resume # proof 3 end to end: crash mid-tool, restart, count == 1
make verify-pinning # proof 2: in-flight session survives a deploy
make verify-tenant-priority # proof 1: real Priority on real workflows
make verify-streaming # live tokens + tool calls over the real SSE route
make verify-guardrail # guardrail verdicts reach the session terminal
make verify-interrupt # human approve/deny mid-reasoning
make verify-sandbox-isolation # Code Interpreter isolation across a crash
make verify-external-storage # S3 claim-check actually shrinks event history
make verify-hosted # tier-3 hosted lane on AgentCore Runtime
make e2e # Playwright against the real UI and real stackRun make replay after any change under app/workflows/, and bump BUILD_ID when workflow behaviour changes.
Built and verified against real AWS and Temporal Cloud:
| Epic | ||
|---|---|---|
| E0 Foundations | ✅ | repo, config, DynamoDB single-table model |
| E1 Durable agent runtime | ✅ | AgentJobWorkflow, tools as activities, human interrupt |
| E2 Agent package system | ✅ | manifests, registry, dos CLI, four reference agents |
| E3 Multi-tenancy & fairness | ✅ | proof 1 |
| E4 Control plane API & streaming | ✅ | FastAPI, Workflow Streams → SSE, token streaming |
| E5 Desktop-OS UI | ✅ | four panes, stage-legibility pass |
| E6 Versioning & chaos | ✅ | proofs 2 and 3 |
| E7 AWS services | ✅ | Gateway, Memory, Guardrails, Code Interpreter, External Storage, hosted lane, multi-agent |
| E8 Demo hardening | 🔜 | to be completed |
| E9 Deploy | 🔜 | to be completed |
| E10 Open-source release | 🚧 | in progress — this README |
Tracked in docs/BACKLOG.md; listed here so nothing is discovered the hard way.
- Deployment (E9). Everything runs locally today against Temporal Cloud and real AWS. SAM templates, Serverless Workers on Lambda, API + Mockoon on App Runner, and UI on Amplify are not built.
- Demo hardening (E8.1/E8.2).
docs/DEMO_SCRIPT.mdis still a placeholder, and there is no full-run recording; Mockoon collection coverage is not audited.make demo-reset,make demo-seedandmake preflightdo exist, and docs/RUNBOOK.md covers operating the stack from a laptop. - Rehearsals. The 200-job fairness dress rehearsal, the 10× kill/restart rehearsal, and a 10-metre projector legibility pass are all still outstanding (each verified at reduced scale).
- Guardrail demonstrability. The
OffPolicyPaymentdenied topic is close to unreachable through a model turn — its definition overlaps the model's own refusal boundary, so the model declines before the gate is consulted. Reachable categories (e.g. the PII and profanity policies) are what the verification uses. See docs/DECISIONS.md. - Open-source release (E10). Temporal Code Exchange publication is not done.
- No per-tenant tool-authorization boundary. Isolation is installs, priority/fairness, and memory namespacing; the manifest — validated against the tool catalog at publish time — is the control point on what an agent may call. Fine when you author the packages; not sufficient for genuinely untrusted tenant packages.
| CONTRIBUTING.md | Add an agent — the three tiers, manifest fields, SOP authoring |
| docs/AWS_SETUP.md | Every manual AWS step, reproducible from scratch |
| docs/DECISIONS.md | ADR log — why, including the mistakes and what they cost |
| docs/BACKLOG.md | Epics → stories → tasks, with real verification notes |
| docs/MULTI_AGENT.md | Swarm inside one job vs agents across jobs, and the durability tradeoff |
| docs/RUNBOOK.md | Running the demo from a laptop — the four terminals, the pre-delivery timeline, and the failure-mode table |
| CLAUDE.md | The design non-negotiables this codebase is held to |
MIT — see LICENSE.