Agentic AI AcademyAgentic AI Academy

Framework- and Model-Agnostic by Design

Why a container that speaks a protocol changes everything

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

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.

  1. 1The container speaks a protocol, not a framework
  2. 2@app.entrypoint: the seam between AWS and your agent
  3. 3Same agent, six wrappers: the invariant vs the variant
  4. 4Model-agnostic: the model is a config value in your code
  5. 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:

  1. Framework-agnostic — Runtime hosts a container that speaks a known protocol, not a framework. The framework lives inside the container.
  2. 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:

ProtocolPortMount pathFormat
HTTP8080/invocations, /wsREST JSON / SSE / WebSocket
MCP8000/mcpJSON-RPC (streamable-http)
A2A9000/ (root)JSON-RPC 2.0, Agent Cards
AG-UI8080/invocations (SSE), /wsEvent 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:

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

Read it from the boundary inward:

  • Above the seam (AWS owns it): app = BedrockAgentCoreApp() and app.run(). A POST /invocations arrives, the SDK deserializes the body to payload, and routes GET /ping health checks on its own. You wrote none of that.
  • At the seam: @app.entrypoint marks the one function AWS calls per request. The function signature is the interface: it receives the request payload and 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:

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

python
@app.entrypoint
async def handler(request: dict):
    agent = Agent()
    async for event in agent.stream_async(request.get("prompt", "")):
        yield event

Tip

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:

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)

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.

python
# 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()                                  # <- identical

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

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

ProviderClassImport
Amazon BedrockBedrockModelstrands.models.bedrock
OpenAIOpenAIModelstrands.models.openai
Anthropic API (direct)AnthropicModelstrands.models.anthropic
Ollama (local)OllamaModelstrands.models.ollama
LiteLLM (100+ incl. Gemini)LiteLLMModelstrands.models.litellm
Customyour subclass
python
# 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.

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

  1. Scaffold the four lines. Create agent.py with the minimal Strands agent: from bedrock_agentcore.runtime import BedrockAgentCoreApp, app = BedrockAgentCoreApp(), an @app.entrypoint def invoke(payload): that calls agent(payload.get("prompt", "Hello")) and returns {"result": result.message}, and app.run() under __main__.
  2. 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. Also curl http://localhost:8080/ping and observe the health response — note you wrote none of that routing.
  3. Swap the model, keep everything else. Change only the model construction: build a BedrockModel(model_id=..., region_name="us-east-1") and pass it as Agent(model=model). Verify the same curl still works. If you get AccessDeniedException, 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.)
  4. 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, and app.run() byte-for-byte identical — change only how you build the graph, call graph.invoke(...), and read out["messages"][-1].content. Run the same curl against it.
  5. 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.'
  6. 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

  1. 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. 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()`.
  3. 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.
  4. 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.
  5. 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).
  6. 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).
  7. 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