AgentCore Runtime, deployed
Chapter 16 decoded AgentCore Runtime as
"sessions as a service" and showed the entrypoint shape. This chapter
deploys it: the exact container contract, the SDK app that generates it,
the control-plane and data-plane APIs, the agentcore CLI that
provisions the whole thing, the IAM execution role, and the Part 1 agent
running in a managed microVM. This is the wire-level "deploy the agent
for real" the earlier chapter deferred. Cloud steps are follow-along
(no AWS account here), but Lab 12.2 makes the core contract genuinely
runnable on your laptop.
The contract, and Lab 12.2
A custom AgentCore Runtime agent is a container that answers two HTTP endpoints on port 8080:
GET /ping, a health check the platform polls; return 200 with a status body.POST /invocations, the request path; receive the payload, run one task, return the response (JSON, or a stream for streaming agents).
That is the entire surface. Lab 12.2 implements it with a real stdlib
HTTP server and runs the Part 1 agent behind
/invocations, so you can see the contract meet an actual agent:
python3 runtime_contract.py
contract server up on http://127.0.0.1:60583 (port 8080 in a real container)
GET /ping -> 200 {"status": "Healthy"}
POST /invocations -> 200
session : sess-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa
answer : Likely cause: deploy v841 at 01:55 shrank the payments-db connec...
Two things to notice. The session id arrives as a header the
platform sets per session (the lab uses the real header name), which is
how a stateless container participates in AgentCore's stateful session
model, your code reads the id and keys its own state by it, exactly the
event-store pattern. And the container runs
your loop unchanged: the lab's handler calls the Part 1 run_agent,
demonstrating the runtime chapter's
"bring-your-own-loop" promise at the wire level. There is no framework
requirement; there is an HTTP contract.
The SDK writes the server for you
You would not hand-write that server in production; the Python SDK's
BedrockAgentCoreApp generates it from a decorator:
# agent_app.py, the real shape
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from loop_agent import TOOLS, run_agent
from mantle_model import MantleModel # Part 2's Bedrock client, ~15 lines
app = BedrockAgentCoreApp()
model = MantleModel()
@app.entrypoint
def invoke(payload):
return {"answer": run_agent(model=model, tools=TOOLS,
task=payload["prompt"])}
app.run() # serves /ping + /invocations on 8080, exactly Lab 12.2
@app.entrypoint wires your function to POST /invocations and
provides /ping; app.run() starts the server. For a streaming
agent, the entrypoint returns a generator and the SDK emits the response
as a stream, the progress-UX path Part 1 wanted. The
decorator is the only AgentCore-specific line in an otherwise ordinary
agent, which is the whole design: the platform meets your loop at one
seam.
The container: ARM64, and the build
The image must be linux/arm64 (AgentCore Runtime runs on Graviton),
pushed to ECR. You can build it by hand, but the agentcore CLI (the
starter toolkit) does the ARM build via CodeBuild so you do not need an
ARM machine locally:
agentcore configure --entrypoint agent_app.py # writes config, picks a role
agentcore launch # ARM build -> ECR -> deploy
agentcore invoke '{"prompt": "why did checkout spike?"}' # test it
configure records the entrypoint, the container settings, and the IAM
execution role (creating one if you let it). launch runs a CodeBuild
ARM64 build, pushes to ECR, and calls the control plane to create the
runtime. invoke calls the data plane with a test payload. Three
commands from a Python file to a running managed agent, which is the
reason most teams start here rather than building the
DIY runtime.
The control plane and data plane
Under the CLI are two API surfaces. The control plane
(bedrock-agentcore-control) manages the runtime resource:
# illustrative CreateAgentRuntime
client.create_agent_runtime(
agentRuntimeName="checkout-investigator",
agentRuntimeArtifact={"containerConfiguration": {
"containerUri": "ACCOUNT.dkr.ecr.us-east-1.amazonaws.com/hive-agent:latest"}},
roleArn=EXECUTION_ROLE,
networkConfiguration={"networkMode": "PUBLIC"},
protocolConfiguration={"serverProtocol": "HTTP"})
# create_agent_runtime_endpoint(...) then exposes an invocable endpoint
The data plane (bedrock-agentcore) invokes it, and the session id
is the load-bearing parameter:
# illustrative InvokeAgentRuntime
resp = client.invoke_agent_runtime(
agentRuntimeArn=RUNTIME_ARN,
runtimeSessionId="sess-" + user_session, # >= 33 chars; same id -> same session
payload=json.dumps({"prompt": task}),
contentType="application/json", accept="application/json")
Reusing a runtimeSessionId lands in the same warm session (same
microVM, retained in-process state) until it idles out or hits the
8-hour ceiling; a new id gets a fresh isolated microVM. This is the
runtime chapter's session model as an API
parameter: your product's per-user session maps one-to-one to a
runtimeSessionId, and the platform handles isolation and lifecycle.
The IAM execution role
The runtime assumes an execution role, and getting its permissions right
is where the first deploy usually stalls. The role's trust policy lets
bedrock-agentcore assume it; its permission policy grants exactly what
the agent needs and nothing more, the least-privilege
principle at the container level:
{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": ["arn:aws:bedrock:*::foundation-model/anthropic.claude-opus-4-8"]},
{"Effect": "Allow", "Action": ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"],
"Resource": "arn:aws:ecr:us-east-1:ACCOUNT:repository/hive-agent"},
{"Effect": "Allow", "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:*:ACCOUNT:log-group:/aws/bedrock-agentcore/*"}
]
}
Add the specific tools' permissions (a query_metrics that reads
CloudWatch needs cloudwatch:GetMetricData) and nothing else. When the
agent acts for a user, this role is the
platform's identity; the user's authority comes through
Identity, the next chapter's subject,
so the execution role stays narrow and the user-scoped power arrives
separately.
The pricing that changes the design
Chapter 16 flagged it; here it is as the number that decides architecture. Runtime bills per second, with CPU charged only while your code is actively consuming it and memory charged for the session's lifetime. An agent session spends most of its wall clock waiting, on the model, on tools, on humans, and during that wait the CPU meter stops. So a session you keep alive all day for a user costs memory-pennies plus CPU only for the seconds it actually computes, which makes the copilot-mesh shape (many long-idle interactive sessions) economical here in a way it is not on always-on Fargate. The fleet economics gain a column: bursty long-idle sessions favor Runtime; saturated never-idle overnight fleets still favor Fargate's flat rate.
Don't be confused: the session id vs the agent runtime. The agent runtime is the deployed resource (one ARN, your container, created once). A session is one live conversation inside it, keyed by
runtimeSessionId, created and destroyed constantly. One runtime serves thousands of concurrent sessions, each in its own microVM. Conflating them, creating a new runtime per user instead of a new session, is the AgentCore version of the Managed-Agents anti-pattern: you accumulate orphaned runtime resources and pay deploy latency per user, when the runtime is the once-created host and the session is the per-user thing.
Full source
"""Lab 12.2: the AgentCore Runtime container contract, locally.
To run your agent on Bedrock AgentCore Runtime you ship a container that
answers a tiny HTTP contract on port 8080:
GET /ping -> a health check; return 200 with a status body.
POST /invocations -> receive the request payload, run one task, return
the response (JSON, or a stream for streaming
agents).
That is the whole surface. Everything else, session isolation in a
microVM, scaling, the 8-hour session lifetime, is the platform's job;
yours is to satisfy these two endpoints. This lab implements the
contract with a real stdlib HTTP server, runs the Part 1 agent behind
POST /invocations, and does a localhost round-trip so you can see the
exact shape a real container must meet.
The production version wraps this in the SDK's BedrockAgentCoreApp
(the @app.entrypoint decorator generates this same server); building it
by hand once shows there is no magic. Standard library only.
Deterministic.
"""
from __future__ import annotations
import contextlib
import io
import json
import threading
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
from loop_agent import INVESTIGATION_SCRIPT, TOOLS, ScriptedModel, run_agent
def run_task_quietly(task: str) -> str:
"""Run the Part 1 agent, suppressing its transcript, return the answer."""
with contextlib.redirect_stdout(io.StringIO()):
return run_agent(ScriptedModel(INVESTIGATION_SCRIPT), TOOLS, task)
class RuntimeHandler(BaseHTTPRequestHandler):
"""The contract an AgentCore Runtime container must satisfy."""
def _send(self, code: int, body: dict) -> None:
payload = json.dumps(body).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def do_GET(self):
if self.path == "/ping":
self._send(200, {"status": "Healthy"}) # health check
else:
self._send(404, {"error": "not found"})
def do_POST(self):
if self.path != "/invocations":
self._send(404, {"error": "not found"})
return
length = int(self.headers.get("Content-Length", 0))
payload = json.loads(self.rfile.read(length) or b"{}")
# AgentCore passes your invocation payload straight through; the
# session id arrives as a header the platform sets per session.
session = self.headers.get("X-Amzn-Bedrock-AgentCore-Runtime-Session-Id",
"(none)")
task = payload.get("prompt", "")
answer = run_task_quietly(task)
self._send(200, {"answer": answer, "session": session})
def log_message(self, *args):
pass # quiet the server
def demo() -> None:
server = HTTPServer(("127.0.0.1", 0), RuntimeHandler) # ephemeral port
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{port}"
print(f"contract server up on {base} (port 8080 in a real container)\n")
# 1. the health check the platform polls
with urllib.request.urlopen(f"{base}/ping") as r:
print(f"GET /ping -> {r.status} {r.read().decode()}")
# 2. an invocation, with a session id header like the platform sets
body = json.dumps({"prompt": "Checkout p99 spiked overnight. Why?"}).encode()
req = urllib.request.Request(
f"{base}/invocations", data=body,
headers={"Content-Type": "application/json",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": "sess-" + "a" * 29})
with urllib.request.urlopen(req) as r:
out = json.loads(r.read())
print(f"POST /invocations -> {r.status}")
print(f" session : {out['session']}")
print(f" answer : {out['answer'][:64]}...")
server.shutdown()
print("\ntwo endpoints, one container: /ping proves health, /invocations "
"runs your loop. The @app.entrypoint decorator in the SDK writes "
"exactly this server for you; AgentCore adds the microVM, the "
"session, and the scaling around it.")
if __name__ == "__main__":
demo()
👉 Next: Memory, Gateway, and Identity, the three AgentCore components that turn a bare runtime into an agent that remembers across sessions, reaches your APIs as tools, and acts as the user without the user's secrets ever touching the prompt.