Multi-Agent Is Not About Scale. It Is a Context Manager in Disguise.
Multi-agent on AgentCore is a context manager implemented as topology: subagents, A2A, and hybrid fleets exist so noisy work gets its own head, not because fan-out diagrams look impressive.

On Amazon Bedrock AgentCore, subagents and the A2A protocol exist so noisy work gets its own head. Speed and security are real side effects of that context decision.
You think you split agents for speed or security. AWS's real reason is that one agent reading ten pages fills its own head with HTML.
In this article: You will learn AWS's actual rationale for multi-agent design (context isolation, not fan-out for its own sake), the three AgentCore architectures from in-process DeepAgents subagents through
serve_a2ato hybrid fleets, how Agent Cards and the ID chain work on the wire, and the decision rule that keeps you from shipping a slower, more expensive system that only looks impressive in a diagram.
The obvious argument for multi-agent is scale: three competitors, three workers, three times faster. That argument is true, and AWS's reference implementation reports it (four to six minutes for three real sites, versus up to three times longer sequentially). It is also not the argument they lead with.
Their stated rationale is context. If your agent reads ten web pages, its context window fills with raw content. If the same agent then generates charts, the chart-generation logic competes with strategic reasoning for the space that is left. So you delegate deep work to isolated subagents that return only concise results, and the coordinator's context stays clean enough to actually reason.
Read that again as harness engineering. AgentCore still does not ship a first-party context compaction product. Multi-agent architecture is a context manager implemented as topology instead of as a compaction function. You are not selecting and evicting tokens. You are giving the noisy work its own head.
The speed is a side effect. The security is a side effect. Both are real, and both are downstream of a context decision.

Three architectures, ascending
There are exactly three ways to do this on AgentCore, and they differ in how decoupled the workers are.

One: in-process subagents
The default, and the one you should stay on longer than you want to.
Each competitor gets its own browser toolkit and a scoped subagent dict; the coordinator alone holds memory tools and the combined subagent list.
research_subagents = []
for company_name, company_url in COMPETITORS:
browser_toolkit, browser_tools = create_browser_toolkit(region="us-west-2") # ①
browser_toolkit.session_manager.session_wait_timeout = 60.0 # ②
toolkits_to_cleanup.append(browser_toolkit)
research_subagents.append({
"name": f"research-{company_name.lower()}",
"description": f"Researches {company_name} by browsing {company_url}.",
"system_prompt": RESEARCHER_PROMPT,
"tools": browser_tools, # ③
})
agent = create_deep_agent(
model=model,
subagents=[*research_subagents, analyst_subagent], # ④
tools=memory_tools, # ⑤
system_prompt=COORDINATOR_PROMPT,
name="competitive-research-coordinator",
)
① One browser toolkit per competitor means one Browser MicroVM each, with no shared cookies or shared context. ② The session wait timeout is raised so browser navigation does not fail at the default short deadline. ③ Each researcher receives only browser tools; memory, catalog, and sandbox stay off the worker. ④ The coordinator's subagent list fans research workers and the analyst under one in-process agent. ⑤ Memory tools sit on the coordinator only, so researchers that read hostile pages cannot write memory.
Note: The full extracted listing at code/agent-core/part-11-multi-agent-a2a/listings/01-in-process-subagents.py shows the same configuration with the marker comments removed.
One microVM. One deployment. Subagents are dicts with a name, a description, a system prompt, and a tool list. DeepAgents also accepts any LangGraph CompiledStateGraph as a subagent, so a subagent can be an arbitrarily complex graph rather than a prompt with tools.
The Claude Agent SDK does the same thing through AgentDefinition and the Agent tool, with each subagent getting a fresh conversation and an isolated context. Subagent delegation, parallelization, and orchestrator-workers ship off the shelf in the SDK.
Two lines carry the whole architecture.
One toolkit per competitor means one Browser MicroVM per competitor. Three isolated Chromium instances, no shared cookies, no shared context, running concurrently.
"tools": browser_tools means each researcher holds its own browser and nothing else. No memory tools. No catalog. No sandbox. The researcher that reads the hostile page cannot write to memory, because it does not have the tool. Not policy. Physics.
That is the cheapest security control in multi-agent design, and you were going to build it anyway for context reasons.
Two: protocol-fronted with A2A
When your agent needs to be callable by agents that are not yours, you front it with the A2A protocol on AgentCore and publish an Agent Card.
pip install "bedrock-agentcore[a2a]"
An A2A executor maps the protocol task context onto a DeepAgents thread, publishes an Agent Card as the public contract, and serves both through serve_a2a.
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import AgentCapabilities, AgentCard, AgentSkill, Part, TextPart
from a2a.utils import new_task
from bedrock_agentcore.runtime import serve_a2a
class DeepAgentA2AExecutor(AgentExecutor): # ①
def __init__(self, agent):
self.agent = agent
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
task = context.current_task or new_task(context.message)
if not context.current_task:
await event_queue.enqueue_event(task)
updater = TaskUpdater(event_queue, task.id, task.context_id)
# The A2A context_id becomes the DeepAgents thread_id
config = {"configurable": {"thread_id": task.context_id}} # ②
result = await self.agent.ainvoke(
{"messages": [("user", context.get_user_input())]}, config=config # ③
)
await updater.add_artifact([Part(root=TextPart(text=result["messages"][-1].content))]) # ④
await updater.complete()
async def cancel(self, context, event_queue):
pass
card = AgentCard( # ⑤
name="market-intel-researcher",
description="Researches a competitor's public pricing pages and returns typed findings.",
url="https://<runtime-endpoint>/", version="0.1.0",
capabilities=AgentCapabilities(streaming=True),
skills=[AgentSkill(id="research", name="researcher",
description="Long-horizon competitor research", tags=["research"])],
default_input_modes=["text"], default_output_modes=["text"],
)
serve_a2a(DeepAgentA2AExecutor(deep_agent), card) # ⑥
① The executor wraps your DeepAgents agent so A2A can drive it through AgentExecutor.
② The A2A task context_id is threaded into DeepAgents as thread_id, continuing the ID chain across the hop.
③ ainvoke runs the agent with the inbound user text and that thread configuration.
④ The final message content is published as an A2A artifact, then the task is marked complete.
⑤ The Agent Card is the public contract: name, skills, tags, and modes that other teams and orchestrators read.
⑥ serve_a2a binds the executor and card into a Runtime-facing A2A endpoint.
Note: The full extracted listing at code/agent-core/part-11-multi-agent-a2a/listings/02-a2a-executor-and-card.py shows the same configuration with the marker comments removed.
Two things deserve more attention than the boilerplate around them.
The Agent Card is a contract, and it is the only thing another team reads. Name, description, skills, tags, input and output modes. Tool descriptions are already the highest-leverage text in an agent codebase; an Agent Card is a tool description for an entire agent, and it will be read by an orchestrator you did not write, run by a team you have not met. Write it like an API doc, not like a label.
The ID chain shows up again. config = {"configurable": {"thread_id": task.context_id}}. The A2A task's context_id becomes the DeepAgents thread_id. And BedrockCallContextBuilder, which serve_a2a uses, extracts X-Amzn-Bedrock-AgentCore-Runtime-Session-Id into BedrockAgentCoreContext exactly as a plain entrypoint does.
Count what that one string now keys: the microVM lease, the sandbox session, the checkpointed conversation, and the A2A task context. Every stateful thing in this stack hangs off an identifier you thread by hand, and it has never once been optional.

