Vier Agenten existieren jetzt dort, wo Teil 4 einen zurückgelassen hat: Ein Supervisor, ein Runbook-Agent und ein Cost-Agent stoßen zu incident-triage, koordiniert nicht durch Python-Funktionsaufrufe innerhalb eines Prozesses, sondern durch AgentCore Runtimes eigene InvokeAgentRuntime-API, weil alle vier weiterhin als separate, isolierte Runtime-Ressourcen deployt werden, dieselbe Isolation, für die Teil 3 AgentCore Runtime überhaupt erst gewählt hat. Eine weitere AWS-Ressource verbindet sie: eine einzige AgentCore-Memory-Instanz, geteilt über alle vier per Actor-ID, sodass eine Diagnose, die Triage um 3 Uhr morgens schreibt, für den Runbook-Agenten, an den sie Sekunden später übergibt, noch lesbar ist, und für welchen Agenten auch immer, der sich nächsten Monat einen ähnlich geformten Vorfall ansieht.

Teil 1 hat sich für AgentCore plus Strands entschieden und vier Agenten hinter einem Supervisor als Zielform skizziert. Teil 2 hat die Kontogrenze und den ops-readonly/ops-mutate-Split gebaut, den jeder Agent seither wiederverwendet. Teil 3 hat den ersten Agenten ausgeliefert, zwei Tools direkt in seinen eigenen Prozess verdrahtet. Teil 4 hat diese Tools, plus ein zweites Read-Tool und das einzige mutierende Tool der Plattform, hinter AgentCore Gateway verschoben: vier Lambdas, die ein MCP-Client entdecken kann, statt vier Dinge, die pro Agent von Hand zu verdrahten wären. Dieser Teil ist der Punkt, an dem diese Investition anfängt, sich für mehr als einen Aufrufer auszuzahlen: Der Runbook- und der Cost-Agent verbinden sich beide mit demselben Gateway und erben alle vier Tools, ohne die kontoübergreifende Credential-Behandlung aus irgendetwas neu herzuleiten. Der begleitende Code liegt auf github.com/flightlesstux/agents-on-call, unter agents/supervisor/, agents/runbook/, agents/cost_optimizer/ und den Ergänzungen dieses Teils zu terraform/10-agent-runtime/; jeder Ausschnitt unten stammt direkt aus diesen Dateien, nicht für den Beitrag vereinfacht.

Wann ein Agent aufhört zu genügen

Teil 3s Triage-Agent hatte einen System-Prompt, zwei Tools, eine Aufgabe. Das hat auch Teil 4s vier Tools noch getragen, weil alle vier weiterhin dieser einen Aufgabe dienten: diagnostizieren, niemals remediieren. Der Druck, der tatsächlich einen zweiten Agenten erzwingt, ist nicht die Tool-Anzahl, es ist die Anzahl der Aufgaben. Ein Runbook-Agent (eine Diagnose in einen vorgeschlagenen SSM-Automation-Lauf verwandeln) und ein Cost-Agent (einmal am Tag, unaufgefordert, Cost Explorer lesen und Rightsizing vorschlagen) sind keine Varianten von Triages Aufgabe, sie sind andere Aufgaben, mit anderen Zeitplänen und, in einen einzigen System-Prompt gequetscht, widersprüchlichen Anweisungen im selben Kontextfenster. "Du führst niemals eine mutierende Aktion aus, du hast kein Tool, das es könnte" (Triages eigene Regel, unverändert seit Teil 3) und "schlage einen ssm_execute-Aufruf vor, wenn ein passender Runbook-Chunk ihn stützt" (die eigentliche Aufgabe des Runbook-Agenten) sind keine Sätze, die ein Prompt sauber nebeneinander formuliert; ein Agent, der beides tun soll, driftet mit der Disziplin der einen Aufgabe in Richtung der Handlungsbereitschaft der anderen, oder verliert die Nuance der zweiten Aufgabe unter der Vorsicht der ersten. Die Arbeit über Prozesse zu splitten, nicht nur über Prompt-Abschnitte, ist das, was Triages eigene Disziplin zwei Teile später lesbar hält: Sein System-Prompt hat sich seit Teil 3 um keine Zeile geändert.

