Wrapping Any Framework
The same agent across Strands, LangGraph, CrewAI, and more
- Explain why AgentCore is framework-agnostic: `@app.entrypoint` is the seam between AWS's managed HTTP server and your framework's agent object
- Identify the invariant scaffold (`@app.entrypoint`, `payload.get("prompt")`, `return {"result": ...}`, `app.run()`, the `configure`→`launch`→`invoke` deploy flow) that never changes between frameworks
- Write the three variant lines — build object, call inside the entrypoint, pull the result — for Strands, LangGraph, CrewAI, OpenAI Agents SDK, Google ADK, and LlamaIndex
- Convert a synchronous entrypoint to an async one (or an `asyncio.run(...)` wrapper) for the OpenAI Agents SDK and Google ADK
- Describe what makes Strands AWS-native and why it is the first-class recommended SDK
- Navigate the `03-integrations/agentic-frameworks/` samples to find the canonical, runnable wrapper for any framework
AgentCore Runtime hosts a container that speaks a known protocol, not a framework — so the same wrapper hosts an agent built with Strands, LangGraph, CrewAI, the OpenAI Agents SDK, Google ADK, or LlamaIndex. This lesson makes that concrete with the 'six wrappers' pattern: the `@app.entrypoint`, the `payload.get("prompt")` read, the `{"result": ...}` return, `app.run()`, and the deploy flow stay invariant, while only the body — build the agent object, call it, pull the result — changes per framework.
- 1The seam: a container that speaks a protocol, not a framework
- 2The invariant scaffold vs the variant body
- 3The synchronous wrappers: Strands, LangGraph, CrewAI
- 4The async wrappers: OpenAI Agents SDK and Google ADK
- 5Why Strands is the AWS-native default
- 6The canonical reference and the one deploy flow
The seam: a container that speaks a protocol, not a framework
AgentCore Runtime does not host LangGraph, or CrewAI, or Strands. It hosts a container that speaks a known protocol (HTTP/MCP/A2A) and runs it in a serverless, per-session microVM. The framework lives inside that container, and AWS never needs to know which one you chose.
The BedrockAgentCoreApp SDK is what makes this work. It is a thin Starlette/Uvicorn ASGI wrapper that turns a plain Python function into the Runtime service contract — wiring up /invocations and /ping, session headers, streaming, and async task tracking. The @app.entrypoint decorator is the seam: on one side is AWS's managed HTTP server, on the other side is your framework's agent object. AWS calls your function with a payload dict and ships back whatever you return as JSON.
That seam is the whole trick. The body inside the entrypoint is the only part that changes between frameworks. Everything around it — the import, the app = BedrockAgentCoreApp(), the decorator, app.run(), and the deploy flow — is identical whether you wrap Strands or LlamaIndex.
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
app = BedrockAgentCoreApp()
agent = Agent() # <- the only framework-specific lines
@app.entrypoint
def invoke(payload):
result = agent(payload.get("prompt", "Hello")) # <- ...are these three
return {"result": result.message} # in the middle
if __name__ == "__main__":
app.run() # local http://localhost:8080; in the container binds 0.0.0.0:8080AWS officially supports Strands Agents (first-class / recommended), LangGraph/LangChain, CrewAI, LlamaIndex, Google ADK, and the OpenAI Agents SDK, plus arbitrary custom code. The framework list is one of the fast-moving facts — confirm the current set against the Use any agent framework page before you bet on it.
Key insight
Why this matters more than it looks
Framework-agnosticism is not a marketing checkbox — it is what lets you swap orchestration libraries without re-platforming. If LangGraph is the wrong tool for a workload, you change the three lines in the body, not the deploy pipeline, the IAM role, the ECR repo, or the invoke client. The blast radius of a framework decision shrinks to one function.
Watch out
Don't block the entrypoint
A blocking call inside @app.entrypoint starves the /ping health thread, and Runtime kills the session at the 15-minute synchronous timeout during long work. This applies to every framework. For long jobs, offload to a thread or use an async entrypoint plus @app.async_task so /ping reports HealthyBusy.
The invariant scaffold vs the variant body
Pin down exactly what never changes. The invariant — five things present in every framework's wrapper:
@app.entrypoint— the decorator that registers your function as the agent handler.payload.get("prompt")— how you read the caller's input out of the request dict.return {"result": ...}— the shape AWS serializes back to the caller.app.run()— boots the ASGI server (binds0.0.0.0:8080in the container, serves/invocations+/ping).- The deploy flow —
agentcore configure -e my_agent.py→agentcore launch(a.k.a.deploy) →agentcore invoke. Identical regardless of framework.
The variant — three steps in the middle that differ per framework:
- Build the agent object (once, at module load).
- Call it inside the entrypoint with the prompt.
- Pull the result out of whatever object the framework returns.
Here is the matrix that drives the rest of the lesson. Memorize the columns, not the rows:
| Framework | Build object | Call inside entrypoint | Pull result |
|---|---|---|---|
| Strands | agent = Agent(tools=[...]) | agent(prompt) | result.message |
| LangGraph | graph = builder.compile() | graph.invoke({"messages": [...]}) | out["messages"][-1].content |
| CrewAI | crew = AgentcoreCrewAi().crew() | crew.kickoff(inputs=...) | result.raw |
| OpenAI Agents SDK | agent = Agent(...) | await Runner.run(agent, query) | result.final_output |
| Google ADK | root_agent = Agent(model=...) | runner.run_async(...) | event.content.parts[0].text |
| LlamaIndex | query engine / agent | engine.query(prompt) (typical) | str(response) |
Notice the LlamaIndex row is hedged. The exact build/call/pull snippet for LlamaIndex is not verified from the docs — engine.query(prompt) and str(response) are the typical shape, but you should pull the precise, runnable code from the samples repo rather than trusting this row verbatim.
Tip
A reading discipline that pays off
When you open an unfamiliar framework's sample, find the three variant lines first: where is the object built, where is it called, what is unwrapped on return. Everything else in the file is the invariant scaffold you already know. This turns 'learning a new framework wrapper' into a five-minute diff.
The synchronous wrappers: Strands, LangGraph, CrewAI
Three of the six frameworks have a plain synchronous call, so a plain def entrypoint works.
Strands — the AWS-native reference. Build an Agent, call it like a function, read .message:
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
app = BedrockAgentCoreApp()
agent = Agent(tools=[...])
@app.entrypoint
def invoke(payload):
result = agent(payload.get("prompt", "Hello"))
return {"result": result.message}
if __name__ == "__main__":
app.run()LangGraph reaches Bedrock through langchain-aws. The model is ChatBedrockConverse (or init_chat_model(..., model_provider="bedrock_converse")); you compile a graph and call graph.invoke(...), then read the last message's content:
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from langchain_aws import ChatBedrockConverse
# from langgraph... import builder # your graph definition
app = BedrockAgentCoreApp()
llm = ChatBedrockConverse(model="us.anthropic.claude-...") # model IDs change — verify the live catalog
graph = builder.compile()
@app.entrypoint
def invoke(payload):
prompt = payload.get("prompt", "Hello")
out = graph.invoke({"messages": [("user", prompt)]})
return {"result": out["messages"][-1].content}
if __name__ == "__main__":
app.run()CrewAI wraps a multi-agent crew. The sample defines agents and tasks in config/agents.yaml and config/tasks.yaml, exposes a crew() factory, and you call crew.kickoff(inputs=...), reading .raw:
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from crew import AgentcoreCrewAi # the sample's crew factory
app = BedrockAgentCoreApp()
crew = AgentcoreCrewAi().crew()
@app.entrypoint
def invoke(payload):
result = crew.kickoff(inputs={"topic": payload.get("prompt", "Hello")})
return {"result": result.raw}
if __name__ == "__main__":
app.run()The invariant scaffold is byte-for-byte identical across all three. Only the build line, the call, and the unwrap differ.
Watch out
CrewAI needs Python 3.11+
The CrewAI sample depends on chroma-hnswlib, which requires Python 3.11 or newer. If your base image or configure runtime is older, the build fails on a native wheel that has nothing to do with AgentCore. Set the runtime version accordingly (the toolkit's -rt/--runtime flag) and match your local interpreter.
Note
LangGraph's path to Bedrock
LangGraph itself does not call Bedrock — langchain-aws does. Use ChatBedrockConverse (or model_provider="bedrock_converse") as the model. Forgetting langchain-aws in requirements.txt is the most common LangGraph deploy failure.
The async wrappers: OpenAI Agents SDK and Google ADK
Two frameworks are async: the OpenAI Agents SDK (await Runner.run(...)) and Google ADK (runner.run_async(...)). You have two equally valid ways to host them.
Option A — make the entrypoint itself async. BedrockAgentCoreApp supports an async def entrypoint directly, so you can await inside it:
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from agents import Agent, Runner # OpenAI Agents SDK
app = BedrockAgentCoreApp()
agent = Agent(name="Assistant", instructions="You are helpful.")
@app.entrypoint
async def invoke(payload):
result = await Runner.run(agent, payload.get("prompt", "Hello"))
return {"result": result.final_output}
if __name__ == "__main__":
app.run()Option B — keep a sync entrypoint and wrap with asyncio.run(...). Useful when the rest of your code is synchronous:
import asyncio
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from agents import Agent, Runner
app = BedrockAgentCoreApp()
agent = Agent(name="Assistant", instructions="You are helpful.")
@app.entrypoint
def invoke(payload):
result = asyncio.run(Runner.run(agent, payload.get("prompt", "Hello")))
return {"result": result.final_output}
if __name__ == "__main__":
app.run()Google ADK is async too, and it streams events — you build a root_agent, drive it with runner.run_async(...), and pull text out of the event objects. The result lives at event.content.parts[0].text:
from bedrock_agentcore.runtime import BedrockAgentCoreApp
# from google.adk... import Agent, Runner, root_agent setup
app = BedrockAgentCoreApp()
root_agent = Agent(model="...") # ADK agent
@app.entrypoint
async def invoke(payload):
prompt = payload.get("prompt", "Hello")
final_text = ""
async for event in runner.run_async(user_id="u", session_id="s", new_message=prompt):
if event.content and event.content.parts:
final_text = event.content.parts[0].text
return {"result": final_text}
if __name__ == "__main__":
app.run()The deploy flow does not change for async agents — configure, launch, invoke work identically. The only adaptation is the entrypoint signature.
Tip
When to prefer async over asyncio.run
An async def entrypoint is the cleaner choice when the framework is natively async (OpenAI, ADK) and when you want streaming — an async-generator entrypoint auto-serves Server-Sent Events, one yield per data: event. Reach for the asyncio.run(...) wrapper only when you're bolting an async call onto otherwise-synchronous code. Either way, never let a long await block the /ping thread.
Why Strands is the AWS-native default
Strands appears first in the matrix and is the recommended SDK for a reason: it is AWS's own open-source agent framework, and it integrates with the rest of AgentCore in ways the others don't.
- Apache-2.0 licensed, open source.
- Model-driven: you declare a model + tools + system prompt, and the SDK runs the agentic loop for you — no hand-rolled orchestration.
- The
@tooldecorator turns any Python function into a tool the agent can call. strands-agents-toolsships 20+ prebuilt tools out of the box.- MCP integration — Strands agents can consume MCP servers directly (this is how they reach AgentCore Gateway tools).
- Native AgentCore hooks —
AgentCoreMemorySessionManagerpersists conversation to AgentCore Memory, andStrandsA2AExecutorprovides Agent-to-Agent (A2A) support. - A TypeScript SDK (
@strands-agents/sdk) exists alongside the Python one.
The @tool decorator is the most ergonomic part of the model-driven loop:
from strands import Agent, tool
@tool
def get_weather(city: str) -> str:
"""Return the current weather for a city."""
return f"It's sunny in {city}."
agent = Agent(tools=[get_weather]) # the SDK decides when to call itStrands is also battle-tested inside AWS — it powers Amazon Q Developer, AWS Glue, and the VPC Reachability Analyzer. None of this locks you into Strands; the wrapper pattern means you can start on Strands and move to LangGraph later by changing the body. But when there's no strong reason to pick another framework, Strands gives you the tightest integration with Memory, Gateway, and A2A for the least glue code.
Example
Model-as-config, not model-as-rewrite
Strands keeps the model inside your agent code. Bedrock models go through BedrockModel(model_id=..., region_name=..., temperature=...); non-Bedrock models use provider classes (OpenAIModel, AnthropicModel, OllamaModel, LiteLLMModel). Swapping providers is a one-line change to the model object — the entrypoint and deploy flow are untouched. Model IDs are volatile; pin a current one and verify it against the live model catalog.
The canonical reference and the one deploy flow
When the snippets above leave you unsure — especially for LlamaIndex, where the exact code is unverified — go to the canonical source: the samples repo at 03-integrations/agentic-frameworks/. It has a folder per framework:
03-integrations/agentic-frameworks/
├── adk/
├── autogen/
├── claude-agent/
├── crewai/
├── java_adk/
├── langchain/
├── langgraph/
├── llamaindex/ <- pull the exact LlamaIndex wrapper from here
├── openai-agents/
├── pydanticai-agents/
├── strands-agents/
└── typescript_mastra/Each folder is a complete, runnable wrapper. The presence of autogen, pydanticai-agents, claude-agent, and typescript_mastra underscores the point: the pattern extends well beyond the six headline frameworks.
Once the body is written, the deploy flow is the same for all of them. Using the Starter Toolkit CLI:
# 1. Configure: generates .bedrock_agentcore.yaml + Dockerfile
agentcore configure -e my_agent.py
# 2. Launch (a.k.a. deploy): builds an ARM64 image via CodeBuild (no local Docker),
# pushes to ECR, creates the Runtime, wires CloudWatch — prints the agent runtime ARN
agentcore launch
# 3. Invoke
agentcore invoke '{"prompt": "What is the weather?"}'Or from Python with the Starter Toolkit Runtime object:
from bedrock_agentcore_starter_toolkit import Runtime
runtime = Runtime()
runtime.configure(entrypoint="my_agent.py", auto_create_execution_role=True,
auto_create_ecr=True, requirements_file="requirements.txt")
runtime.launch() # the Python method is always .launch()
runtime.invoke({"prompt": "Hi"})And you can always test the exact cloud contract locally before deploying:
python my_agent.py # or: agentcore launch --local
curl -X POST http://localhost:8080/invocations \
-H 'Content-Type: application/json' \
-d '{"prompt":"What is the weather?"}'Watch out
Two `agentcore` binaries, different verbs
There are two CLIs that both ship an agentcore binary. The legacy Starter Toolkit uses configure / launch / invoke; the newer agentcore-cli uses create / dev / deploy / invoke. The toolkit also renamed launch→deploy in v0.3.x (same operation). The Python Runtime method is always .launch(). Pin which CLI a tutorial assumes — copy-pasting verbs across the two fails.
Watch out
ARM64 is mandatory
Runtime runs on AWS Graviton (ARM64). The CLIs and CodeBuild handle the architecture automatically, but a hand-built amd64 image silently fails to start. If you build your own container, it must be linux/arm64 (docker buildx build --platform linux/arm64). This is framework-independent and one of the most common deploy failures.
Try it: One agent, three wrappers, one deploy flow
Goal: prove framework-agnosticism with your own hands by hosting the same prompt-answering agent under three different frameworks, changing only the body of the entrypoint.
- Set up. Create a fresh project on Python 3.11+ (so CrewAI works later), and
pip install bedrock-agentcore bedrock-agentcore-starter-toolkit. Write a minimal Strands agent inagent_strands.pyusing the canonical scaffold:from bedrock_agentcore.runtime import BedrockAgentCoreApp,app = BedrockAgentCoreApp(), anAgent(), an@app.entrypoint def invoke(payload)that doesresult = agent(payload.get("prompt", "Hello"))and returns{"result": result.message}, andapp.run(). - Test the contract locally. Run
python agent_strands.py, then in another terminal:curl -X POST http://localhost:8080/invocations -H 'Content-Type: application/json' -d '{"prompt":"Name three uses for a paperclip"}'. Confirm you get a{"result": ...}JSON back. (If you hitAccessDeniedException, enable Bedrock model access for your model/region.) - Swap the framework — keep the scaffold. Copy the file to
agent_langgraph.py. Change ONLY the three variant lines: build a graph with aChatBedrockConversemodel (pip install langchain-aws), callgraph.invoke({"messages": [("user", prompt)]}), and return{"result": out["messages"][-1].content}. Re-run the same curl. Then do it a third time inagent_openai.pyusing the OpenAI Agents SDK — note you must make the entrypointasync defandawait Runner.run(agent, prompt), returningresult.final_output. - Diff your wrappers. Run
diff agent_strands.py agent_langgraph.py. Verify the only differences are the build line, the call, and the unwrap — the@app.entrypoint,payload.get,{"result": ...}, andapp.run()lines are identical. - Deploy one of them. Pick a wrapper and run
agentcore configure -e agent_langgraph.py, thenagentcore launch, thenagentcore invoke '{"prompt":"Name three uses for a paperclip"}'. Confirm the build produces an ARM64 image and the invoke returns the same{"result": ...}shape you saw locally. - Reflect (three sentences). Which lines did you actually have to change to switch frameworks? Which framework needed an async entrypoint, and why? If you next wanted LlamaIndex, where exactly would you look for the correct build/call/pull snippet rather than guessing?
Key takeaways
- 1AgentCore Runtime hosts a container that speaks a protocol, not a framework. `@app.entrypoint` is the seam between AWS's managed HTTP server and your agent object, and the body inside it is the only thing that changes between frameworks.
- 2The invariant in every wrapper: `@app.entrypoint`, `payload.get("prompt")`, `return {"result": ...}`, `app.run()`, and the `configure`→`launch`→`invoke` deploy flow.
- 3The variant is three lines: build the object, call it inside the entrypoint, pull the result — Strands `agent()`/`result.message`; LangGraph `graph.invoke`/`out["messages"][-1].content`; CrewAI `crew.kickoff`/`result.raw`; OpenAI `Runner.run`/`result.final_output`; ADK `runner.run_async`/`event.content.parts[0].text`; LlamaIndex (pull the exact snippet from samples).
- 4The OpenAI Agents SDK and Google ADK are async — use an `async def` entrypoint or wrap the call in `asyncio.run(...)`. The deploy flow is unchanged.
- 5Per-framework gotchas: CrewAI needs Python 3.11+ (`chroma-hnswlib`); LangGraph reaches Bedrock via `ChatBedrockConverse` from `langchain-aws`; the LlamaIndex snippet is unverified — copy it from the samples.
- 6Strands is the AWS-native, recommended SDK: Apache-2.0, model-driven, `@tool` decorator, `strands-agents-tools`, MCP integration, and native AgentCore hooks (`AgentCoreMemorySessionManager`, `StrandsA2AExecutor`).
- 7The canonical reference for every wrapper is the samples repo at `03-integrations/agentic-frameworks/`, with a runnable folder per framework.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.In the six-wrapper pattern, which of these is part of the *variant* — the code that changes from one framework to the next?
2.You wrap a Google ADK agent in a plain synchronous `def invoke(payload):` entrypoint and call `runner.run_async(...)` directly. What is the problem?
3.Your CrewAI agent builds and runs fine locally on Python 3.10 but fails during `agentcore launch` on a native-wheel error before any AgentCore code runs. What is the most likely cause?
4.Why does AgentCore Runtime describe itself as framework-agnostic?
Go deeper
Hand-picked sources to keep learning
The authoritative (and volatile) list of supported frameworks and models, plus the wrapping pattern. Verify the framework list here before relying on it.
Canonical, runnable wrapper per framework (adk, crewai, langgraph, llamaindex, openai-agents, strands-agents, and more). Pull the exact LlamaIndex snippet from here.
The AWS-native SDK: model-driven loop, @tool decorator, strands-agents-tools, MCP integration, AgentCore Memory/A2A hooks.
Why Strands is Apache-2.0 and model-driven, and where AWS runs it in production (Amazon Q Developer, AWS Glue, VPC Reachability Analyzer).
The container-that-speaks-a-protocol model, InvokeAgentRuntime, session isolation, and the service contract the entrypoint implements.
Model IDs and inference-profile prefixes change between releases — always pin a current ID against this live catalog (the default Strands model is volatile).