The A2A wire format, since you will debug it, is JSON-RPC: method: "message/send", with params.message.parts carrying {"kind": "text", "text": ...} and configuration.blocking controlling whether you wait.
Three: hybrid orchestration
The full decoupling. A DeepAgents orchestrator whose "subagents" are thin clients calling separately deployed AgentCore agents.
import httpx
from langchain_core.tools import tool
@tool
async def call_research_subagent(query: str) -> str: # ①
"""Delegate a competitor research task to the remote research agent."""
async with httpx.AsyncClient(timeout=120) as client:
r = await client.post(A2A_ENDPOINT, json={ # ②
"jsonrpc": "2.0", "id": 1, "method": "message/send",
"params": {"message": {"role": "user", "parts": [{"kind": "text", "text": query}]},
"configuration": {"blocking": True}},
})
return r.json()["result"]["status"]["message"]["parts"][0]["text"] # ③
orchestrator = create_deep_agent(
model=ChatBedrockConverse(model="us.anthropic.claude-sonnet-4-6"),
tools=[call_research_subagent, call_catalog_agent, call_analyst_agent], # ④
system_prompt="Route work to the right specialist and synthesize the results.",
)
① A LangChain tool is the thin client surface: the orchestrator delegates by calling a function, not by owning the worker process.
② The tool posts JSON-RPC message/send to the remote A2A endpoint and waits with blocking: True.
③ Only the returned text is pulled out of the response, so remote intermediate work never enters the orchestrator's context.
④ Remote specialists appear as ordinary tools on create_deep_agent, so each is a separate deployment behind a local call site.
Note: The full extracted listing at code/agent-core/part-11-multi-agent-a2a/listings/03-hybrid-a2a-orchestrator.py shows the same configuration with the marker comments removed.
Now each specialist is its own deployment: its own scaling, its own model choice, its own release cadence, its own permissions. A cheap-model page-extractor fleet under an Opus-class synthesis agent, each sized to its job.
There is also a non-A2A variant: call the deployed agents through the Runtime invocation API directly, with AgentCoreRuntimeClient building authenticated WebSocket URLs and presigned URLs. That is less standard and one less protocol to learn. Use A2A when other teams need to discover you; use direct invocation when it is your fleet talking to itself.
And the composition is recursive: wrap the orchestrator in @app.entrypoint or serve_a2a, and you have an AgentCore-deployed agent orchestrating a fleet of AgentCore-deployed agents, which can itself be a worker for something else.
The decision rule is not technical
Stay in-process until an organizational boundary forces you out. Not a performance boundary. In-process subagents already run concurrently in separate MicroVMs. You are not buying parallelism by going remote; one browser toolkit per competitor already gave you that inside one deployment.
What you buy by going remote is independence: a different team owning the researcher, a different release cadence, a different model, a different budget, or a different blast radius. Those are org facts, not architecture facts.
So the ladder is: one agent until context pressure justifies subagents. In-process subagents until a team boundary or a deploy cadence justifies separate deployments. A2A when someone outside your repo needs to call you.