Context Bloat ist die leisere Version desselben Drucks. Ein Triage-Agent, der zusätzlich Cost Explorers Eigenheiten und das Retrieval-Verhalten des Runbook-Korpus mit sich trägt, gibt System-Prompt-Tokens und Tool-Schema-Gewicht für Aufgaben aus, die er in der jeweiligen Invocation meistens gar nicht erledigt. Vier Agenten, von denen jeder nur hält, was seine eigene Aufgabe braucht, sind eine Token-Effizienz-Entscheidung, so sehr wie eine organisatorische.

Agenten als Tools, die Form, die zu vier separaten Runtimes passt

Strands Agents 1.0 hat in seinem Production-Release vier neue Multi-Agent-Primitive plus Agent-to-Agent-Protokoll-Support ausgeliefert, und "Agents as Tools" hat im SDK mehr als eine reale Form. Eine Child-Agent-Instanz direkt in die tools=[...]-Liste eines Parents zu geben, oder eine in einen Decorator zu wickeln, der sie mit einer Tool-Spec exponiert, funktioniert beides gut, und beides verlangt dasselbe: Der Sub-Agent lebt im selben Python-Prozess wie sein Aufrufer. Das ist nicht die Form dieser Plattform. Triage, Runbook und Cost deployen jeweils als eigene aws_bedrockagentcore_agent_runtime-Ressource, unabhängig versioniert, unabhängig IAM-gescoped, jede in der Session-Isolation, für die Teil 3 AgentCore Runtime gezielt ausgewählt hat. Drei Runtimes in einen Prozess zu kollabieren, um die In-Process-Abkürzung zu nutzen, würde diese Isolation für eine syntaktische Bequemlichkeit rückgängig machen.

FormWie es funktioniertPasst zu dieser Plattform?
tools=[agent_instance]Ein Strands-Agent-Objekt direkt in die eigene Tools-Liste eines Parents gebenNein: braucht den Sub-Agenten im selben Prozess und kollabiert die Isolation dreier separater Runtimes in eine
.as_tool()Einen In-Process-Agentenaufruf in einen Decorator wickeln, der eine Tool-Spec exponiertNein, gleicher Grund: weiterhin ein Prozess, ein Blast Radius, wenn er abstürzt
@tool ruft InvokeAgentRuntime aufEine einfache Strands-Tool-Funktion, die die bereits deployte Runtime eines anderen Agenten über das Netzwerk aufruftJa: passt zur Form mit vier separaten Runtimes, auf die sich Teil 3s Terraform bereits festgelegt hat

Der Supervisor nutzt also die dritte Form: mit @tool dekorierte Funktionen, die bedrock-agentcores Data-Plane-Operation InvokeAgentRuntime gegen die Runtime-ARN jedes Spezialisten aufrufen. Der Trade-off ist real und es lohnt sich, ihn zu benennen statt zu verstecken: Ein In-Process-Aufruf ist ein Funktionsaufruf, dies hier ist ein Netzwerk-Hop mit eigener Latenz und einer eigenen Art, auf halbem Weg zu scheitern. Bewusst bezahlt, nicht aus Versehen, für die Isolation, auf die sich Teil 3 bereits festgelegt hat:

def _invoke_specialist(agent_runtime_arn: str, actor_id: str, prompt: str) -> str:
    """Invoke one specialist's AgentCore Runtime and return its text result.
    ...
    """
    response = _agentcore.invoke_agent_runtime(
        agentRuntimeArn=agent_runtime_arn,
        runtimeSessionId=_session_id,
        contentType="application/json",
        payload=json.dumps({"prompt": prompt, "session_id": _session_id}).encode("utf-8"),
    )
    if response["statusCode"] != 200:
        raise RuntimeError(f"{actor_id} runtime invocation failed with status {response['statusCode']}")

    body = json.loads(response["response"].read())
    if "error" in body:
        raise RuntimeError(f"{actor_id} returned an error: {body['error']}")
    return body["result"]


@tool
def ask_triage_agent(alarm_context: str) -> str:
    """Ask the incident-triage specialist to diagnose a firing alarm.
    ...
    """
    result = _invoke_specialist(TRIAGE_AGENT_RUNTIME_ARN, "incident-triage", alarm_context)
    record_incident_event("supervisor", _session_id, {"delegated_to": "incident-triage", "result": result})
    return result

