Framework- and Model-Agnostic by Design
Why a container that speaks a protocol changes everything
- Explain why AgentCore Runtime is framework-agnostic: it hosts a protocol-speaking container, and your framework lives inside it
- Identify `@app.entrypoint` as the seam between AWS's managed HTTP server and your framework's agent object
- Name the AWS-supported frameworks (Strands first-class, plus LangGraph/LangChain, CrewAI, LlamaIndex, Google ADK, OpenAI Agents SDK, and custom)
- Configure the model as code — Bedrock, OpenAI, Gemini, Anthropic API, or local — without AgentCore ever inspecting it
- Describe the portability payoff: no rewrite to switch frameworks or models, and à-la-carte adoption of individual services
AgentCore Runtime hosts a container that speaks a known wire protocol (HTTP, MCP, A2A, or AG-UI) — not a framework. Your agent framework lives INSIDE that container, and `@app.entrypoint` is the seam joining AWS's managed HTTP server to your agent object. Because the model is just a config value inside your code, you can switch frameworks or model providers without rewriting your deployment, and adopt AgentCore services à la carte.
- 1The container speaks a protocol, not a framework
- 2@app.entrypoint: the seam between AWS and your agent
- 3Same agent, six wrappers: the invariant vs the variant
- 4Model-agnostic: the model is a config value in your code
- 5Why it matters: portability, no rewrites, à-la-carte adoption
The container speaks a protocol, not a framework
Most platforms ask you to adopt their abstraction: their agent class, their orchestration loop, their plugin system. AgentCore Runtime makes a different bet. It hosts a container that speaks a known wire protocol — and it never looks inside to see which framework you used to build the thing answering requests.
This is the conceptual backbone of the whole platform. AWS states two foundational properties:
- Framework-agnostic — Runtime hosts a container that speaks a known protocol, not a framework. The framework lives inside the container.
- Model-agnostic — the model is just a config value inside your agent code (Bedrock, OpenAI, Gemini, Anthropic API, etc.); AgentCore never inspects it.
Concretely: you write an agent in any framework (or none), wrap it so it exposes a small standardized contract, package it as an ARM64 container (or a direct code zip), and hand it to AWS. AWS runs it serverlessly — each user session in its own dedicated microVM — and invokes it through the InvokeAgentRuntime data-plane API.
The contract is one of four protocols. Your runtime declares one protocol mode, and AWS routes the container port accordingly:
| Protocol | Port | Mount path | Format |
|---|---|---|---|
| HTTP | 8080 | /invocations, /ws | REST JSON / SSE / WebSocket |
| MCP | 8000 | /mcp | JSON-RPC (streamable-http) |
| A2A | 9000 | / (root) | JSON-RPC 2.0, Agent Cards |
| AG-UI | 8080 | /invocations (SSE), /ws | Event streams (SSE/WebSocket) |
The point of the table is not to memorize ports — it is to see the architecture. AWS owns the outside of the box: the ports, the health checks, the session headers, the routing. You own the inside: whatever code answers on that port. As long as your container honors the contract, AWS does not care whether the answer came from Strands, LangGraph, a hand-rolled if statement, or a model you host yourself.
Key insight
Why this is the whole platform in one idea
The prototype-to-production gap is real: building an agent demo on a laptop is easy; operating it (session isolation, identity, memory, observability) is hard. AgentCore's answer is to standardize the boundary — a protocol contract — instead of the implementation. Standardize the seam and you can manage the operational concerns generically, for any framework, forever.
@app.entrypoint: the seam between AWS and your agent
If the protocol is the contract, the @app.entrypoint decorator is the seam where AWS's managed HTTP server hands a request to your framework's agent object — and hands your reply back. It is the single most important line to understand in this lesson.
The Python SDK (package bedrock-agentcore) ships BedrockAgentCoreApp, a thin Starlette/Uvicorn ASGI wrapper. It turns a function into the full HTTP service contract: it wires /invocations, /ping, optionally /ws, the session headers, streaming, and async task tracking — all the protocol plumbing from the table above. It is framework-agnostic by construction; it does not know or care what runs inside your entrypoint.
Here is the minimal agent. Note the "four lines to deploy" — import, construct, decorate, run:
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
app = BedrockAgentCoreApp() # 2. the managed HTTP server
agent = Agent() # your framework's agent object
@app.entrypoint # 3. the SEAM
def invoke(payload):
result = agent(payload.get("prompt", "Hello"))
return {"result": result.message}
if __name__ == "__main__":
app.run() # 4. local http://localhost:8080; in container binds 0.0.0.0:8080Read it from the boundary inward:
- Above the seam (AWS owns it):
app = BedrockAgentCoreApp()andapp.run(). APOST /invocationsarrives, the SDK deserializes the body topayload, and routesGET /pinghealth checks on its own. You wrote none of that. - At the seam:
@app.entrypointmarks the one function AWS calls per request. The function signature is the interface: it receives the requestpayloadand returns a JSON-serializable result. - Below the seam (you own it):
agent(payload.get("prompt", "Hello")). This body is the only part that changes between frameworks.
Because BedrockAgentCoreApp reproduces the exact cloud contract locally, you can test the wire protocol on your laptop before deploying:
curl -X POST http://localhost:8080/invocations \
-H 'Content-Type: application/json' \
-d '{"prompt":"What is the weather?"}'For streaming, an async generator entrypoint auto-serves text/event-stream; each yield becomes one SSE data: event — same seam, different shape:
@app.entrypoint
async def handler(request: dict):
agent = Agent()
async for event in agent.stream_async(request.get("prompt", "")):
yield eventTip
The import path has two spellings
Canonical is from bedrock_agentcore.runtime import BedrockAgentCoreApp. Some examples use the shorter from bedrock_agentcore import BedrockAgentCoreApp. Both work; prefer the explicit .runtime path in your own code.
Watch out
Don't block the entrypoint
A blocking call inside @app.entrypoint starves the /ping health thread, so the session can be killed mid-task. For long work, offload to threads or async, and (for background tasks) use @app.async_task so /ping reports HealthyBusy. We cover this in the long-running lesson — for now, just know the seam must stay responsive.
Same agent, six wrappers: the invariant vs the variant
Framework-agnostic is not marketing — it is a concrete, testable claim: only the body inside the entrypoint changes between frameworks. AWS names a specific set of supported frameworks, with Strands Agents first-class/recommended, plus LangGraph/LangChain, CrewAI, LlamaIndex, Google ADK, OpenAI Agents SDK, and your own custom code.
Each framework differs in exactly three places: how you build the agent object, how you call it inside the entrypoint, and how you pull the result out:
| 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) |
Line up the LangGraph version against the Strands version from the previous section and the invariant pops out: @app.entrypoint, payload.get("prompt"), returning {"result": ...}, app.run(), and the entire deploy flow are identical. Only the three highlighted columns move.
# LangGraph — same skeleton, different body
from bedrock_agentcore.runtime import BedrockAgentCoreApp
app = BedrockAgentCoreApp()
graph = builder.compile() # <- build differs
@app.entrypoint
def invoke(payload):
out = graph.invoke({"messages": [("user", payload.get("prompt", ""))]}) # <- call differs
return {"result": out["messages"][-1].content} # <- pull differs
if __name__ == "__main__":
app.run() # <- identicalA couple of per-framework notes worth carrying forward: OpenAI Agents SDK and Google ADK are async, so make the entrypoint async (or wrap the call in asyncio.run(...)). CrewAI's sample needs Python 3.11+ and defines agents/tasks in config/agents.yaml / config/tasks.yaml. The AWS samples repo keeps reference folders for all of these under 03-integrations/agentic-frameworks/ (including autogen, claude-agent, pydanticai-agents, and a TypeScript mastra sample).
Note
The exact LlamaIndex snippet is unconfirmed
The LlamaIndex row shows the typical shape (engine.query(prompt) → str(response)), but the precise entrypoint snippet was not verified against first-party docs at research time. If you build the LlamaIndex variant, pull the exact code from the AWS samples repo rather than copying the table verbatim.
Example
Strands is AWS-native, not mandatory
Strands Agents is an Apache-2.0, model-driven SDK (define model + tools + prompt; the SDK runs the agentic loop). Its @tool decorator turns any Python function into a tool, strands-agents-tools ships 20+ prebuilt tools, and it has native AgentCore hooks like AgentCoreMemorySessionManager and StrandsA2AExecutor. It is the smoothest path — but the platform works identically with LangGraph or CrewAI.
Model-agnostic: the model is a config value in your code
The second backbone property is the mirror image of the first. Just as AgentCore never inspects your framework, it never inspects your model. The model is configured inside your agent code — AgentCore Runtime has no model field, no provider dropdown, no API surface that says "which LLM." It hosts a container; the container decides.
With Strands, a Bedrock model is one constructor:
from strands import Agent
from strands.models.bedrock import BedrockModel
model = BedrockModel(
model_id="amazon.nova-pro-v1:0", # or a Claude / cross-region inference profile
region_name="us-east-1",
temperature=0.3,
)
agent = Agent(model=model)Bedrock model IDs can be a plain ID (amazon.nova-pro-v1:0) or a cross-region inference profile prefixed us. / global.. Switching to a non-Bedrock provider means swapping the provider class — nothing about the entrypoint, the container, or the deployment changes:
| Provider | Class | Import |
|---|---|---|
| Amazon Bedrock | BedrockModel | strands.models.bedrock |
| OpenAI | OpenAIModel | strands.models.openai |
| Anthropic API (direct) | AnthropicModel | strands.models.anthropic |
| Ollama (local) | OllamaModel | strands.models.ollama |
| LiteLLM (100+ incl. Gemini) | LiteLLMModel | strands.models.litellm |
| Custom | your subclass | — |
# Same agent, OpenAI instead of Bedrock — one line moves
from strands.models.openai import OpenAIModel
model = OpenAIModel(model_id="gpt-4o") # API key injected at runtime, not baked in
agent = Agent(model=model)Where do the credentials for a non-Bedrock provider come from? Not baked into the image. AgentCore Identity injects them at call time from a managed token vault using decorators like @requires_api_key(provider_name=..., into="api_key"); keys are stored in AWS Secrets Manager via CreateApiKeyCredentialProvider. (Deep dive comes in the Identity module — for now, just know secrets live outside the container.)
Watch out
Model IDs are volatile — pin a current one and verify live
Model IDs change between releases — this is true for every provider, so treat any specific ID (Bedrock amazon.nova-pro-v1:0, an OpenAI ID like gpt-4o, etc.) as a moving target. As of writing the default Strands model is reported as global.anthropic.claude-sonnet-4-5-20250929-v1:0, and claude-sonnet-4-20250514 is now legacy — but verify against the live model catalog before pinning. A first-run AccessDeniedException almost always means Bedrock model access isn't enabled for that model/region in your account, not a code bug.
Why it matters: portability, no rewrites, à-la-carte adoption
Decoupling the operational platform from the framework and the model buys you three things that compound in production.
1. Portability. Your agent is a container that honors a contract. The same artifact runs locally under app.run(), in CI behind the curl against /invocations, and in AWS behind InvokeAgentRuntime — without code changes at the seam. You are not locked into a vendor's agent abstraction; you are locked into a protocol, which is a far smaller, more portable commitment.
2. No rewrite to switch frameworks or models. This is the practical payoff of "same agent, six wrappers" and "model as config." Migrating from LangGraph to Strands touches the three columns inside the entrypoint, not the deployment. Swapping Claude for an OpenAI or Gemini model touches one provider line. Your session isolation, identity, memory, and observability wiring — the expensive parts — stay put. Compare this to classic, fully-managed Amazon Bedrock Agents, which ties you to Bedrock-hosted models and AWS's own orchestration loop. AgentCore is the modular, code-first, framework-agnostic alternative; the two coexist rather than one deprecating the other.
3. À-la-carte adoption. AgentCore services are modular and "work together or independently." You do not have to adopt the whole platform. You can run your agent on EKS, Lambda, or ECS and still use only Gateway (expose tools as MCP), only Code Interpreter (a managed sandbox), or only Memory (a managed store). Each service has its own SDK/API surface and is independently billable.
Adopt the whole platform ...or pick services à la carte
┌──────────────────────────┐ ┌──────────────────────────┐
│ AgentCore Runtime │ │ Your agent on EKS/Lambda │
│ (your container) │ │ │ │
│ + Memory + Gateway │ │ calls only ── Gateway │
│ + Identity + Observ. │ │ calls only ── Memory │
└──────────────────────────┘ └──────────────────────────┘The through-line: by standardizing the boundary (a protocol contract + the @app.entrypoint seam) instead of the implementation (a framework or a model), AgentCore lets your agent logic stay yours while AWS handles the operational heavy lifting — and lets you change your mind about frameworks, models, and which services to use, without a migration project.
Key insight
The decision rule for this lesson
When you read later lessons about Gateway, Memory, or Identity, keep asking: "Does this assume a specific framework or model?" The answer is almost always no — and that is by design. The platform meets your agent at the protocol boundary, never inside it.
Try it: Prove framework- and model-agnostic with one entrypoint
Goal: feel the seam directly — build one agent, run it against the local HTTP contract, then swap the framework body and the model with zero changes above the seam.
- Scaffold the four lines. Create
agent.pywith the minimal Strands agent:from bedrock_agentcore.runtime import BedrockAgentCoreApp,app = BedrockAgentCoreApp(), an@app.entrypoint def invoke(payload):that callsagent(payload.get("prompt", "Hello"))and returns{"result": result.message}, andapp.run()under__main__. - Hit the contract locally. Run
python agent.py, then in another terminal POST to the exact cloud contract:curl -X POST http://localhost:8080/invocations -H 'Content-Type: application/json' -d '{"prompt":"What is the weather?"}'. Confirm you get JSON back. Alsocurl http://localhost:8080/pingand observe the health response — note you wrote none of that routing. - Swap the model, keep everything else. Change only the model construction: build a
BedrockModel(model_id=..., region_name="us-east-1")and pass it asAgent(model=model). Verify the same curl still works. If you getAccessDeniedException, enable model access for that model/region in the Bedrock console — that's the gotcha, not a code bug. (Use a current model ID from the live catalog; do not trust a hard-coded snapshot.) - Swap the framework, keep the skeleton. From the AWS samples repo, copy the LangGraph entrypoint body into a second file
agent_langgraph.py. Keep the import,app = BedrockAgentCoreApp(),@app.entrypoint,payload.get("prompt"), the{"result": ...}envelope, andapp.run()byte-for-byte identical — change only how you build the graph, callgraph.invoke(...), and readout["messages"][-1].content. Run the same curl against it. - Diff the two files. Run
diff agent.py agent_langgraph.py. Confirm the only differences are inside the entrypoint body (the build/call/pull lines). That diff is the entire meaning of 'framework-agnostic.' - Reflect (3 sentences). Which lines were truly invariant across both frameworks and both models? Where would a provider API key come from in production if you switched to OpenAI (hint: not the image)? When would à-la-carte adoption — using only Gateway or Memory while hosting your agent elsewhere — be the right call?
Key takeaways
- 1AgentCore Runtime hosts a container that speaks a known wire protocol (HTTP/MCP/A2A/AG-UI), NOT a framework. Your framework lives inside the container; AWS owns the ports, health checks, and routing, and never inspects what runs within.
- 2`@app.entrypoint` (from `BedrockAgentCoreApp` in the `bedrock-agentcore` SDK) is the seam between AWS's managed HTTP server and your agent object. The four lines to deploy: import, `app = BedrockAgentCoreApp()`, `@app.entrypoint`, `app.run()`.
- 3Only the body inside the entrypoint changes between frameworks. AWS supports Strands (first-class), LangGraph/LangChain, CrewAI, LlamaIndex, Google ADK, OpenAI Agents SDK, and custom code — differing only in how you build, call, and read the agent.
- 4Model-agnostic means the model is a config value in your code: `BedrockModel` for Bedrock, or Strands provider classes (`OpenAIModel`, `AnthropicModel`, `OllamaModel`, `LiteLLMModel`) for OpenAI/Anthropic/local/Gemini. AgentCore never inspects the model.
- 5Provider credentials are NOT baked into the image — AgentCore Identity injects them at call time from a token vault (`@requires_api_key`, secrets in AWS Secrets Manager).
- 6The payoff: portability (same container local/CI/cloud), no rewrite to switch frameworks or models, and à-la-carte adoption (use only Gateway/Memory/Code Interpreter while running your agent on EKS/Lambda/ECS).
- 7Model IDs are volatile — pin a current ID and verify against the live Bedrock model catalog; a first-run `AccessDeniedException` usually means model access isn't enabled for that model/region.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.What does AgentCore Runtime actually host, and what consequence follows?
2.In a `BedrockAgentCoreApp` agent, what is the precise role of `@app.entrypoint`?
3.You're porting an agent from Strands to LangGraph on AgentCore. What has to change?
4.Which statement about AgentCore's model-agnostic design is correct?
Go deeper
Hand-picked sources to keep learning
The first-party page on framework-agnostic design and the supported framework list. Verify the live framework and model lists here.
The exact wire contract your container must honor: HTTP/MCP/A2A/AG-UI ports, mount paths, formats, and auth.
Runnable entrypoint examples per framework (strands-agents, langgraph, crewai, openai-agents, adk, llamaindex, and more) — pull exact snippets from here.
Source for `BedrockAgentCoreApp`, `@app.entrypoint`, streaming, and `serve_a2a`. SDK versions are volatile — check the README/PyPI.
The AWS-native, first-class framework: `Agent`, `@tool`, `BedrockModel` and the non-Bedrock provider classes (OpenAIModel, AnthropicModel, OllamaModel, LiteLLMModel).
The live model catalog. Model IDs are volatile — confirm a current ID here rather than trusting a snapshot, and enable model access per model/region to avoid AccessDeniedException.