Agentic AI AcademyAgentic AI Academy

Wrapping Any Framework

The same agent across Strands, LangGraph, CrewAI, and more

Intermediate 14 minBuilder
What you'll be able to do
  • 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
At a glance

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.

  1. 1The seam: a container that speaks a protocol, not a framework
  2. 2The invariant scaffold vs the variant body
  3. 3The synchronous wrappers: Strands, LangGraph, CrewAI
  4. 4The async wrappers: OpenAI Agents SDK and Google ADK
  5. 5Why Strands is the AWS-native default
  6. 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.

python
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:8080

AWS 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:

  1. @app.entrypoint — the decorator that registers your function as the agent handler.
  2. payload.get("prompt") — how you read the caller's input out of the request dict.
  3. return {"result": ...} — the shape AWS serializes back to the caller.
  4. app.run() — boots the ASGI server (binds 0.0.0.0:8080 in the container, serves /invocations + /ping).
  5. The deploy flowagentcore configure -e my_agent.pyagentcore 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:

FrameworkBuild objectCall inside entrypointPull result
Strandsagent = Agent(tools=[...])agent(prompt)result.message
LangGraphgraph = builder.compile()graph.invoke({"messages": [...]})out["messages"][-1].content
CrewAIcrew = AgentcoreCrewAi().crew()crew.kickoff(inputs=...)result.raw
OpenAI Agents SDKagent = Agent(...)await Runner.run(agent, query)result.final_output
Google ADKroot_agent = Agent(model=...)runner.run_async(...)event.content.parts[0].text
LlamaIndexquery engine / agentengine.query(prompt) (typical)str(response)

Notice the LlamaIndex row is hedged. The exact build/call/pull snippet for LlamaIndex is not verified from the docsengine.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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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 @tool decorator turns any Python function into a tool the agent can call.
  • strands-agents-tools ships 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 hooksAgentCoreMemorySessionManager persists conversation to AgentCore Memory, and StrandsA2AExecutor provides 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:

python
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 it

Strands 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:

text
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:

bash
# 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:

python
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:

bash
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 launchdeploy 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.

  1. 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 in agent_strands.py using the canonical scaffold: from bedrock_agentcore.runtime import BedrockAgentCoreApp, app = BedrockAgentCoreApp(), an Agent(), an @app.entrypoint def invoke(payload) that does result = agent(payload.get("prompt", "Hello")) and returns {"result": result.message}, and app.run().
  2. 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 hit AccessDeniedException, enable Bedrock model access for your model/region.)
  3. Swap the framework — keep the scaffold. Copy the file to agent_langgraph.py. Change ONLY the three variant lines: build a graph with a ChatBedrockConverse model (pip install langchain-aws), call graph.invoke({"messages": [("user", prompt)]}), and return {"result": out["messages"][-1].content}. Re-run the same curl. Then do it a third time in agent_openai.py using the OpenAI Agents SDK — note you must make the entrypoint async def and await Runner.run(agent, prompt), returning result.final_output.
  4. 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": ...}, and app.run() lines are identical.
  5. Deploy one of them. Pick a wrapper and run agentcore configure -e agent_langgraph.py, then agentcore launch, then agentcore 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.
  6. 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

  1. 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.
  2. 2The invariant in every wrapper: `@app.entrypoint`, `payload.get("prompt")`, `return {"result": ...}`, `app.run()`, and the `configure`→`launch`→`invoke` deploy flow.
  3. 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).
  4. 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.
  5. 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.
  6. 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`).
  7. 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