Zwei Details, bei denen sich ein Innehalten lohnt. Erstens: _invoke_specialist wirft bei einem Nicht-200-Status oder einem Error-Body eine Exception, statt einen String zurückzugeben, der sich wie ein normaler Befund liest: Ein Supervisor, der "Runbook hat nichts gefunden" nicht von "Runbooks Invocation ist fehlgeschlagen" unterscheiden kann, synthetisiert falsche Zuversicht in alles, was er einem Menschen erzählt. Zweitens: _session_id wird einmal pro Supervisor-Invocation geprägt, nicht einmal pro Spezialisten-Aufruf, und unverändert an jeden Spezialisten weitergereicht: Das ist es, was AgentCore Runtime die Traces aller drei Spezialisten unter einem Vorfall gruppieren lässt, und was der geteilten Memory unten erlaubt, die Events eines Vorfalls von denen eines anderen zu unterscheiden.

Der Runbook-Agent: Knowledge Base über SSM-Dokumenten, schlägt vor, führt nie aus

Die gesamte Aufgabe des Runbook-Agenten besteht darin, eine Diagnose in einen Remediation-Kandidaten zu verwandeln, bezogen aus einem echten Dokument, nicht erfunden. Dorthin kommt er mit zwei Tool-Quellen: einem neuen kb_retrieve-Tool über einer Bedrock Knowledge Base auf dem Runbook-Korpus, und jedem Tool, das Gateway bereits indiziert, entdeckt zur Connect-Zeit statt von Hand deklariert:

@tool
def kb_retrieve(query: str, max_results: int = 5) -> str:
    """Search the runbook Knowledge Base for playbook chunks relevant to query.
    ...
    """
    response = _bedrock_agent_runtime.retrieve(
        knowledgeBaseId=RUNBOOK_KNOWLEDGE_BASE_ID,
        retrievalQuery={"text": query},
        retrievalConfiguration={"vectorSearchConfiguration": {"numberOfResults": max_results}},
    )
    ...
    return json.dumps({"query": query, "chunks": chunks})


_gateway = connect_gateway_tools()

agent = Agent(
    model=model,
    system_prompt=SYSTEM_PROMPT,
    tools=[kb_retrieve, *_gateway.list_tools_sync()],
    name="runbook",
    ...
)

connect_gateway_tools() signiert jeden Request an Gateways MCP-Endpunkt mit den Execution-Role-Credentials des Runtime-Containers selbst, SigV4, passend zu Teil 4s Wahl von authorizer_type = "AWS_IAM" auf der Gateway-Ressource selbst, und gibt zurück, was auch immer diese IAM-Rolle an Tools erreichen kann: cloudwatch-read, logs-read, cost-read und ssm-execute, die identischen vier, die Teil 4 gebaut hat, nirgendwo im eigenen Code dieses Agenten gelistet oder referenziert. Ob das Modell ssm-execute tatsächlich aufruft, ist eine System-Prompt- und IAM-Entscheidung, keine zur Code-Zeit. Und ssm-execute hat sich seit Teil 4 nicht geändert: Es verlangt weiterhin diagnosis und blast_radius bei jedem Aufruf, startet weiterhin nur den Step-Functions-Approval-Flow, niemals direkt eine Aktion im Spoke-Konto. Der System-Prompt des Runbook-Agenten macht daraus eine harte Regel statt einer impliziten: Ein Aufruf von ssm_execute wird dem Supervisor als "an einen Menschen geschickt" gemeldet, niemals als "erledigt", egal wie zuversichtlich der eigene Text des Modells klingt.

Der Cost-Agent: ein geplanter Sweep in dasselbe Gate

Der Cost-Agent ist der eine Spezialist, den nichts pagt: EventBridge Scheduler ruft ihn einmal am Tag pro Spoke auf, von keinem Alarm angestoßen, der Zeitplan selbst vollständig in Terraform definiert statt als Behauptung in einem Docstring belassen:

resource "aws_scheduler_schedule" "cost_agent_daily" {
  name                = "${var.platform_name}-cost-agent-daily"
  description         = "Daily trigger for the cost agent's Cost Explorer sweep across configured spoke accounts."
  schedule_expression = "cron(0 6 * * ? *)"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:bedrockagentcore:invokeAgentRuntime"
    role_arn = aws_iam_role.cost_scheduler.arn

    ...
  }
}

