La communication avec Claude Managed Agents est basée sur les événements. Vous envoyez des événements utilisateur à l'agent, et recevez en retour des événements d'agent et de session pour suivre l'état.
Les événements circulent dans deux directions.
user.* démarrent une session et l'orientent au fur et à mesure de sa progression ; system.message ajoute un contexte de niveau système qui s'applique au tour qui l'accompagne et à tous les tours suivants.Les chaînes de type des événements de session, de span, d'agent, utilisateur et système suivent une convention de nommage {domain}.{action}. Les événements d'aperçu delta réservés au flux (event_start, event_delta) constituent l'exception. Consultez Types d'événements dans la référence pour le catalogue complet.
Chaque événement persisté inclut un horodatage processed_at défini lorsque le traitement de l'événement se termine. Sur les événements que vous envoyez, processed_at est null tant que l'événement est encore en file d'attente derrière des événements antérieurs. Les exceptions sont user.define_outcome, user.custom_tool_result et user.tool_result, qui sont traités dès réception et renvoyés en écho avec processed_at déjà renseigné.
Envoyez un événement user.message pour démarrer ou poursuivre le travail de l'agent :
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.message",
"content": [
{
"type": "text",
"text": "Analyze the performance of the sort function in utils.py",
},
],
},
],
)Envoyez un événement user.interrupt pour arrêter l'agent en cours d'exécution, puis enchaînez avec un événement user.message pour le rediriger :
# L'agent est en train d'analyser un fichier...
# Interrompre avec une nouvelle direction :
client.beta.sessions.events.send(
session.id,
events=[
{"type": "user.interrupt"},
{
"type": "user.message",
"content": [
{
"type": "text",
"text": "Instead, focus on fixing the bug in line 42.",
},
],
},
],
)L'agent accuse réception de l'interruption et passe à la nouvelle tâche. Le tour interrompu se termine par un événement session.status_idle dont le stop_reason est end_turn, la même valeur que pour un tour qui se termine de lui-même ; il n'existe pas de raison d'arrêt spécifique à l'interruption.
Par défaut, le texte de réponse de l'agent atteint le flux sous forme d'événements agent.message mis en tampon, chacun n'étant émis qu'une fois terminée la requête de modèle qui l'a produit. Les « event deltas » (deltas d'événements) vous permettent d'afficher ce texte de manière incrémentale, sous forme d'aperçu en direct, pendant que le modèle est encore en train de le générer. Un aperçu n'est pas la réponse : les aperçus sont une aide à l'affichage fournie au mieux, et l'agent.message mis en tampon reste toujours l'enregistrement faisant autorité. Un client qui ignore les aperçus reçoit tout de même un flux complet et correct.
Les aperçus sont optionnels et activés par connexion de flux. Ajoutez le paramètre de requête event_deltas[] au flux que vous lisez, en le répétant une fois pour chaque type d'événement dont vous souhaitez un aperçu. Comme [] est un motif glob du shell, mettez l'URL entre guillemets chaque fois que vous construisez la requête dans un shell ; les exemples encodent les crochets en pourcentage sous la forme %5B%5D, ce qui fonctionne également. Les deux points de terminaison de flux acceptent le paramètre : le flux au niveau de la session à GET /v1/sessions/{session_id}/events/stream, et le flux propre à chaque thread de session à GET /v1/sessions/{session_id}/threads/{thread_id}/stream. Les valeurs acceptées sont agent.message et agent.thinking ; toute autre valeur renvoie une erreur 400, tout comme une requête comportant plus de 100 valeurs. Les aperçus d'un sous-agent apparaissent sur le flux de thread propre à ce sous-agent.
Lorsqu'un événement faisant l'objet d'un aperçu commence, le flux émet un event_start portant le type et l'id de l'événement à venir :
{
"type": "event_start",
"event": {
"type": "agent.message",
"id": "sevt_01abc..."
}
}Pour agent.message, le début est suivi d'événements event_delta portant du texte incrémental. Chaque delta désigne l'événement qu'il prolonge dans event_id et le bloc de contenu qu'il prolonge dans delta.index :
{
"type": "event_delta",
"event_id": "sevt_01abc...",
"delta": {
"type": "content_delta",
"index": 0,
"content": {
"type": "text",
"text": "Here is the summary"
}
}
}Lorsqu'un événement agent.thinking fait l'objet d'un aperçu, seul l'event_start est émis. Aucun événement event_delta ne suit, et l'événement agent.thinking mis en tampon qui conclut l'aperçu ne porte aucun contenu de réflexion ; il s'agit d'un signal de progression, et non d'un porteur de contenu.
Contrairement aux événements persistés, event_start et event_delta n'ont ni id ni processed_at propres. Le seul identifiant qu'ils portent est l'id de l'événement dont ils donnent un aperçu.
Chaque SDK prenant en charge les deltas d'événements inclut un utilitaire d'accumulation qui gère pour vous la comptabilité des index. Les utilitaires Go, Java, Ruby et C# indexent également l'aperçu en cours d'accumulation par l'id de l'événement ; avec les utilitaires Python, TypeScript et PHP, vous gérez vous-même cette table de correspondance et intégrez chaque delta dans l'entrée correspondant à son id. Le modèle manuel fonctionne également dans tous les langages lorsque vous avez besoin d'une comptabilité personnalisée : appliquez-le aux types d'événements générés.
Dans le modèle manuel, traitez l'aperçu comme un tampon de travail et l'événement mis en tampon comme l'enregistrement. Indexez le tampon par (event_id, index). Réconciliez par requête de modèle : un tour s'ouvre avec un unique événement session.status_running, puis, sur un tour qui se termine normalement, chaque requête de modèle produit, dans l'ordre, span.model_request_start, event_start, les événements event_delta, l'agent.message mis en tampon, et enfin span.model_request_end (dans l'onglet Événements de span). Sur le fil, voici la portion en aperçu de cette séquence, entrelacée avec les autres événements mis en tampon de la connexion :
event_start {"event": {"type": "agent.message", "id": "sevt_01abc..."}}
event_delta {"event_id": "sevt_01abc...", "delta": {"type": "content_delta", "index": 0, "content": {"type": "text", "text": "..."}}}
...
agent.message {"id": "sevt_01abc...", "content": [...]}La ligne event_delta se répète une fois par fragment de texte. Traitez chaque événement à son arrivée :
event_start, notez l'id annoncé. Les identifiants correspondent toujours : event_start.event.id, chaque event_delta.event_id et l'id de l'agent.message mis en tampon ont la même valeur.event_delta, ajoutez delta.content.text à l'entrée située à (event_id, delta.index) et affichez le texte en cours. Le premier delta pour un index crée cette entrée.agent.message mis en tampon arrive, faites-le correspondre par id, supprimez l'aperçu accumulé et affichez le contenu du message à la place.span.model_request_end, fermez tout aperçu qui n'a pas été réconcilié par son événement mis en tampon. Aucun autre delta n'arrivera pour lui. Si le tour échoue ou est interrompu, l'événement mis en tampon pourrait ne jamais arriver ; span.model_request_end arrive tout de même.Garanties sur lesquelles repose le modèle :
(event_id, index), donne un préfixe de content[index].text dans l'événement mis en tampon (un préfixe, pas nécessairement le texte entier, car des deltas peuvent être abandonnés sous charge).event_start par event_id, et l'événement mis en tampon est la dernière chose que cette connexion délivre pour cet id.# Instantanés d'aperçu, indexés par id d'événement. accumulate_managed_agents_event agrège chaque
# event_start / event_delta en un instantané agent.message ; l'agent.message
# mis en mémoire tampon le remplace.
previews: dict[str, BetaManagedAgentsAgentMessageEvent] = {}
# Activez les aperçus agent.message sur cette connexion
with client.beta.sessions.events.stream(
session.id, event_deltas=["agent.message"]
) as stream:
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.message",
"content": [{"type": "text", "text": "Describe the repo in one sentence."}],
},
],
)
for event in stream:
match event.type:
case "event_start":
snapshot = accumulate_managed_agents_event(None, event)
if snapshot is not None:
previews[event.event.id] = snapshot
print(f"event_start {event.event.type} {event.event.id}")
case "event_delta":
preview = accumulate_managed_agents_event(previews.get(event.event_id), event)
if preview is not None:
previews[event.event_id] = preview
text = "".join(block.text for block in preview.content)
print(f"event_delta preview: {text!r}")
case "agent.message":
# L'événement mis en mémoire tampon fait foi : il remplace et ferme l'aperçu
preview = accumulate_managed_agents_event(previews.pop(event.id, None), event)
text = "".join(block.text for block in preview.content)
print(f"agent.message {event.id} {text!r}")
case "span.model_request_end":
# Plus aucun delta n'arrivera. Fermez tout aperçu dont
# l'événement mis en mémoire tampon n'est jamais arrivé.
for event_id in previews:
print(f"span.model_request_end closing preview for {event_id}")
previews.clear()
case "session.status_idle":
breakDans une session multi-agents, chaque thread de session possède son propre flux d'événements à GET /v1/sessions/{session_id}/threads/{thread_id}/stream, et il accepte le même paramètre event_deltas[] avec les mêmes valeurs. Les aperçus sont limités au thread par conception : une connexion ne donne un aperçu que du thread qu'elle lit. Les aperçus d'un thread enfant sont délivrés sur le flux propre à cet enfant et ne sont jamais republiés sur le flux au niveau de la session, dont les aperçus restent limités au thread principal. Pour observer le texte d'un sous-agent au fur et à mesure que le modèle le génère, ouvrez le flux de thread de ce sous-agent.
Il est facile de se tromper dans le chemin du flux de thread : il s'agit de /threads/{thread_id}/stream, et non de /events/stream (qui n'existe qu'au niveau de la session), et il n'existe pas de point de terminaison /threads/{thread_id}/events/stream.
Les événements d'aperçu eux-mêmes ne changent pas. event_start et event_delta ont la même forme sur un flux de thread que sur le flux au niveau de la session, et le modèle accumuler et réconcilier s'applique tel qu'il est décrit. Le seul ajustement concerne la comptabilité : exécutez une instance d'accumulateur par connexion de flux.
# Lister les threads de la session et choisir un enfant : les threads enfants ont un
# parent_thread_id non nul, et le parent_thread_id du thread principal est null.
THREAD_ID=$(
curl --fail-with-body -sS \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/threads?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" |
jq -er 'first(.data[] | select(.parent_thread_id != null)).id'
)
# Le stream du thread enfant prend le même paramètre event_deltas[] que le
# stream de session. Encoder les crochets en pourcent (%5B%5D) et mettre l'URL entre guillemets.
exec {stream}< <(
curl --fail-with-body -sS -N \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/stream?beta=true&event_deltas%5B%5D=agent.message" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "accept: text/event-stream"
)
while IFS= read -r -u "$stream" event_line; do
[[ $event_line == data:* ]] || continue
event_json=${event_line#data: }
case $(jq -r '.type' <<<"$event_json") in
event_delta)
jq -j '.delta.content.text' <<<"$event_json"
;;
agent.message)
# L'événement mis en mémoire tampon est l'enregistrement de référence ; afficher son contenu.
printf '\n'
jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
printf '\n'
;;
session.thread_status_idle)
break
;;
esac
done
exec {stream}<&-La boucle de lecture se termine sur session.thread_status_idle, l'événement émis lorsque le tour du thread de session se termine et que le thread devient inactif.
Les aperçus sont optimisés pour la réactivité. Développez en tenant compte de ces contraintes :
agent.message mis en tampon arrive tout de même complet. Ne traitez jamais un aperçu accumulé comme définitif.agent.message que votre aperçu attendait. Il n'existe aucun moyen de redemander les deltas manqués.agent.thinking avec début uniquement : un aperçu agent.thinking n'émet que l'event_start pour signaler qu'un bloc de réflexion a commencé ; aucun événement event_delta ne le suit.event_start et event_delta n'existent que sur le flux en direct. Ils n'apparaissent ni dans l'historique des événements de la session (GET /v1/sessions/{session_id}/events) ni dans l'historique des événements d'aucun thread de session.Si le flux ne se comporte pas comme vous l'attendez :
| Vous observez | Ce que cela signifie |
|---|---|
Un flux avec des événements mis en tampon mais sans event_start ni event_delta | La connexion que vous lisez n'a pas souscrit (event_deltas[] s'applique par connexion, et non par session), ou le tour n'a jamais touché le thread que vous recevez en streaming. Les aperçus étant limités au thread, listez les threads de la session (GET /v1/sessions/{session_id}/threads) pour trouver celui qui s'est exécuté. |
| Une erreur 404 sur l'URL du flux | Le chemin ou un identifiant est incorrect, ou la requête ne porte aucun en-tête bêta managed-agents. Les points de terminaison de thread sont protégés par la bêta ; sans l'en-tête, ils n'existent donc pas. |
Une erreur 400 mentionnant event_deltas | Seuls agent.message et agent.thinking sont acceptés. |
Lorsque l'agent invoque un outil personnalisé :
agent.custom_tool_use contenant le nom de l'outil et son entrée.session.status_idle contenant stop_reason: requires_action. Les identifiants des événements bloquants se trouvent dans le tableau stop_reason.event_ids.user.custom_tool_result pour chacun, en passant l'identifiant de l'événement dans le paramètre custom_tool_use_id accompagné du contenu du résultat.running.with client.beta.sessions.events.stream(session.id) as stream:
for event in stream:
if event.type == "session.status_idle" and (stop_reason := event.stop_reason):
match stop_reason.type:
case "requires_action":
for event_id in stop_reason.event_ids:
# Rechercher l'événement d'utilisation d'outil personnalisé et l'exécuter
tool_event = events_by_id[event_id]
result = call_tool(tool_event.name, tool_event.input)
# Renvoyer le résultat
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.custom_tool_result",
"custom_tool_use_id": event_id,
"content": [{"type": "text", "text": result}],
},
],
)
case "end_turn":
breakLorsqu'une politique d'autorisation exige une confirmation avant l'exécution d'un outil :
agent.tool_use ou agent.mcp_tool_use.session.status_idle contenant stop_reason: requires_action. Les identifiants des événements bloquants se trouvent dans le tableau stop_reason.event_ids.user.tool_confirmation pour chacun, en passant l'identifiant de l'événement dans le paramètre tool_use_id. Définissez result sur "allow" ou "deny". Utilisez deny_message pour expliquer un refus.running.with client.beta.sessions.events.stream(session.id) as stream:
for event in stream:
if event.type == "session.status_idle" and (stop_reason := event.stop_reason):
match stop_reason.type:
case "requires_action":
for event_id in stop_reason.event_ids:
# Approuver l'appel d'outil en attente
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.tool_confirmation",
"tool_use_id": event_id,
"result": "allow",
},
],
)
case "end_turn":
breakLes sessions persistent entre les interactions. L'historique de conversation est conservé, sauf si la session est explicitement supprimée. Lorsqu'une session devient inactive, son sandbox fait l'objet d'un point de contrôle, préservant l'état complet du sandbox, y compris le système de fichiers, les paquets installés et tous les fichiers créés par l'agent. Cela vous permet de reprendre proprement après une période d'inactivité.
Pour reprendre une session, envoyez-lui un événement user.message comme d'habitude :
# En production, passez l'ID stocké de la session que vous souhaitez reprendre.
ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
events:
- type: user.message
content:
- type: text
text: Now run the tests against the changes you made earlier.
YAMLUne session créée avec un budget se met en pause au lieu de dépasser ses dépenses. Lorsque le coût catalogue suivi de la session atteint le plafond, la plateforme met chaque thread en pause avant sa prochaine requête de modèle, et la session devient inactive avec un stop_reason de budget_reached au lieu de se terminer. La requête qui a fait passer le total au-delà du plafond s'exécute jusqu'à son terme, de sorte que le list_cost rapporté par l'instantané session.usage peut indiquer une valeur égale ou légèrement supérieure au plafond. Sur le flux, la pause arrive sous la forme de trois événements, dans l'ordre :
session.thread_status_idle avec stop_reason: budget_reached, pour chaque thread au moment où il se met en pause.session.usage, un instantané de l'utilisation cumulée de la session et du coût catalogue suivi.session.status_idle avec stop_reason: budget_reached. L'événement session.usage précède toujours immédiatement cet état inactif.Un thread dont la requête finale franchit le plafond et termine son tour en même temps rapporte end_turn sur son propre événement session.thread_status_idle, tandis que la session rapporte toujours budget_reached ; basez-vous sur le stop_reason au niveau de la session pour détecter la pause.
Tant que la session est à son plafond, elle n'accepte que les événements qui règlent le travail déjà en cours : user.tool_confirmation, user.tool_result, user.custom_tool_result et user.interrupt. Tout événement qui démarrerait un nouveau travail, y compris user.message, est rejeté avec une erreur 400 mentionnant cette liste. Lorsqu'une session comporte à la fois un thread en attente d'une demande d'outil et un thread en pause au plafond, le stop_reason au niveau de la session est requires_action, et non budget_reached : régler la demande ne déclenche pas de requête de modèle, répondez-y donc comme d'habitude.
Aucun événement ne reprend une session en pause à son plafond. Mettez plutôt à jour le budget de la session : modifier le plafond pour toute valeur supérieure au coût catalogue consommé, ou supprimer le budget en mettant à jour la session avec "budget": null, reprend automatiquement le travail en pause. Consultez Budgets de session pour savoir comment le coût catalogue est suivi et connaître la sémantique complète de mise à jour du budget.
Envoyez un événement system.message pour fournir à l'agent un contexte privilégié de niveau système qui s'applique au tour qui l'accompagne et à tous les tours suivants. Contrairement au champ system de la définition de l'agent (qui définit l'invite système de premier niveau), le contenu de system.message est ajouté au contexte système de la session sous la forme d'un tour role: "system" au lieu de remplacer cette invite. Utilisez-le lorsque l'agent a besoin de directives de niveau système mises à jour en cours de session : un persona différent, des contraintes révisées ou un contexte récupéré à l'exécution qui doit façonner le comportement du modèle par la suite.
ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
events:
- type: system.message
content:
- type: text
text: "The user's current timezone is America/New_York."
YAMLTant que la session est inactive avec stop_reason: requires_action, un system.message n'est accepté que lorsqu'il suit un événement de résultat d'outil dans la même requête ; envoyé seul ou avec un user.message, il est rejeté jusqu'à ce que les événements d'outil en attente soient résolus. content accepte de 1 à 1000 éléments de texte.
L'objet session inclut un champ usage contenant l'utilisation cumulée de la session : le nombre de jetons, l'utilisation d'outils serveur, le temps actif et le coût catalogue suivi. Récupérez la session une fois qu'elle est devenue inactive pour lire les derniers totaux.
{
"id": "sesn_01...",
"status": "idle",
"usage": {
"input_tokens": 5000,
"output_tokens": 3200,
"cache_read_input_tokens": 20000,
"cache_creation": {
"ephemeral_5m_input_tokens": 2000,
"ephemeral_1h_input_tokens": 0
},
"list_cost": {
"amount": "187",
"currency": "USD"
},
"active_seconds": 342.5,
"server_tool_use": {
"web_search_requests": 3,
"web_fetch_requests": 0
}
}
}input_tokens indique les jetons d'entrée non mis en cache et output_tokens indique le total des jetons de sortie sur l'ensemble des appels de modèle de la session. Le champ cache_read_input_tokens indique les jetons lus depuis le cache de prompts, et l'objet cache_creation ventile les jetons de création de cache par durée de vie du cache (ephemeral_5m_input_tokens et ephemeral_1h_input_tokens). Les entrées de cache utilisent par défaut un TTL de 5 minutes, de sorte que les tours consécutifs dans cette fenêtre bénéficient des lectures de cache, ce qui réduit le coût par jeton.
list_cost est la consommation cumulée de la session tarifée aux prix catalogue publics, sous forme d'un nombre entier de centimes dans une chaîne de caractères, accompagné d'un code de devise. active_seconds est le temps cumulé pendant lequel la session avait au moins un thread en cours d'exécution ; l'activité simultanée de threads concurrents n'est comptée qu'une seule fois, contrairement au champ active_seconds de l'objet stats de la session, qui additionne le temps actif propre à chaque thread. Ce chiffre dédupliqué est la durée sur laquelle le coût d'exécution de la session est tarifé. server_tool_use compte les requêtes d'outils exécutées côté serveur à des fins de tarification : les requêtes de recherche web sont intégrées au coût catalogue par requête, tandis que les requêtes de récupération web (web fetch) n'entraînent aucun frais par requête et ne sont pas mesurées, de sorte que web_fetch_requests affiche 0. Le champ usage propre à chaque thread de session comporte également list_cost et active_seconds. Les chiffres par thread sont arrondis indépendamment et excluent le coût de temps d'exécution de la session, de sorte que leur somme ne correspond pas exactement au list_cost de la session ; le chiffre de la session est celui qui fait autorité.
Vous n'avez pas besoin d'interroger la session pour observer ces totaux. L'événement session.usage transporte le même instantané cumulé (l'objet usage, plus le budget de la session, qui vaut null lorsque la session n'en a pas) sur le flux de la session et dans l'historique des événements. Il est émis lors des transitions vers l'état inactif plutôt que selon un minuteur : la session en émet un immédiatement avant de devenir inactive, quelle que soit la raison d'arrêt, et un lorsqu'un thread se met en pause en atteignant un budget de session. Un lecteur de flux voit donc le coût final d'un tour, ou du travail qui a atteint un budget, sans récupération supplémentaire.
Pour appliquer une limite de dépenses, définissez un budget de session plutôt que d'interroger l'utilisation et d'arrêter la session vous-même. La plateforme tarife la consommation de la session en continu et met chaque thread en pause avant sa prochaine requête de modèle dès que le coût catalogue de la session atteint le plafond ; consultez Atteindre un budget de session pour voir à quoi cela ressemble sur le flux.
La Claude Console fournit une vue chronologique visuelle de vos sessions d'agent. Accédez à la section Claude Managed Agents dans la Console pour voir :
session.errorWas this page helpful?