And the flat single-agent design wins more often than the conference talks suggest. Every delegation multiplies tokens: the coordinator's prompt, the subagent's prompt, the handoff, the summary, the synthesis. Every handoff is a place a task gets lost or silently half-completed. If your work is not genuinely parallel and not genuinely context-hostile, the multi-agent version is slower, more expensive, and harder to debug, and it will look impressive in the architecture diagram.
Gotcha: workers inherit nothing
This is the one that bites, and it bites in production rather than in testing.
A separately deployed worker is a separate deployment. It has its own workload identity (one is provisioned automatically per Runtime). Its own IAM role. Its own memory resource and actor_id namespace, or none. Its own tool grants. Its own Policy. Its own inbound auth mode, which is IAM or JWT, never both.
Nothing is inherited from the coordinator. Nothing.
The failure this produces is not an error, it is drift. You harden the coordinator carefully. You add a validator, tighten Policy, scope the Gateway targets. The workers, which were scaffolded from a template three sprints ago, are still running with the broad permissions they were born with, and they are the ones touching the hostile pages.
In production: provision each worker's harness explicitly, in the same IaC that provisions the coordinator, and write the permissions down per agent. If your deployment manifest lists a worker's ARN but not its policy, you are hoping.
There is a real design question about identity propagation across hops. Within one agent, the chain (caller JWT to Runtime, workload token to vault, per-user OAuth to the provider) can stay clean. Across an A2A hop it needs designing: the worker either acts as itself (2LO, and your audit log says "the researcher did it") or acts on behalf of the analyst (which means the identity has to travel). AWS's stated philosophy is that every hop should carry the minimum identity needed for that hop and no more. That is a design stance, not a default, and if per-analyst entitlements matter you have to build for it deliberately.
Watching it happen
Traces stop being nice-to-have here.
The CloudWatch GenAI Observability hierarchy is exactly this architecture: the coordinator's run at the top, a child span per research subagent, the analyst subagent after. Inside each span, tool calls with inputs, outputs, timing, and token usage.
That view gives you two things nothing else does.
You can prove the concurrency. "The subagents run in parallel" is a claim. Overlapping wall-clock spans are evidence. A supposedly parallel fan-out can turn out perfectly sequential because of a shared client with a connection pool of one, and no amount of reading the code finds it.
You can find the failing hop. In a single agent, a failure is a stack trace. In a fan-out, a failure is "the report is missing GitLab," and the trace is the only thing that tells you the GitLab researcher's browser navigation timed out at 10 seconds because somebody forgot session_wait_timeout = 60.0.

Pattern check: the subagent split implements the context manager (isolated contexts), isolation (one MicroVM per browser), and progressive disclosure (each worker sees only its own tools) at the same time. What the platform does not give you is the decision about where the boundaries go, and the default (every subagent gets every tool) is wrong in all three dimensions at once.
Do this today
- Keep one agent until raw page content or other bulk intermediate state is crowding out synthesis. Split for context pressure, not for a prettier box diagram.
- When you do split, start with in-process DeepAgents subagents (or Claude Agent SDK
AgentDefinition): one browser toolkit per worker, browser tools only on researchers, memory tools only on the coordinator. - Set
session_wait_timeout(for example60.0) on each browser toolkit before you trust "parallel research" in production. - Publish an Agent Card only when another team or orchestrator must discover you; write skills and descriptions like API docs. Prefer direct Runtime invocation for your own fleet talking to itself.
- Provision each remote worker's IAM role, Policy, memory resource, and inbound auth in the same IaC as the coordinator. Assume workers inherit nothing, because they do not.
Topology is the compaction function you already own
What you have now is a coordinator that fans per-competitor research out to workers, each with its own browser in its own MicroVM, each holding only the tools its job requires, returning concise typed findings to a coordinator whose context stayed clean enough to synthesize them, with an analyst subagent doing the math in a sandbox and the whole tree visible as one trace.
That is the architecture. It is AWS's architecture, honestly, and the notebook is public.
Look at what you just built. A fleet of agents, each with its own identity, its own permissions, and its own release cadence, most of which have no memory tools, one of which does. And the ones with no memory tools spend their whole day reading pages written by your competitors.
The next problem is not wiring another hop. It is what happens when one of those pages is written by someone who knows all of this.