Er liest über dieselbe Gateway-Verbindung wie Runbook (cost-read, plus cloudwatch-read und logs-read, um eine Ausgabenanomalie gegen echte Last zu erhärten, bevor er sie meldet), und wenn eine Rightsizing-Aktion gerechtfertigt aussieht, schlägt er sie über den identischen ssm-execute-Pfad vor, den Runbook nutzt. Eine Approval-Pipeline, zwei Vorschlagende: Ein Mensch, der Slack reviewt, sollte weder wissen müssen noch sich darum kümmern, ob ein offener Vorschlag aus einer Vorfallsdiagnose oder einem nächtlichen Sweep stammt.

Das eine, was es sich bewusst einzudesignen lohnt, ist, eine Ablehnung nicht neu zu verhandeln. Ein täglicher Sweep ohne Erinnerung an gestern wird dieselbe "idle" Instanz jeden Morgen melden, bis ein Mensch sie entweder freigibt oder es leid ist, sie abzulehnen, und keins von beidem ist gut:

prior_episodes = retrieve_incident_context("cost", prompt)
if prior_episodes:
    prompt = (
        f"{prompt}\n\nNote: {len(prior_episodes)} related past cost proposal(s) "
        "exist in memory; check whether any were denied before re-proposing."
    )

AgentCore Memory: ein geteilter Vorfall, vier Schreiber

AgentCore Memory berechnet Short-Term-Events mit 0,25 $ pro 1.000 CreateEvent-Aufrufen und Long-Term-Retrieval mit 0,50 $ pro 1.000 RetrieveMemoryRecords-Aufrufen, billig genug, dass vier Agenten, die je ein Event pro Invocation schreiben, neben dem Token-Preis eines einzigen Modellaufrufs einen Rundungsfehler kosten. Die Strategie, die hier die Arbeit macht, ist EPISODIC, einer von AWS' eingebauten Typen: Sie erfasst jede Session als strukturierte Episode (Kontext, Reasoning, Aktionen, Ergebnisse) und lässt ihren eigenen Reflection-Schritt über Episoden laufen, um breitere Muster zu extrahieren, ohne dass diese Plattform diese Extraktion selbst prompt-engineeren müsste. Terraform-Support für den Typ EPISODIC auf aws_bedrockagentcore_memory_strategy landete am 29. April 2026, fast zwei Monate vor dem eigenen Datum dieses Teils:

resource "aws_bedrockagentcore_memory" "incident" {
  name                  = "${replace(var.platform_name, "-", "_")}_incident_memory"
  description           = "Shared short-term event stream and long-term episodic memory for one incident's supervisor, triage, runbook, and cost agents."
  event_expiry_duration = var.memory_event_expiry_days

  # memory_execution_role_arn deliberately omitted: per the fact table, it
  # is required only when a memory uses a CUSTOM strategy with a
  # model-processing override block (SEMANTIC_OVERRIDE,
  # USER_PREFERENCE_OVERRIDE, and so on). EPISODIC below is a built-in
  # type, not CUSTOM, so AWS runs its own reflection step without this
  # platform needing to grant or manage a role for it.
  tags = merge(var.tags, { Component = "memory" })
}

...

resource "aws_bedrockagentcore_memory_strategy" "episodic" {
  name       = "incident-episodes"
  memory_id  = aws_bedrockagentcore_memory.incident.id
  type       = "EPISODIC"
  namespaces = ["/incidents/{actorId}"]
}

Jeder Agent schreibt über dieselben zwei Funktionen in diese eine Memory, und genau das hält vier unabhängig deployte Runtimes davon ab, im Lesen und Schreiben geteilten Zustands auseinanderzudriften:

def record_incident_event(actor_id: str, session_id: str, event: dict) -> str | None:
    """Write one short-term event to the shared incident memory.
    ...
    """
    if not MEMORY_ID:
        return None

    response = _agentcore.create_event(
        memoryId=MEMORY_ID,
        actorId=actor_id,
        sessionId=session_id,
        eventTimestamp=time.time(),
        payload=[
            {
                "conversational": {
                    "content": {"text": json.dumps(event, default=str)},
                    "role": "ASSISTANT",
                }
            }
        ],
    )
    return response["event"]["eventId"]

Zwei Dinge tut sie bewusst nicht. Sie gibt None zurück, statt zu werfen, wenn die Memory-ID nicht gesetzt ist, sodass ein Agent im lokalen Test ohne provisionierte Memory-Ressource zu "keine geteilte Memory" degradiert, statt eine Diagnose an einer fehlenden Umgebungsvariable scheitern zu lassen. Und actor_id ist ein String, den ein Agent hereinreicht, kein Wert, den diese Funktion gegen irgendetwas validiert, mit Absicht: Ein fünfter Agent später braucht hier keine Schema-Änderung, nur einen neuen Actor, der in dieselbe Namespace-Form schreibt.

