The situation
The subscriber help desk runs a reasoning loop the team wrote themselves, hosted on the AgentCore runtime. That decision is settled: they wanted control over the prompt structure, the tool-calling contract, and which model answers each step, and the harness would have taken all three.
Now a second agent is coming. Operations have asked for one that reconciles supplier invoices against delivery records, and it is different enough from the help desk that bolting it onto the existing prompt would make both worse. Two agents means the framework choice stops being an accident of whoever wrote the first one, and the team would rather settle it deliberately before there are four.
What makes this a real decision rather than a preference is that the runtime underneath is fixed. AgentCore hosts any framework, so nothing is ruled out on compatibility, and the differences that remain are about what each one hands you and what it leaves you to build. The team’s list is short. They need traces they can actually read when a reconciliation goes wrong. They need tools shared rather than reimplemented twice, and a plan for the day the two agents have to talk to each other.
What actually matters
The first thing to name is that the loop is the least interesting part. Every framework here runs the same cycle. The model returns either an answer or a tool-use block, something executes the tool, the result goes back, and round it goes again. Choosing between them on loop syntax is choosing on the part that varies least. What varies is everything arranged around the loop.
The second is observability, where the field genuinely splits, and it splits on a technicality with large consequences. Reconstructing an agent run on this runtime means emitting OpenTelemetry spans. Service metrics arrive by default; the spans that describe what happened inside a loop come from the framework, once CloudWatch Transaction Search is on and tracing is enabled for the agent. A framework that already speaks OpenTelemetry, and specifically the GenAI semantic conventions for agent and tool spans, means auto-instrumentation produces readable traces with almost no work. A framework that does not means writing the tracer, deciding what a span is, and naming the attributes yourself, then discovering during an incident which ones you failed to record. The same reasoning that makes tracing an agent’s decisions an up-front decision applies here: you are choosing how much of that work is already done.
The third is how tools reach the agent, and whether two agents can share them. A framework with native support for the Model Context Protocol consumes a gateway’s tool surface directly. Both agents then point at the same gateway and inherit the same authorisation, credentials, and tool definitions. A framework without it needs an adapter layer: code that exists only to bridge two things meant to fit, and a place for the two agents’ tool behaviour to drift apart.
The fourth is what multi-agent looks like when you get there, because the second agent is the one that shows whether the framework has anything to offer the third. Some express coordination as first-class structures, a graph of agents or a swarm working the same problem. Some express it as agents exposed to each other as tools. Some leave it entirely to you. None of these is wrong, but adopting a framework whose multi-agent story is “write it yourself” and then needing multi-agent six months later is a bad order to find that out in.
Underneath all of it: the language the team already writes matters more than any feature comparison. A framework that fits the code the team can maintain beats a marginally better one in a language they will avoid touching.
What we’ll filter on
- Does it emit OpenTelemetry spans with GenAI semantic conventions, so traces arrive from auto-instrumentation rather than instrumentation work?
- Does it consume MCP tools natively, so a gateway is a first-class tool source rather than an adapter?
- What is the multi-agent story when one agent becomes several?
- How much control does it give over the loop, the prompt structure, and the model per step?
- Is it available in the language the team actually maintains?
- How much of the deployment path to the runtime is already written?
The landscape
Strands Agents
AWS’s own open-source agent SDK, the one the AgentCore CLI marks as recommended, and the framework the managed harness itself runs on. It is model-driven by design: the model drives its own steps and emits tool-use blocks as it goes, rather than following a workflow you drew. That is the same cycle the other frameworks run, stated as the organising idea rather than one mode among several.
MCP is native in both Python and TypeScript, through an MCPClient handed straight to the agent constructor. A gateway’s streamable-HTTP endpoint is then a tool source rather than something to wrap. The first-party provider list is broad: Amazon Bedrock, Amazon Nova, Anthropic, Google, OpenAI, Ollama, Mistral, LiteLLM, SageMaker and Writer among them, several Python-only, plus a custom-provider interface. The model behind a step is a configuration change rather than a rewrite.
The loop has the controls a production agent needs and most frameworks make you add. Invocation limits cap turns, output tokens, and total tokens on a single call, scoped to that invocation rather than accumulating across the agent’s life. agent.cancel() stops a run from outside, and an external AbortSignal composes with it. An idempotency token makes a retried invocation block on the original and return its result instead of starting a second run. That matters more than it sounds when a client retries a slow agent. An exception raised inside a tool is converted into a tool result carrying an error status, so the model receives the failure as content and the run continues.
The API around the loop is where a team’s own behaviour attaches, and two parts of it carry most of the work. The @tool decorator turns a plain function into a tool. The first paragraph of the docstring becomes the description, the Args section describes the parameters, and the type hints complete the input schema. The text the model reads and the signature the code enforces come from one source, and in TypeScript a tool() helper does the same job from a Zod schema. Hooks carry the rest. BeforeToolCallEvent fires ahead of execution and can cancel the call with a message, substitute a different tool, or rewrite the parameters. An authorisation check or a validation goes there, without wrapping every tool by hand. The care that goes into a gateway’s tool schemas applies just as much to the tools an agent defines for itself.
Deployment to the runtime is a wrapper around the agent:
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
app = BedrockAgentCoreApp()
agent = Agent()
@app.entrypoint
def invoke(payload):
result = agent(payload.get("prompt", ""))
return {"result": result.message}
if __name__ == "__main__":
app.run()
Observability is native OpenTelemetry. Graph and swarm are orchestrators built into the SDK; workflow and agents-as-tools are patterns you assemble on top of them, and A2A sits alongside MCP.
LangGraph
The graph-shaped member of the LangChain family, and the one to reach for when you want the control flow drawn rather than discovered. You define nodes and edges, the model runs inside nodes, and the graph determines the order. It is the closest thing here to a state machine that happens to contain a model.
That explicitness is the attraction and the drawback. Cycles, branches, and checkpoints are yours to specify. That is exactly right when a workflow has a shape you can state and want enforced, and a lot of scaffolding when the path has to be found at runtime. LangChain’s tooling ecosystem is the largest of any option here.
MCP arrives through langchain-mcp-adapters, a first-party package that converts a server’s tools into LangChain tools. That is a thinner adapter than writing one yourself, and it is still a layer between the gateway and the agent rather than the gateway being a tool source directly.
It runs on the runtime, and AgentCore’s observability documentation names LangChain alongside Strands and CrewAI as frameworks that arrive with OpenTelemetry and GenAI-convention support, with an auto-instrumentation package covering the rest. LangChain’s own tracing has historically pointed at LangSmith, a separate product with its own account, so the thing to check is that spans reach CloudWatch rather than only the vendor’s console.
CrewAI
Organises work as a crew of role-playing agents with assigned goals, which makes multi-agent the default shape rather than something you grow into. When the problem genuinely decomposes into named roles, that framing is a fast way to express it. The vocabulary also carries well with people who are never going to read the code.
The same framing is the constraint. A single-agent task expressed as a crew of one carries the ceremony without the benefit, and a role framing leads to splitting work that one agent would handle in a single loop. It runs on the runtime, and AgentCore names it among the frameworks that arrive with OpenTelemetry and GenAI-convention support. MCP comes from the mcp extra of crewai-tools, either as an MCPServerAdapter or declared on the agent directly.
The OpenAI Agents SDK, Google ADK, and other framework agents
The runtime is deliberately framework-agnostic. The CLI scaffolds Google’s Agent Development Kit and the OpenAI Agents SDK alongside Strands and LangChain. Anything else deploys against the same contract: /invocations for POST and /ping for GET, on port 8080. MCP, A2A and AG-UI are alternative protocols on their own ports, each with its own contract. Packaging is a zip of code by default, with no Docker needed, or an ARM64 container image instead.
What you check is the same list. Whether it emits OpenTelemetry with the GenAI conventions determines whether observability is a dependency or a project. Whether it speaks MCP determines whether the gateway is a tool source or an adapter. For a framework outside the documented set, AgentCore supports third-party instrumentation libraries: OpenInference, OpenLLMetry, OpenLIT and Traceloop.
A custom loop over Converse
No framework at all: your code calls the Converse API with a toolConfig, reads the toolUse block, runs the tool, returns a toolResult, and calls again. Total control, and for a genuinely small agent it is less machinery than adopting a framework to do the same thing.
Everything else is yours. Spans, retries, cancellation, token budgets, multi-agent coordination, and the MCP client are all code you write and maintain. Each one is a place to be subtly wrong in a way that only shows up in production. Reasonable when the agent is small and permanent, and steadily less so as it grows.
Agent Squad
Agent Squad sits a layer above every option listed so far. A framework builds one agent: the loop, the tools, the prompt, the model behind each step. Agent Squad routes a request to the right agent out of several, using a classifier that takes the request and the conversation so far. It carries shared session state, so a follow-up lands back with the agent that answered the first question.
Two things to know before leaning on it. It composes with a framework choice rather than replacing one. Strands agents run underneath an Agent Squad classifier without being written differently, so a team can settle the framework now and add routing later. It is also no longer an AWS project. It started life in AWS Labs as the Multi-Agent Orchestrator, and maintenance has since moved outside AWS, so treat it as a community library rather than something AgentCore depends on. Routing is what coordinating several agents turns on, and it stays cleaner kept separate from the framework question rather than answered at the same time.
Not a framework at all
Worth naming to rule out for this team rather than in general. If neither agent needed a custom loop, the managed harness would take a declared model, instructions, tools, skills and memory and run the cycle on the same runtime. The framework question would not arise. The harness is itself built on Strands, and it exports to Strands code when configuration stops being enough, so the two paths meet. The help desk gave up the harness deliberately, and the invoice agent will share its tools and its traces, so both sit on the runtime. A team without that constraint should check the harness first.
Evaluation
Side by side
| Option | OTel + GenAI spans | Native MCP | Multi-agent story | Loop control | Languages | Deploy path |
|---|---|---|---|---|---|---|
| Strands Agents | ✓ native | ✓ constructor | ✓ graph, swarm, A2A | ✓ limits, cancel, idempotency | Python, TypeScript | ✓ CLI scaffold |
| LangGraph | ✓ | Adapter package | ✓ (you draw the graph) | ✓ explicit, verbose | Python, JavaScript | ✓ CLI scaffold |
| CrewAI | ✓ | ✓ tools extra | ✓ roles by default | Partial (framework-shaped) | Python | ✓ zip or container |
| Other framework SDKs | Check per framework | Check per framework | Varies | Varies | Varies | ✓ zip or container |
| Custom Converse loop | ✗ (you write it) | ✗ (you write it) | ✗ | ✓ total | Any | ✓ zip or container |
Agent Squad has no row here because it fills none of these columns: it routes between agents rather than building one, so it is a layer to add later rather than an option to choose between now.
Reading it for this team: nothing is disqualified, which is the honest starting position, and the columns that separate the field are the first two. A framework that already emits the right spans and already consumes MCP needs no second build. The gateway and the observability setup made for the help desk extend straight to the invoice agent. Everything in the custom-loop row that reads as control also reads as work.
The solution
Write both agents on Strands, and point them at the same gateway. It is the only option that scores clean on every column the team actually listed, and the two that decide it are observability and tools.
Traces arrive as a dependency rather than a project. Strands emits OpenTelemetry with the GenAI semantic conventions natively. Adding aws-opentelemetry-distro and running under opentelemetry-instrument produces a readable span tree for each run: the agent invocation at the top, then a span per loop cycle, with model calls and tool calls beneath. CloudWatch Transaction Search is a one-time account setup, and tracing is a per-agent toggle. Spans land in each agent’s own log group where the Region supports that default and the distro is 0.18.0 or later, and in the shared aws/spans log group otherwise. Both agents appear together on the CloudWatch GenAI Observability page, correlated by session and trace id. The help desk has already done that setup, and the invoice agent inherits it.
Tools stay in one place. Native MCP means the gateway is a tool source rather than something to adapt, so getSubscription is defined once, authorised once, and consumed by both agents. When a tool’s schema changes, it changes for both, and there is no adapter layer for the two agents’ behaviour to drift apart inside.
The loop controls matter more for the invoice agent than the help desk. A reconciliation run over a batch is exactly where an agent can spin. Invocation limits cap turns and total tokens on a single call, and agent.cancel() gives an external timeout something to call. An idempotency token means a client retrying a slow reconciliation blocks on the original rather than starting a second run. Converting tool exceptions into error results keeps a single bad supplier record from ending a batch.
Deployment is a wrapper and a short requirements file. BedrockAgentCoreApp wraps an @app.entrypoint function. The requirements carry bedrock-agentcore and strands-agents for the agent, aws-opentelemetry-distro and boto3 for the traces. The runtime expects /invocations for POST and /ping for GET on port 8080, and the default build packages the code as a zip, so Docker only enters the picture if you choose an ARM64 container image instead. The AgentCore CLI covers create, dev, deploy, and invoke, so the path from a local run to a deployed agent is short enough that nobody builds a bespoke one.
Keep the multi-agent primitives in reserve rather than reaching for them now. Two agents that share tools and do not call each other are two agents, and coordinating them is a problem to have before solving. What the graph, swarm, and A2A support give the team today is the knowledge that an answer exists for the day the invoice agent has to ask the help desk agent something. That risk is what made the framework choice worth settling deliberately.
Picking Strands does not close the multi-agent question, and leaving it open is the right state for it. Coordination might end up as a graph inside one agent, as agents exposed to each other as tools, or as a classifier routing between two agents that keep their own loops. The shape of the third agent will answer that better than anything decided today, and today’s decision shuts off none of those routes.
Why not LangGraph. Drawing the control flow is the right instinct when a workflow has a shape you want enforced, and both of these agents are meant to find their own path. A drawn graph would be scaffolding around a sequence the model is meant to produce. Check where its spans land too: LangChain’s tracing has pointed at a separate product, and what you want is spans in CloudWatch next to everything else.
Why not CrewAI. Roles are a good fit for a problem that decomposes into named specialists, and neither of these is that problem yet. Expressing a single-agent job as a crew of one carries the ceremony without the benefit, and the role framing leads to splitting work that one agent would handle in a single loop.
Why not a custom loop. The team already owns a reasoning loop and knows what it took. Writing a second one means writing spans, retries, cancellation, token budgets, and an MCP client again. Each is a place to be subtly wrong in a way that surfaces in production rather than in review.
Worked example
The invoice agent gets three tools, all of them gateway targets that already exist for the help desk or are added alongside them: getDeliveryRecord, getSupplierInvoice, and flagDiscrepancy. None is reimplemented in the agent, because the gateway publishes them as MCP tools and Strands consumes them directly.
The agent is a handful of lines. Agent() with the gateway attached as a tool source and a system prompt describing the reconciliation rules, wrapped in BedrockAgentCoreApp with an @app.entrypoint that takes an invoice id. Invocation limits cap it at a dozen turns, because a reconciliation that has not resolved in a dozen turns is stuck rather than thorough.
A run against a mismatched invoice looks like this in the trace. Turn one calls getSupplierInvoice, turn two calls getDeliveryRecord, turn three returns text comparing two quantities that differ by two crates, turn four calls flagDiscrepancy with the invoice id and the difference. Each cycle is a span, every tool call carries its arguments and its result, and the whole run is grouped under one trace id and the batch’s session id. When operations ask why invoice 4471 was flagged, the answer is a query.
The failure that proves the setup is a supplier whose delivery record is missing. getDeliveryRecord raises, and Strands converts that into a tool result with an error status rather than letting it propagate. The next turn marks the invoice unverifiable instead of a discrepancy and moves on. One bad record stops one invoice. Without that conversion it stops the batch.
What’s worth remembering
- AgentCore hosts any framework, so nothing is decided on compatibility; the choice is about what each framework hands you and what it leaves you to build.
- Whether a framework emits OpenTelemetry with the GenAI semantic conventions determines whether traces are a dependency or a project, and it separates the field further than loop syntax does.
- Native MCP support makes a gateway a tool source rather than an adapter, so several agents share one definition, one authorisation, and one place to change it.
- Strands is AWS’s own SDK and the one the managed harness runs on: native OpenTelemetry, native MCP, graph and swarm patterns for later, and a documented wrapper for the runtime.
- Check the multi-agent story before you need it, because finding out the framework has none on the day you need it is a bad order to find out in.
- Agent Squad routes between agents rather than building one, so it stacks on top of a framework choice instead of competing with it, and it is now maintained outside AWS.