Agents on Call, Parte 5. La Squadra: Supervisore e Tre Specialisti
Un supervisore delega via rete agli agenti triage, runbook e cost, AgentCore Memory collega i loro risultati, e quando un solo agente resta la scelta migliore.

Quattro agenti esistono ora dove la Parte 4 ne aveva lasciato uno: un supervisore, un agente runbook e un agente cost si uniscono a incident-triage, coordinati non da chiamate di funzione Python dentro un unico processo ma dalla API InvokeAgentRuntime di AgentCore Runtime stesso, perché tutti e quattro continuano a essere distribuiti come risorse Runtime separate e isolate, lo stesso isolamento per cui la Parte 3 aveva scelto AgentCore Runtime fin dall'inizio. Una risorsa AWS in più li lega insieme: una singola istanza di AgentCore Memory, condivisa tra tutti e quattro tramite actor ID, così che una diagnosi scritta da triage alle 3 di notte resti leggibile per l'agente runbook a cui passa la mano pochi secondi dopo, e per qualunque agente guardi il mese prossimo un incidente con questa stessa forma.
La Parte 1 ha scelto AgentCore più Strands e ha abbozzato quattro agenti dietro un supervisore come forma obiettivo. La Parte 2 ha costruito il confine di account e la separazione ops-readonly/ops-mutate che ogni agente da allora ha riutilizzato. La Parte 3 ha rilasciato il primo agente, con due tool collegati direttamente nel suo stesso processo. La Parte 4 ha spostato quei tool, più un secondo tool in lettura e l'unico tool mutante della piattaforma, dietro AgentCore Gateway: quattro Lambda che un client MCP può scoprire, invece di quattro cose da cablare a mano per ogni agente. Questa parte è dove quell'investimento inizia a ripagare per più di un chiamante: gli agenti runbook e cost si connettono entrambi allo stesso Gateway ed ereditano tutti e quattro i tool senza dover ricavare da zero la gestione delle credenziali cross-account. Il codice di accompagnamento vive su github.com/flightlesstux/agents-on-call, in agents/supervisor/, agents/runbook/, agents/cost_optimizer/ e nelle aggiunte di questa parte a terraform/10-agent-runtime/; ogni snippet qui sotto è estratto da quei file, non semplificato per il post.
Quando un solo agente smette di bastare
L'agente di triage della Parte 3 aveva un system prompt, due tool, un compito. Ha retto bene fino ai quattro tool della Parte 4, perché tutti e quattro servivano ancora quell'unico compito: diagnosticare, mai rimediare. La pressione che davvero impone un secondo agente non è il numero di tool, è il numero di compiti. Un agente runbook (trasformare una diagnosi in una proposta di esecuzione SSM Automation) e un agente cost (leggere Cost Explorer una volta al giorno, senza sollecitazione, e proporre rightsizing) non sono variazioni del compito di triage, sono compiti diversi, con schedulazioni diverse e, se stipati in un unico system prompt, istruzioni contraddittorie che convivono nella stessa context window. "Non compi mai un'azione mutante, non hai alcun tool che possa farlo" (la regola propria di triage, invariata dalla Parte 3) e "proponi una chiamata ssm_execute quando un chunk di runbook corrispondente la supporta" (il vero compito dell'agente runbook) non sono frasi che un solo prompt riesce a enunciare insieme in modo pulito; un agente a cui viene chiesto di fare entrambe le cose fa scivolare la disciplina di un compito verso la disponibilità ad agire dell'altro, o perde la sfumatura del secondo compito sotto la cautela del primo. Dividere il lavoro tra processi, non solo tra sezioni di prompt, è ciò che mantiene leggibile la disciplina di triage due parti dopo: il suo system prompt non ha cambiato una riga dalla Parte 3.
Il context bloat è la versione più silenziosa della stessa pressione. Un agente di triage che si porta dietro anche le stranezze di Cost Explorer e il comportamento di retrieval del corpus dei runbook spende token di system prompt e peso di schema dei tool su compiti che, nella maggior parte delle invocazioni, non sta svolgendo. Quattro agenti, ciascuno con solo ciò che serve al proprio compito, è una decisione di efficienza sui token tanto quanto una decisione organizzativa.
Agenti come tool, la forma adatta a quattro Runtime separati
Strands Agents 1.0 ha rilasciato quattro nuove primitive multi-agente più il supporto al protocollo Agent-to-Agent nella sua release di produzione, e "agents as tools" ha più di una forma reale nell'SDK. Passare un'istanza Agent figlia direttamente nella lista tools=[...] di un genitore, o avvolgerne una in un decorator che la espone con una tool spec, funzionano bene entrambe ed entrambe richiedono la stessa cosa: il sub-agente vive nello stesso processo Python del suo chiamante. Non è la forma di questa piattaforma. Triage, runbook e cost sono distribuiti ciascuno come propria risorsa aws_bedrockagentcore_agent_runtime, versionati indipendentemente, con scoping IAM indipendente, ciascuno nell'isolamento di sessione per cui la Parte 3 ha scelto AgentCore Runtime specificamente. Far collassare tre Runtime in un unico processo per usare la scorciatoia in-process disferebbe quell'isolamento in cambio di una comodità sintattica.
| Forma | Come funziona | Si adatta a questa piattaforma? |
|---|---|---|
tools=[agent_instance] | Passa un oggetto Agent di Strands direttamente nella lista di tool del genitore | No: richiede il sub-agente nello stesso processo, facendo collassare l'isolamento di tre Runtime separati in uno solo |
.as_tool() | Avvolge una chiamata ad agente in-process in un decorator che espone una tool spec | No, per lo stesso motivo: sempre un solo processo, un solo raggio d'impatto se va in crash |
@tool che chiama InvokeAgentRuntime | Una semplice funzione tool di Strands che chiama via rete il Runtime già distribuito di un altro agente | Sì: corrisponde alla forma a quattro Runtime separati a cui il Terraform della Parte 3 si era già vincolato |
Il supervisore usa quindi la terza forma: funzioni decorate con @tool che chiamano l'operazione data-plane InvokeAgentRuntime di bedrock-agentcore contro l'ARN del runtime di ciascuno specialista. Il trade-off è reale e vale la pena nominarlo invece di nasconderlo: una chiamata in-process è una chiamata di funzione, questa è un hop di rete con la propria latenza e il proprio modo di fallire a metà. Pagato deliberatamente, non per caso, per l'isolamento a cui la Parte 3 si era già vincolata:
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 resultDue dettagli su cui vale la pena soffermarsi. Primo, _invoke_specialist solleva un'eccezione su uno status non-200 o su un body di errore invece di restituire una stringa che si legge come un normale risultato: un supervisore che non sa distinguere "runbook non ha trovato nulla" da "l'invocazione di runbook è fallita" sintetizzerà falsa fiducia in qualunque cosa dica a un umano. Secondo, _session_id viene coniato una volta per invocazione del supervisore, non una volta per chiamata di specialista, e passato a ogni specialista senza modifiche: è ciò che permette ad AgentCore Runtime di raggruppare le trace di tutti e tre gli specialisti sotto un unico incidente, ed è ciò che permette alla memoria condivisa qui sotto di distinguere gli eventi di un incidente da quelli di un altro.
L'agente runbook: Knowledge Base sui documenti SSM, propone, non esegue mai
L'intero compito dell'agente runbook è trasformare una diagnosi in una remediation candidata, proveniente da un documento reale, non inventata. Ci arriva con due fonti di tool: un nuovo tool kb_retrieve su una Bedrock Knowledge Base costruita sul corpus dei runbook, e ogni tool che Gateway già indicizza, scoperto al momento della connessione invece che dichiarato a mano:
@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() firma ogni richiesta verso l'endpoint MCP del Gateway con le credenziali del ruolo di esecuzione del container runtime stesso, SigV4, in linea con la scelta della Parte 4 di authorizer_type = "AWS_IAM" sulla risorsa Gateway stessa, e restituisce qualunque tool quel ruolo IAM possa raggiungere: cloudwatch-read, logs-read, cost-read e ssm-execute, gli stessi identici quattro costruiti dalla Parte 4, non elencati né referenziati da nessuna parte nel codice di questo agente. Se il modello chiami davvero ssm-execute è una decisione di system prompt e di IAM, non una decisione presa nel codice. E ssm-execute non è cambiato dalla Parte 4: richiede ancora diagnosis e blast_radius a ogni chiamata, avvia sempre e solo il flusso di approvazione di Step Functions, mai un'azione in un account spoke direttamente. Il system prompt dell'agente runbook ne fa una regola dura invece che implicita: chiamare ssm_execute viene riportato al supervisore come "inviato a un umano", mai come "fatto", per quanto sicuro di sé suoni il testo del modello.
L'agente cost: uno sweep schedulato dentro lo stesso gate
L'agente cost è l'unico specialista che nessun allarme sveglia: EventBridge Scheduler lo invoca una volta al giorno per spoke, senza alcun allarme a sollecitarlo, con la schedule stessa definita interamente in Terraform invece che lasciata come affermazione in una docstring:
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
...
}
}Legge attraverso la stessa connessione Gateway di runbook (cost-read, più cloudwatch-read e logs-read per corroborare un'anomalia di spesa contro il carico reale prima di segnalarla), e quando un'azione di rightsizing sembra giustificata, propone attraverso l'identico percorso ssm-execute che usa runbook. Una sola pipeline di approvazione, due proponenti: un umano che rivede Slack non dovrebbe aver bisogno di sapere, né di curarsi, se una proposta in attesa arriva da una diagnosi di incidente o da uno sweep notturno.
L'unica cosa che vale la pena progettare deliberatamente è non rimettere in discussione un rifiuto già dato. Uno sweep giornaliero senza memoria di ieri segnalerà la stessa istanza "idle" ogni mattina finché un umano non la approva o non si stanca di rifiutarla, e nessuno dei due esiti è buono:
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: un incidente condiviso, quattro scrittori
AgentCore Memory fattura gli eventi short-term a 0,25 $ ogni 1.000 chiamate CreateEvent e il retrieval long-term a 0,50 $ ogni 1.000 chiamate RetrieveMemoryRecords, abbastanza poco che quattro agenti che scrivono un evento ciascuno per invocazione costano un errore di arrotondamento rispetto al prezzo in token di una singola chiamata al modello. La strategia che fa il lavoro qui è EPISODIC, uno dei tipi built-in di AWS: cattura ogni sessione come un episodio strutturato (contesto, ragionamento, azioni, esiti) ed esegue un proprio passo di riflessione attraverso gli episodi per estrarre pattern più ampi, senza che questa piattaforma debba fare da sé il prompt engineering di quell'estrazione. Il supporto Terraform per il tipo EPISODIC su aws_bedrockagentcore_memory_strategy è arrivato il 29 aprile 2026, quasi due mesi prima della data stessa di questa parte:
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}"]
}Ogni agente scrive in quell'unica memoria attraverso le stesse due funzioni, che è ciò che impedisce a quattro Runtime distribuiti indipendentemente di divergere su come leggono e scrivono lo stato condiviso:
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"]Due cose che deliberatamente non fa. Restituisce None invece di sollevare un'eccezione quando il memory ID non è impostato, così un agente sotto test locale senza una risorsa memory provisionata degrada a "nessuna memoria condivisa" invece di far fallire una diagnosi per una variabile d'ambiente mancante. E actor_id è una stringa che un agente passa, non un valore che questa funzione valida contro qualcosa, di proposito: aggiungere un quinto agente più avanti non richiede alcuna modifica di schema qui, solo un nuovo actor che scrive nella stessa forma di namespace.
Onestà: quando il multi-agente è overkill
Quattro AgentCore Runtime più una risorsa memory più il networking cross-runtime sono infrastruttura reale per ciò che un team ops più piccolo potrebbe far girare come un solo agente con sette tool e un system prompt più lungo. AgentCore Runtime fattura 0,0895 $ per vCPU-ora e 0,00945 $ per GB-ora con il tempo idle gratuito, quindi quattro piccoli runtime non sono costosi in termini assoluti, ma sono quattro cose da distribuire, quattro ruoli IAM da auditare, e un hop InvokeAgentRuntime di latenza e superficie di fallimento che un processo singolo non ha mai. Il trigger onesto per dividere non è "questa piattaforma è cresciuta", è lo stesso con cui questa parte si è aperta: compiti distinti le cui istruzioni si contraddicono in un unico system prompt, o schedulazioni distinte (un agente guidato dagli allarmi e uno sweep giornaliero non sollecitato) che non condividono un unico loop naturale. Una piattaforma ops con un solo compito, triage e nient'altro, non riceve nessuna di queste pressioni e dovrebbe restare un solo agente con buoni tool: nessuna chiamata cross-runtime su cui ragionare, nessuna risorsa memory da tenere fresca, nessun modulo Terraform con for_each a coordinare tre ruoli che potrebbero benissimo essere uno solo. Dividere presto, prima che esista un secondo compito reale, compra un isolamento di cui nessuno aveva ancora bisogno al prezzo di un hop di rete che nessuno voleva ancora.
Modalità di fallimento da tenere d'occhio
Quattro che vale la pena nominare prima che questo giri contro un incidente reale. Triage non scrive ancora nella memoria condivisa: il suo codice della Parte 3 precede la risorsa memory di questa parte, e nulla in questa parte lo aggiorna retroattivamente, il che significa che agli episodi della strategia di riflessione manca l'unico agente che gira per primo su quasi ogni incidente, un vero buco, non una svista da sorvolare. Una chiamata cross-runtime che fallisce a metà è un fallimento diverso da una in-process: _invoke_specialist che solleva un'eccezione a metà incidente lascia ciò che il supervisore ha già registrato in memoria lì come timeline parziale, utile per un umano che riprende l'incidente a mano ma facile da leggere erroneamente come completa se nessuno controlla. Il passo di riflessione di EPISODIC gira in modo asincrono sulle sessioni chiuse, quindi retrieve_incident_context che restituisce una lista vuota significa o "genuinamente nuovo" o "la riflessione non è ancora girata", e né questo codice né il prompt dell'agente sanno attualmente distinguere i due casi. E le policy IAM dei nuovi agenti hanno una reale dipendenza di ordinamento: la policy del ruolo del supervisore referenzia gli ARN dei runtime di runbook e cost, quindi un apply Terraform interrotto tra il provisioning di quei runtime e il provisioning del ruolo del supervisore lascia il plan successivo a mostrare la policy del supervisore come non ancora applicata, non una misconfigurazione permanente, ma un motivo per lasciar finire un apply invece di fidarsi di uno parziale.
Leggi questo dopo
- Parte 4, Tool e il Gateway: MCP, Allowlist, Sola Lettura di Default, per i quattro tool a cui gli agenti runbook e cost di questa parte si connettono e che riutilizzano senza ricavare nulla da zero.
- Parte 6, Guardrail: La Parte che Tutti Saltano, dove ogni invocazione del modello di questi quattro agenti riceve un layer di contenuto e anti prompt-injection che nessuno di loro ha ancora.
- How to call multiple terraform modules in a single terragrunt file su ercan.cloud, lo stesso problema di "coordinare più unità distribuite indipendentemente senza duplicare il cablaggio tra loro" che i runtime degli agenti basati su
for_eachdi questa parte risolvono dentro un unico modulo, visto dal lato multi-modulo.
Le directory complete agents/supervisor/, agents/runbook/, agents/cost_optimizer/ e il Terraform di memory e runtime dietro di esse vivono nel repository di accompagnamento su github.com/flightlesstux/agents-on-call. Per il lato database e infrastruttura di come far girare piattaforme come questa su scala, gli appunti tecnici sono su ercan.cloud, e l'hub è su ercanermis.com.
Altro da Ercan
Altri due siti, stesso autore, terreno diverso.
Cloud, AWS, EKS, Terraform, platform engineering.
Note sul campo da sistemi in produzione. EKS, IAM, Terraform su scala organizzativa, observability, ottimizzazione dei costi.
Visita ercan.cloud →L'hub. Chi sono, consulenza, contatti.
Hub personale per entrambe le tracce di scrittura. Chi sono, come funziona la consulenza, come contattarmi.
Visita ercanermis.com →