One coordinator, four specialists, a sandbox, a memory of prior deals, and a human gate on every consequential action — built end to end on Strands Agents and Bedrock AgentCore, with nothing leaving your AWS account.
Atlas Deal Desk evaluates acquisition targets for a mid-market industrial roll-up. Given a short list — Acme Robotics, Bridgewell Dynamics, Norwood Automation — it has to do the unglamorous core of corporate development: build a DCF for each, pull public-market comparables, read the sector tailwinds, check the open diligence red flags, weigh it all against what prior deals taught the committee, and hand back a clear PURSUE / HOLD / PASS for each with an IRR and a rationale.
None of those steps is hard alone. The difficulty is that they interlock and pull in different kinds of work: a DCF is arithmetic that must be exact; macro research is open-web; diligence is a credentialed internal API; and the final call depends on institutional memory — "we walked from Brightline because integration risk was the silent killer." Pour all of that into one prompt with every tool attached and you get the failure mode every agent engineer knows: context bloat, unreliable tool selection, and methodology no one can review.
The fix is structure — a team, not a monolith — and a production substrate underneath it. The interesting question is who owns that substrate.
A managed agent runtime gets you to a capable agent in a few API calls — the loop, the sandbox, memory, and skills all run as a service. That velocity is real. But for a buyer in finance, pharma, or the public sector, one question decides the platform before any feature comparison: does our data leave our account?
Atlas answers "no" by construction. It runs entirely on AWS services you own:
Inference is Bedrock. Code runs in an AgentCore microVM. Memory is an AgentCore store. Secrets live in AgentCore Identity. Every call is captured by CloudTrail and traced by AgentCore Observability. The trust boundary is your own IAM and VPC — not a second vendor's access model. That is the entire argument, and for regulated deal work it is usually the whole conversation.
The team is four specialists, each its own Strands Agent with its own context window and its own tools, exposed to the coordinator as a tool — the agents-as-tools pattern. Heavy, short-lived context (a full comps pull, a DCF's intermediate tables) stays inside the specialist and never crowds the coordinator.
@tool
def financial_analyst(target: str) -> str:
"""Build a DCF for one target and return NPV, IRR, and the three
most sensitive assumptions."""
sub = build_subagent("financial-analyst") # own context + run_python tool
return str(sub(f"Build a DCF for {target}…")) # returns a compact result
The coordinator calls all four and gets four compact summaries back, never the raw work. When you want deterministic parallelism instead of letting the model choose when to fan out, the same four agents drop into a Strands GraphBuilder graph — coordinator node, four specialist nodes, edges fanning out — and run concurrently.
If a step carries its own large, short-lived context that's irrelevant to the outer task, give it its own agent. A 5-year DCF qualifies; the intermediate model is irrelevant once the IRR pops out. Writing a one-line Slack note does not — the isolation overhead would exceed the benefit.
One config flip — ATLAS_MULTIAGENT — toggles the coordinator between a solo agent and the full roster, so the workshop can measure the before/after rather than assert it.
The financial-analyst doesn't eyeball a DCF; it writes pandas and runs it. Model-generated code on your own host is a bad evening waiting to happen, so it executes in AgentCore Code Interpreter — a managed, serverless microVM, per session, server-side. One run_python tool hides two backends so the agent logic never changes:
def run_python(code: str, data_files: list[str]) -> str:
if config.use_code_interpreter: # ATLAS_CODE_INTERPRETER=1
return _run_agentcore(code, data_files) # managed microVM, pulls CSVs from S3
return _run_local(code, data_files) # local subprocess fallback for dev
The target financials live in a versioned S3 bucket; the sandbox pulls the requested CSVs into its working directory before the code runs. The seam is the point: for a customer who needs everything in their own VPC, you swap the backend for a container job that reads their S3 — and the agent doesn't change. Only the execution boundary moves.
When the managed sandbox is enabled but the SDK or permission is missing, run_python returns a loud AGENTCORE_ERROR: rather than quietly falling back to local execution. You should always know which boundary your code actually ran in.
The committee learns from every deal. Those lessons — Brightline (integration risk killed it), Keswick (customer concentration repriced at renewal), Tessaro (a clean profile plus a motivated seller is the best return) — live in AgentCore Memory with a semantic strategy, under a namespace, and are recalled before any recommendation.
In code, two surfaces: a recall_priors tool the coordinator calls explicitly, and a MemoryHookProvider that loads recent priors on AgentInitializedEvent and persists each turn on MessageAddedEvent — so today's evaluation is tomorrow's prior. Until you provision the store, a local fallback reads the seed markdown, so the workshop runs from minute one.
Before competitive-positioning forms a view, it must check the committee's diligence tracker for open red flags — "Bridgewell: top-customer renewal unconfirmed." In production that tracker is an internal API or a SaaS issue tracker, exposed to the agent as MCP tools through AgentCore Gateway, with the credential held by AgentCore Identity (outbound auth) and never written into agent code.
| Concern | Managed-runtime approach | Atlas on AWS |
|---|---|---|
| Expose a tool | MCP server attached to the agent | AgentCore Gateway turns an API/Lambda into MCP tools |
| Hold the credential | Managed vault | AgentCore Identity (outbound, user-delegated or autonomous) |
| Audit the access | Provider console | CloudTrail — every token fetch, in your account |
Gateway also gives you semantic search across a large tool inventory, so an agent facing hundreds of tools keeps a focused, low-latency set. To keep the workshop runnable with no third-party signup, Atlas ships a local mock of the tracker with the same issues; set ATLAS_GATEWAY_URL and the real Gateway/MCP path takes over without touching the agent.
The committee needs a comparable artifact, not free text. The coordinator returns a validated InvestmentThesis via Strands structured output — the agent fills a Pydantic model, the SDK validates it, and the same shape is what the grader checks. It replaces a managed rubric with an artifact you own in code.
And decomposition is a claim — "the team is better than the solo agent." A claim needs a measurement. Atlas ships an eval suite that runs the same tasks against both and grades the result:
numeric_tolerance grades IRR within a band, because a reorder-style judgment has legitimate spread.# solo coordinator vs full team, side by side
uv run python evals/run.py --compare
Recording a committee decision is a side effect you don't want an agent taking unsupervised. Atlas gates it with a Strands hook on BeforeToolCallEvent: when record_decision is about to run, an approver is asked; deny, and the call is cancelled and the model is told.
def _before_tool(self, event):
name = _tool_name(event)
if name not in self.gated: # only gate side-effecting tools
return
if not self.approver(name, _tool_input(event)):
_cancel(event, "DENIED by a human reviewer.") # the call never runs
The approver is pluggable: a terminal prompt in the CLI, Allow / Deny buttons in the Streamlit UI, or an auto-approve in the eval harness. Same hook, three surfaces — the gate is policy, not UI.
Because the coordinator is "just" a Strands program, deployment is ordinary AWS. Wrapping it in a BedrockAgentCoreApp gives it the production contract — POST /invocations with SSE streaming and GET /ping — running in an isolated microVM per session:
@app.entrypoint
async def invoke(payload: dict):
agent = build_coordinator()
async for event in agent.stream_async(payload["prompt"]):
yield event # streamed to the UI as it happens
The CLI (v0.21+) packages the source as a CodeZip and deploys through CDK / CloudFormation in a single command — no Dockerfile, no ECR push. The CDK stack creates the Runtime, the IAM execution role, and a DEFAULT endpoint entirely inside your account. Each update is a new immutable version; point a named endpoint (dev / test / prod) at a version and roll back instantly by repointing it. The same code runs on your laptop or the Runtime — only the credential source changes.
Every model call, tool call, and sub-agent hop is an OpenTelemetry span in the AgentCore / CloudWatch GenAI dashboard. CloudTrail records every Bedrock, Memory, Identity, and Code Interpreter call. S3 versioning records every skill change. "Show me every action this agent took, which model, which data, on what date" has a native answer — no extra vendor in the audit scope.
Atlas Deal Desk ships as a hands-on workshop: a starter/ tree with eight stubbed functions — one per capability — and a complete reference to diff against. Build it in order and the deal team comes alive a piece at a time.
The throughline is the same one that makes decomposition worth it: every piece — a sub-agent, a skill, a memory, a gated action — is a part you can add, version, test, and reason about on its own. A monolith grows by accretion into something no one understands; this grows by composition into something you can still explain to a teammate, to an auditor, or to yourself six months later.
The full repository — both agents, the four sub-agents, skills, the sandbox abstraction, the eval harness, the Streamlit UI, and the provisioning scripts — is on GitHub: github.com/laghao/atlas-deal-desk. Runs entirely on Amazon Bedrock + AgentCore. MIT licensed.