Agents on Call, Teil 3. Erster Agent: Incident Triage in Strands
Der erste Agent geht live: Strands-Triage-Agent auf AgentCore Runtime, evidenzbasierter System-Prompt, zwei Read-only-Tools, plus das deployende Terraform.

Der erste funktionierende Agent dieser Serie sind etwa 260 Zeilen Python: ein Strands-Agent, zwei Read-only-Tools, die eine kontoübergreifende IAM-Rolle annehmen, bevor sie boto3 aufrufen, und ein System-Prompt, dessen gesamte Aufgabe darin besteht, das Modell davon abzuhalten, bei etwas, das es tatsächlich nicht geprüft hat, selbstbewusst zu klingen. Noch kein AgentCore Gateway, kein Supervisor, kein Multi-Agenten-Handoff: Das kommt in späteren Teilen. Dieser Teil handelt davon, einen Agenten eine Aufgabe korrekt erledigen zu lassen, deployt auf AgentCore Runtime, bevor irgendetwas hinzukommt, das das Debuggen erschwert.
Teil 1 hat das Szenario gesetzt (ein SaaS-Shop mit 50 Engineers, 40 Pages pro Woche, Read-only durchgesetzt von IAM als nicht verhandelbarer Zwang) und sich für AgentCore plus Strands entschieden statt für klassische Bedrock Agents oder eine selbstgebaute Loop. Teil 2 hat die Kontogrenze gebaut, in der diese Agenten leben: die Spoke-Rollen ops-readonly und ops-mutate, sowie ein Application Inference Profile pro Agent, das Bedrocks Cross-Region-Routing umschließt. Dieser Teil nutzt beides direkt. Nichts hier wird von Grund auf neu provisioniert; es konsumiert, was Teil 2 bereits gebaut hat. Der begleitende Code liegt auf github.com/flightlesstux/agents-on-call, unter agents/triage/agent.py und terraform/10-agent-runtime/, und jeder Ausschnitt unten stammt direkt aus diesen Dateien, nicht für den Beitrag vereinfacht.
Anatomie eines Strands-Agenten
Strands Agents ist ein modellgetriebenes Python-SDK, von AWS im Mai 2025 unter Apache 2.0 open-sourced und intern für Amazon Q Developer, AWS Glue und VPC Reachability Analyzer genutzt, bevor es das Haus je verlassen hat. Das Versprechen ist keine neue Orchestrierungssprache: Es ist eine dünne Loop um ein Modell, das bereits weiß, wie man Tools aufruft, verdrahtet mit Bedrock, Anthropic direkt, Ollama oder einem über LiteLLM proxied Provider, ohne dass sich ändert, wie der Agent geschrieben wird. Die gesamte Form eines Agenten passt in fünf Schritte.
1. Konfiguration kommt aus der Umgebung, nicht aus Literalen in der Datei. Alles, was die Runtime wissen muss, wird vom Terraform-Modul unten als Umgebungsvariable gesetzt, sodass nichts Kontospezifisches oder Geheimes im Quellcode hardcoded ist:
AWS_REGION = os.environ.get("AWS_REGION", "eu-west-1")
INFERENCE_PROFILE_ARN = os.environ["AGENT_INFERENCE_PROFILE_ARN"]
OPS_READONLY_ROLE_NAME = os.environ.get("OPS_READONLY_ROLE_NAME", "ops-readonly")
SPOKE_EXTERNAL_ID = os.environ["SPOKE_EXTERNAL_ID"]2. Tools sind einfache Python-Funktionen mit einem Decorator. Strands liest die Type Hints und den Docstring einer Funktion, um die Tool-Spec zu bauen, die das Modell tatsächlich sieht, der Docstring ist also kein Kommentar für Menschen, sondern der Interface-Vertrag:
@tool
def cloudwatch_read(
spoke_account_id: str,
namespace: str,
metric_name: str,
dimensions: dict[str, str],
lookback_minutes: int = 60,
stat: str = "Average",
period_seconds: int = 60,
) -> str:
"""Read a CloudWatch metric's recent datapoints from a spoke account.
...
"""3. Das Modell wird über das Inference Profile aus Teil 2 verdrahtet, nicht über eine rohe Model-ID. Die Temperature bleibt absichtlich niedrig: Dieser Agent schlägt eine Diagnose aus Evidenz vor, er braucht keine kreative Varianz in seiner Wortwahl.
model = BedrockModel(
model_id=INFERENCE_PROFILE_ARN,
region_name=AWS_REGION,
temperature=0.1,
max_tokens=2048,
)4. Der Agent selbst ist ein einziger Konstruktor-Aufruf: ein Modell, ein System-Prompt und eine Liste von Tools. Es gibt keinen Orchestrierungsgraphen zu autorisieren, weil Strands die Tool-Calling-Loop aus dem eigenen Tool-Use-Output des Modells ableitet:
agent = Agent(
model=model,
system_prompt=SYSTEM_PROMPT,
tools=[cloudwatch_read, logs_read],
name="incident-triage",
description=(
"First-pass responder for CloudWatch alarms: reads metrics and logs, proposes a "
"root cause candidate, defers all remediation to a human or the runbook agent."
),
)5. Ein AgentCore-Runtime-Entrypoint umschließt ihn. BedrockAgentCoreApp kommt aus dem separaten Paket bedrock-agentcore, nicht aus Strands selbst: Strands ist das Agenten-Framework, AgentCore ist die Hosting-Schicht, und der Decorator @app.entrypoint ist die Naht zwischen beiden.
app = BedrockAgentCoreApp()
@app.entrypoint
def invoke(payload: dict) -> dict:
"""AgentCore Runtime entrypoint.
...
"""
prompt = payload.get("prompt")
if not prompt:
return {"error": "payload.prompt is required"}
result = agent(prompt)
return {"result": str(result)}
if __name__ == "__main__":
app.run()Der Aufruf agent(prompt) ist die gesamte Agenten-Loop von außen betrachtet: Strands schickt den Prompt und die Tool-Specs an Bedrock, bekommt entweder eine finale Antwort oder eine Tool-Use-Anfrage zurück, führt das angeforderte Tool aus, speist das Ergebnis zurück und wiederholt das, bis das Modell keine Tools mehr anfordert. Nichts von dieser Loop ist Code, den dieses Projekt besitzt; es ist Code, den dieses Projekt unter der selbstgebauten Option, die Teil 1 verworfen hat, von Hand hätte schreiben müssen.
System-Prompt-Design: Evidenz vor Schlussfolgerungen
Ein Triage-Agent, der selbstbewusst klingt, ist gefährlicher als einer, der sagt "Ich weiß es noch nicht", weil ein Mensch, der seinen Output um drei Uhr morgens liest, genauso auf den Ton reagiert wie auf den Inhalt. Der System-Prompt existiert fast ausschließlich, um diesen Fehlermodus zu verhindern, nicht um den Agenten schlauer zu machen:
1. Evidence before conclusions. Call cloudwatch_read and logs_read before stating any
root cause candidate. A hypothesis formed before you have queried at least one metric
and one log group is a guess, not a diagnosis, and you must not present it as one.
2. Cite the metric name and namespace for every claim: say "AWS/ApplicationELB
TargetResponseTime rose from 120ms to 4.8s over the last 15 minutes", not "latency went
up". A reader with no access to your tool calls should be able to verify every number you
state by re-running the same query.
3. State uncertainty explicitly. ...
4. No speculation about causes your tools cannot see. ...
5. Empty results are findings. A metric with no datapoints in the query window, or a log
query that returns zero rows, is evidence (the failure is silent, or upstream of this log
group), not a reason to retry with a wider window until something shows up.
6. Close with a structured summary: the alarm, the root cause candidate(s) with confidence,
the exact metrics and log queries you ran, and what you would check next if you had one
more tool call.Regel zwei leistet die meiste Arbeit. "Cite the metric name" klingt wie eine stilistische Präferenz, aber es ist das, was den Output des Agenten von einer Behauptung, der ein Mensch vertrauen muss, in eine Behauptung verwandelt, die ein Mensch in fünfzehn Sekunden verifizieren kann, indem er dieselbe CloudWatch-Query einfügt. Regel fünf existiert, weil der natürliche Fehlermodus eines Agenten mit Retry-Budget darin besteht, das Zeitfenster immer weiter zu vergrößern oder die Query zu ändern, bis er etwas findet, woraufhin "etwas" als Ursache gemeldet wird, selbst wenn es unzusammenhängend ist. Ein leeres Ergebnis ist Daten. Es als zu meldenden Endpunkt zu behandeln, statt als Aufforderung, weiter zu fischen, ist das, was den Agenten davon abhält, eine Diagnose aus Rauschen zu fabrizieren.
Zwei Tools, reines Python, noch kein Gateway
Die Zielarchitektur aus Teil 1 stellt jedes Tool hinter AgentCore Gateway: MCP-kompatibel, zentral IAM-scoped, eine Allowlist für vier Tools über die gesamte Plattform. Das ist Teil 4. Hier, mit genau einem Agenten und zwei Tools, wäre Gateway Indirektion, zwischen der es noch nichts zu routen gibt. Beide Tools sind einfache, mit @tool dekorierte Python-Funktionen, die der Agent direkt aufruft, und beide tun dasselbe, bevor sie AWS berühren: ops-readonly im Ziel-Spoke-Konto annehmen.
def _assume_ops_readonly(spoke_account_id: str) -> boto3.Session:
sts = boto3.client("sts", region_name=AWS_REGION, config=_boto_config)
role_arn = f"arn:aws:iam::{spoke_account_id}:role/{OPS_READONLY_ROLE_NAME}"
creds = sts.assume_role(
RoleArn=role_arn,
RoleSessionName="triage-agent-read",
ExternalId=SPOKE_EXTERNAL_ID,
DurationSeconds=900,
)["Credentials"]
return boto3.Session(
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"],
region_name=AWS_REGION,
)Fünfzehn Minuten, nicht die Ein-Stunden-Obergrenze, die die Trust Policy von ops-readonly erlaubt: Ein Tool-Aufruf, der nach fünfzehn Minuten noch Credentials braucht, hängt an etwas anderem fest, und ein kürzer lebendes Credential verkleinert den Blast Radius, falls es je aus dem Prozess herausgeleakt wird. cloudwatch_read umschließt get_metric_statistics und gibt Datapoints als JSON zurück, älteste zuerst. logs_read startet eine CloudWatch-Logs-Insights-Query und pollt auf Fertigstellung, gedeckelt auf achtzehn Sekunden über zwanzig Versuche, und wirft bei einer feststeckenden Query lieber einen Fehler, statt ein leeres Ergebnis zurückzugeben: Eine Log-Query, die still timet outet und null passende Zeilen meldet, liest sich exakt wie "keine Fehler gefunden", und das ist genau der eine Fehlermodus, den dieses Tool nicht haben darf. Beide Tools akzeptieren ein Argument spoke_account_id, das Modell entscheidet also anhand dessen, was das Alarm-Event ihm mitteilt, welches Konto abgefragt wird, nicht ein in das Deployment eingebackener Wert.
Was AgentCore Runtime gegenüber Lambda oder ECS bringt
Nichts hier erfordert strikt AgentCore Runtime. Ein Strands-Agent ist normales Python; er könnte in einer Lambda-Funktion oder einem ECS-Task mit einem boto3-Client und einer Loop laufen. AgentCore Runtime, seit dem 13. Oktober 2025 allgemein verfügbar, bringt drei Dinge, die sonst selbstgebaute Infrastruktur wären:
- Ein Achtstunden-Ausführungsfenster pro Session. Lambdas harte Fünfzehn-Minuten-Obergrenze erzwingt für alles, was länger läuft, entweder einen sehr kurzlebigen Agenten oder einen handgebauten Continuation-Mechanismus über Invocations hinweg, was eine Multi-Tool-Diagnose-Loop mit Retries durchaus kann. Das Session-Modell von AgentCore Runtime braucht diesen Workaround nicht.
- Vollständige Session-Isolation zwischen Invocations, ohne eine Per-Session-Sandbox von Hand aus separaten Lambda-Execution-Contexts oder ECS-Tasks zu bauen.
- Pro-Sekunde-Abrechnung ohne Kosten für Idle-CPU. 0,0895 $ pro vCPU-Stunde und 0,00945 $ pro GB-Stunde, sekundengenau abgerechnet, mit einer Speicheruntergrenze von 128 MB, und ausdrücklich keine Kosten für die Zeit, die ein Agent blockiert auf eine LLM-Antwort oder einen Tool-Aufruf wartend verbringt, was typischerweise 30 bis 70 Prozent der Wall-Clock-Zeit einer Session ausmacht. Ein ECS-Task oder eine provisionierte Lambda würden diese Idle-Zeit unabhängig davon abrechnen, ob die CPU irgendetwas tut.
Der Trade-off ist ein echter Zwang, keine Fußnote: AgentCore Runtime unterstützt nur den ARM64-Befehlssatz, jede Abhängigkeit mit kompilierten Erweiterungen muss sich also zu einem ARM64- oder manylinux-Wheel auflösen, sonst scheitert das Deployment beim Upload an der ELF-Header-Validierung. Die zwei Abhängigkeiten dieses Agenten, strands-agents und bedrock-agentcore, liefern heute keine kompilierten Erweiterungen aus, hier beißt es also nicht. Es wird den ersten Agenten dieser Serie beißen, der so etwas wie einen nativen Datenbanktreiber braucht, und der Fix an diesem Punkt besteht darin, das Deployment-Artefakt mit pip install --platform manylinux2014_aarch64 --only-binary=:all: zu bauen statt mit einem einfachen pip install.
Terraform für die Runtime
aws_bedrockagentcore_agent_runtime unterstützt zwei sich gegenseitig ausschließende Artefakttypen unter agent_runtime_artifact: code_configuration, ein S3-Zip mit einer Runtime-Version und einem Entry Point, oder container_configuration, eine ECR-Image-URI. Dieses Modul nutzt code_configuration. Die eigene CLI von AgentCore setzt neue Agenten standardmäßig auf denselben Direct-Code-Deploy-Pfad (intern CodeZip genannt) statt auf einen Container-Build, und bei genau zwei Abhängigkeiten und keinen zu installierenden Systempaketen wäre ein Dockerfile Zeremonie, die dieser Agent noch nicht braucht. Ein künftiger Agent, dessen Abhängigkeiten Kompilierung brauchen, oder dessen Runtime nicht Python ist, ist der Auslöser für den Wechsel zu container_configuration, keine grundsätzliche Präferenz für den einen Artefakttyp gegenüber dem anderen.
resource "aws_bedrockagentcore_agent_runtime" "triage" {
agent_runtime_name = "${replace(var.platform_name, "-", "_")}_triage"
description = "Incident-triage agent: reads CloudWatch metrics and Logs Insights across spokes, diagnoses, never mutates."
role_arn = aws_iam_role.triage_runtime.arn
agent_runtime_artifact {
code_configuration {
entry_point = ["agent.py"]
runtime = "PYTHON_3_12"
code {
s3 {
bucket = aws_s3_bucket.agent_artifacts.id
prefix = aws_s3_object.triage_agent_code.key
version_id = aws_s3_object.triage_agent_code.version_id
}
}
}
}
environment_variables = {
AWS_REGION = var.aws_region
AGENT_INFERENCE_PROFILE_ARN = var.agent_inference_profile_arn
OPS_READONLY_ROLE_NAME = var.ops_readonly_role_name
SPOKE_EXTERNAL_ID = var.spoke_external_id
}
network_configuration {
network_mode = "PUBLIC"
}
protocol_configuration {
server_protocol = "HTTP"
}
tags = merge(var.tags, {
Agent = "triage"
})
}network_mode = "PUBLIC" statt eines VPC-Attachments ist bewusst gewählt: Die einzigen ausgehenden Aufrufe dieses Agenten sind die Bedrock-Model-Invocation und das kontoübergreifende sts:AssumeRole, beides öffentliche AWS-Service-Endpunkte, erreicht über IAM-authentifiziertes HTTPS. Es gibt keine private Netzwerkressource, eine Datenbank oder einen internen Load Balancer, mit der oder dem dieser Agent direkt spricht, ein VPC-Attachment würde also NAT- und Endpunkt-Kosten für nichts hinzufügen. Die Permission Policy der Execution Role ist in drei Richtungen gescoped, die die drei Dinge widerspiegeln, die dieser Agent tatsächlich berühren darf: bedrock:InvokeModel auf der Inference-Profile-ARN und auf den zugrunde liegenden Foundation-Model-ARNs, zu denen sie routen kann (beides, weil Cross-Region-Inference sowohl auf Profile-Ebene als auch auf Ziel-Modell-Ebene autorisiert, und das Fehlen der zweiten Berechtigung lässt nur die Invocations fehlschlagen, die zufällig in eine Region ohne sie routen, genau das intermittierend aussehende Fehlerbild, das Teil 2 schon für den Modellzugriff selbst markiert hat); sts:AssumeRole gescoped auf die exakte ops-readonly-ARN in jedem Spoke-Konto, das dieser Agent abfragen darf, gebaut aus einer Liste, nicht einem Wildcard; und s3:GetObject auf ihren eigenen Code-Artefakt-Prefix, nichts Weiteres.
Eine erste Diagnose, vom Alarm zur Zusammenfassung
Ein CloudWatch-Alarm auf AWS/ApplicationELB TargetResponseTime feuert für den Checkout-Service in einem Spoke-Konto. Der Supervisor-Agent existiert noch nicht (Teil 5), also fügt für diesen Teil ein Mensch den Alarm-Kontext direkt in den Agenten ein. Was zurückkommt, komprimiert:
> cloudwatch_read(spoke_account_id="222222222222",
namespace="AWS/ApplicationELB", metric_name="TargetResponseTime",
dimensions={"LoadBalancer": "app/prod-checkout/50dc6c495c0c9188"},
lookback_minutes=30)
< 30 datapoints, p-average rising from 0.14s to 4.6s over the last 12 minutes
> cloudwatch_read(spoke_account_id="222222222222",
namespace="AWS/ApplicationELB", metric_name="HTTPCode_Target_5XX_Count",
dimensions={"LoadBalancer": "app/prod-checkout/50dc6c495c0c9188"},
lookback_minutes=30)
< 5XX count flat near zero for 18 minutes, then rising with TargetResponseTime
> logs_read(spoke_account_id="222222222222",
log_group_name="/aws/ecs/prod-checkout",
query_string='fields @timestamp, @message | filter @message like /timeout|pool/ | sort @timestamp desc',
lookback_minutes=15)
< 41 rows, dominant message: "HikariPool-1 - Connection is not available,
request timed out after 30000ms"Die Zusammenfassung des Agenten: AWS/ApplicationELB TargetResponseTime auf app/prod-checkout ist über 12 Minuten von 140ms auf 4,6s gestiegen, wobei HTTPCode_Target_5XX_Count etwa sechs Minuten nach Beginn des Latenzanstiegs zu klettern begann. CloudWatch Logs Insights gegen /aws/ecs/prod-checkout im selben Fenster liefert 41 Zeilen, die zu einem Connection-Pool-Timeout passen, wahrscheinlichste Ursache: Der Datenbank-Connection-Pool des Service ist erschöpft, und Requests stauen sich, bis sie den 30-Sekunden-Pool-Timeout treffen, was sich als erhöhte TargetResponseTime zeigt, bevor daraus ein offener 5XX wird. Konfidenz: moderat, nicht hoch, weil dieser Agent kein Tool hat, das die eigene Connection-Zahl der Datenbank oder das konfigurierte Maximum des Pools lesen kann, er kann also nicht bestätigen, dass der Pool tatsächlich voll ist, im Gegensatz etwa zu einer Downstream-Abhängigkeit, die Connections länger als üblich offen hält. Empfohlene nächste Prüfung: die Datenbank-Connection-Metriken des Runbook-Agenten, sobald dieser Agent existiert, oder ein Mensch, der die aktuelle Active-Connection-Zahl des Pools direkt prüft.
Dieser letzte Absatz zeigt die Regeln des System-Prompts wie beabsichtigt funktionierend, keine für den Beitrag hinzugefügte Absicherung. Der Agent hat echte Evidenz für eine echte Hypothese, zitiert die exakten Metriken und die Query dahinter, und stoppt genau an der Grenze dessen, was seine zwei Tools tatsächlich sehen können, benennt die Lücke statt über sie hinwegzuraten.
Fehlerbilder, auf die man achten sollte
Drei Dinge, die es wert sind zu wissen, bevor dieser Agent gegen einen echten Alarm läuft. Erstens muss ein AssumeRole-Fehler gegen ops-readonly, falsche Konto-ID, abgelaufene External ID, ein noch nicht onboardeter Spoke, in der finalen Zusammenfassung als "Ich konnte dieses Konto nicht lesen" auftauchen, nicht als still übersprungener Tool-Aufruf, den das Modell umgeht, indem es aus der einen erfolgreichen Datenquelle schlussfolgert; eine partielle Diagnose, präsentiert mit derselben Konfidenz wie eine vollständige, ist schlimmer als keine Diagnose. Zweitens ist das Achtzehn-Sekunden-Timeout von Logs Insights eine echte Obergrenze: Eine Query gegen eine High-Volume-Log-Gruppe mit einem breiten Filter kann legitim länger dauern, und der Fix ist eine engere Query (schmaleres Zeitfenster, spezifischerer Filter) vom Modell, kein längeres Timeout, das aus einem hängenden Tool-Aufruf eine hängende Agent-Invocation macht. Drittens reduziert niedrige Temperature die Varianz der Wortwahl, garantiert aber nicht, dass das Modell beide Tools aufruft, bevor es antwortet; Regel eins des System-Prompts ist der eigentliche Durchsetzungsmechanismus, und es lohnt sich, gegen Alarme zu testen, die bewusst darauf ausgelegt sind, eine Abkürzung zu verlocken, ein Alarm, dessen Name allein schon stark eine Ursache nahelegt, um zu bestätigen, dass der Agent trotzdem abfragt, bevor er schlussfolgert.
Weiterlesen
- Teil 2, Das Fundament: Terraform vor den Tokens, für die
ops-readonly-Rolle und das Application Inference Profile, das dieser Agent direkt konsumiert. - Teil 4, Tools und das Gateway: MCP, Allowlists, Read-only per Default, wo diese selben zwei Tools, plus zwei weitere, hinter AgentCore Gateway wandern und zentral gescoped IAM bekommen, statt einzeln, eine Python-Funktion nach der anderen, verdrahtet zu werden.
- Automating AWS CloudWatch Log Group Tagging with Python and Boto3 auf ercan.cloud, dasselbe boto3-gegen-CloudWatch-Muster, das die Tools dieses Teils nutzen, von der reinen Automatisierungsseite statt von der Agenten-Seite aus.
Die vollständige agent.py und das komplette Modul terraform/10-agent-runtime/, einschließlich der IAM-Policy-Dokumente, die aus den obigen Ausschnitten gekürzt wurden, 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.
Weiteres von Ercan
Zwei weitere Seiten, gleicher Autor, anderes Terrain.
Cloud, AWS, EKS, Terraform, Platform Engineering.
Praxisnotizen aus Produktionssystemen. EKS, IAM, Terraform im Organisationsmaßstab, Observability, Kostenoptimierung.
Besuchen ercan.cloud →Die Drehscheibe. Über mich, Beratung, Kontakt.
Persönliche Drehscheibe für beide Schreibspuren. Wer ich bin, wie die Beratung funktioniert, wie Sie mich erreichen.
Besuchen ercanermis.com →