Ehrlich: wann Multi-Agent Overkill ist

Vier AgentCore Runtimes plus eine Memory-Ressource plus Cross-Runtime-Networking sind echte Infrastruktur für etwas, das ein kleineres Ops-Team als einen Agenten mit sieben Tools und einem längeren System-Prompt betreiben könnte. AgentCore Runtime berechnet 0,0895 $ pro vCPU-Stunde und 0,00945 $ pro GB-Stunde, Idle-Zeit ist kostenlos, vier kleine Runtimes sind also absolut gesehen nicht teuer, aber sie sind vier Dinge zu deployen, vier IAM-Rollen zu auditieren und ein InvokeAgentRuntime-Hop an Latenz und Fehlerfläche, den ein einzelner Prozess nie hat. Der ehrliche Auslöser fürs Splitten ist nicht "diese Plattform ist gewachsen", es ist derselbe, mit dem dieser Teil eröffnet hat: unterschiedliche Aufgaben, deren Anweisungen sich in einem System-Prompt widersprechen, oder unterschiedliche Zeitpläne (ein alarmgetriebener Agent und ein unaufgeforderter täglicher Sweep), die keinen natürlichen gemeinsamen Loop teilen. Eine Ops-Plattform mit einer Aufgabe, Triage und sonst nichts, bekommt nichts von diesem Druck und sollte ein Agent mit guten Tools bleiben: kein Cross-Runtime-Aufruf, über den man nachdenken muss, keine Memory-Ressource, die man vor dem Veralten bewahren muss, kein for_each-Terraform-Modul, das drei Rollen koordiniert, die genauso gut eine sein könnten. Früh zu splitten, bevor eine zweite echte Aufgabe existiert, kauft Isolation, die noch niemand brauchte, zum Preis eines Netzwerk-Hops, den auch noch niemand wollte.

Fehlerbilder, auf die man achten sollte

Vier, die es wert sind, benannt zu werden, bevor dies gegen einen echten Vorfall läuft. Triage schreibt noch nicht in die geteilte Memory: Sein Teil-3-Code ist älter als die Memory-Ressource dieses Teils, und nichts in diesem Teil rüstet ihn nach, was bedeutet, dass den Episoden der Reflection-Strategie der eine Agent fehlt, der bei fast jedem Vorfall zuerst läuft, eine echte Lücke, kein Versehen, das man überspielen sollte. Ein Cross-Runtime-Aufruf, der auf halbem Weg scheitert, ist ein anderer Fehler als ein In-Process-Aufruf: Wirft _invoke_specialist mitten im Vorfall, bleibt alles, was der Supervisor bereits in die Memory geschrieben hat, als partielle Timeline liegen, nützlich für einen Menschen, der den Vorfall von Hand übernimmt, aber leicht als vollständig misszuverstehen, wenn niemand nachprüft. EPISODICs Reflection-Schritt läuft asynchron über geschlossene Sessions, eine leere Liste von retrieve_incident_context bedeutet also entweder "wirklich neu" oder "Reflection ist noch nicht gelaufen", und weder dieser Code noch der Prompt des Agenten kann die beiden derzeit auseinanderhalten. Und die IAM-Policies der neuen Agenten haben eine echte Reihenfolge-Abhängigkeit: Die Rollen-Policy des Supervisors referenziert die eigenen ARNs der Runbook- und Cost-Runtimes, ein Terraform-Apply, das zwischen dem Provisionieren dieser Runtimes und dem Provisionieren der Supervisor-Rolle abbricht, hinterlässt also den nächsten Plan mit der Supervisor-Policy als noch-nicht-angewendet, keine bleibende Fehlkonfiguration, aber ein Grund, ein Apply zu Ende laufen zu lassen, statt einem partiellen zu vertrauen.

Weiterlesen

Die vollständigen agents/supervisor/, agents/runbook/, agents/cost_optimizer/ und das Memory- und Runtime-Terraform dahinter liegen im begleitenden Repository unter github.com/flightlesstux/agents-on-call. Für die Datenbank- und Infrastruktur-Seite, solche Plattformen in diesem Maßstab zu betreiben, gibt es die Field Notes auf ercan.cloud, und der Hub liegt auf ercanermis.com.