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.