La comunicación con Claude Managed Agents se basa en eventos. Envías eventos de usuario al agente y recibes de vuelta eventos del agente y de la sesión para hacer seguimiento del estado.
Los eventos fluyen en dos direcciones.
user.* inician una sesión y la dirigen a medida que avanza; system.message agrega contexto a nivel de sistema que se aplica al turno que lo acompaña y a todos los turnos posteriores.Las cadenas de tipo de los eventos de sesión, span, agente, usuario y sistema siguen una convención de nombres {domain}.{action}. Los eventos de vista previa delta exclusivos del stream (event_start, event_delta) son la excepción. Consulta Tipos de eventos en la referencia para ver el catálogo completo.
Cada evento persistido incluye una marca de tiempo processed_at que se establece cuando el evento termina de procesarse. En los eventos que envías, processed_at es null mientras el evento sigue en cola detrás de eventos anteriores. Las excepciones son user.define_outcome, user.custom_tool_result y user.tool_result, que se procesan al recibirse y se devuelven con processed_at ya completado.
Envía un evento user.message para iniciar o continuar el trabajo del agente:
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",
},
],
},
],
)Envía un evento user.interrupt para detener al agente en plena ejecución y luego continúa con un evento user.message para redirigirlo:
# El agente está analizando un archivo...
# Interrumpe con una nueva dirección:
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.",
},
],
},
],
)El agente reconoce la interrupción y cambia a la nueva tarea. El turno interrumpido termina con un evento session.status_idle cuyo stop_reason es end_turn, el mismo valor que un turno que termina por sí solo; no existe un motivo de detención específico para la interrupción.
De forma predeterminada, el texto de respuesta del agente llega al stream como eventos agent.message almacenados en búfer, cada uno emitido solo después de que termina la solicitud al modelo que lo produjo. Los "event deltas" (deltas de eventos) te permiten renderizar ese texto de forma incremental, como una vista previa en vivo, mientras el modelo todavía lo está generando. Una vista previa no es la respuesta: las vistas previas son una ayuda de visualización de mejor esfuerzo, y el agent.message almacenado en búfer es siempre el registro autoritativo. Un cliente que ignora las vistas previas sigue recibiendo un stream completo y correcto.
Las vistas previas son opcionales por conexión de stream. Agrega el parámetro de consulta event_deltas[] al stream que estás leyendo, repitiéndolo una vez por cada tipo de evento del que quieras vista previa. Como [] es un patrón glob del shell, pon la URL entre comillas siempre que construyas la solicitud en un shell; los ejemplos codifican los corchetes en porcentaje como %5B%5D, lo cual también funciona. Ambos endpoints de stream aceptan el parámetro: el stream a nivel de sesión en GET /v1/sessions/{session_id}/events/stream, y el stream propio de cada hilo de sesión en GET /v1/sessions/{session_id}/threads/{thread_id}/stream. Los valores aceptados son agent.message y agent.thinking; cualquier otro valor devuelve un error 400, al igual que una solicitud con más de 100 valores. Las vistas previas de un subagente aparecen en el stream del hilo propio de ese subagente.
Cuando comienza un evento con vista previa, el stream emite un event_start que lleva el tipo y el id del evento próximo:
{
"type": "event_start",
"event": {
"type": "agent.message",
"id": "sevt_01abc..."
}
}Para agent.message, al inicio le siguen eventos event_delta que llevan texto incremental. Cada delta indica el evento que extiende en event_id y el bloque de contenido que extiende en delta.index:
{
"type": "event_delta",
"event_id": "sevt_01abc...",
"delta": {
"type": "content_delta",
"index": 0,
"content": {
"type": "text",
"text": "Here is the summary"
}
}
}Cuando se hace vista previa de un evento agent.thinking, solo se emite el event_start. No le siguen eventos event_delta, y el evento agent.thinking almacenado en búfer que concluye la vista previa no lleva contenido de pensamiento; es una señal de progreso, no un portador de contenido.
A diferencia de los eventos persistidos, event_start y event_delta no tienen id ni processed_at propios. El único identificador que llevan es el id del evento del que hacen vista previa.
Cada SDK que admite deltas de eventos incluye un helper acumulador que se encarga del registro de index por ti. Los helpers de Go, Java, Ruby y C# también indexan la vista previa acumulada por el id del evento; con los helpers de Python, TypeScript y PHP mantienes ese mapa tú mismo e incorporas cada delta en la entrada correspondiente a su id. El patrón manual también funciona en todos los lenguajes cuando necesitas un registro personalizado: aplícalo a los tipos de eventos generados.
En el patrón manual, trata la vista previa como un búfer temporal y el evento almacenado en búfer como el registro. Indexa el búfer por (event_id, index). Concilia por solicitud al modelo: un turno se abre con un único evento session.status_running; luego, en un turno que se completa normalmente, cada solicitud al modelo produce, en orden, span.model_request_start, event_start, los eventos event_delta, el agent.message almacenado en búfer y, finalmente, span.model_request_end (en la pestaña Span events). En la transmisión, esta es la porción con vista previa de esa secuencia, intercalada con los demás eventos almacenados en búfer de la conexión:
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 línea event_delta se repite una vez por fragmento de texto. Procesa cada evento a medida que llega:
event_start, anota el id anunciado. Los identificadores siempre coinciden: event_start.event.id, cada event_delta.event_id y el id del agent.message almacenado en búfer son el mismo valor.event_delta, agrega delta.content.text a la entrada en (event_id, delta.index) y renderiza el texto acumulado. El primer delta para un index crea esa entrada.agent.message almacenado en búfer, hazlo coincidir por id, descarta la vista previa acumulada y renderiza el contenido del mensaje en su lugar.span.model_request_end, cierra cualquier vista previa que no haya sido conciliada por su evento almacenado en búfer. No llegarán más deltas para ella. Si el turno falla o se interrumpe, es posible que el evento almacenado en búfer nunca llegue; span.model_request_end sí llega.Garantías en las que se basa el patrón:
(event_id, index), da un prefijo de content[index].text en el evento almacenado en búfer (un prefijo, no necesariamente el texto completo, porque los deltas pueden descartarse bajo carga).event_start por event_id, y el evento almacenado en búfer es lo último que esa conexión entrega para ese id.# Instantáneas de vista previa, indexadas por id de evento. accumulate_managed_agents_event pliega cada
# event_start / event_delta en una instantánea agent.message; el agent.message
# almacenado en búfer la reemplaza.
previews: dict[str, BetaManagedAgentsAgentMessageEvent] = {}
# Habilita las vistas previas de agent.message en esta conexión
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":
# El evento almacenado en búfer es el registro: reemplaza y cierra la vista previa
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":
# Ya no llegarán más deltas. Cierra cualquier vista previa cuyo
# evento almacenado en búfer nunca llegó.
for event_id in previews:
print(f"span.model_request_end closing preview for {event_id}")
previews.clear()
case "session.status_idle":
breakEn una sesión multiagente, cada hilo de sesión tiene su propio flujo de eventos en GET /v1/sessions/{session_id}/threads/{thread_id}/stream, y acepta el mismo parámetro event_deltas[] con los mismos valores. Las vistas previas tienen alcance de hilo por diseño: una conexión solo hace vista previa del hilo que está leyendo. Las vistas previas de un hilo hijo se entregan en el stream propio de ese hijo y nunca se publican de forma cruzada en el stream a nivel de sesión, cuyas vistas previas permanecen limitadas al hilo principal. Para ver el texto de un subagente a medida que el modelo lo genera, abre el stream del hilo de ese subagente.
Es fácil equivocarse con la ruta del stream del hilo: es /threads/{thread_id}/stream, no /events/stream (que existe solo a nivel de sesión), y no existe un endpoint /threads/{thread_id}/events/stream.
Los eventos de vista previa en sí no cambian. event_start y event_delta tienen la misma forma en un stream de hilo que en el stream a nivel de sesión, y el patrón de acumular y conciliar se aplica tal como está escrito. El único ajuste es de registro: ejecuta una instancia de acumulador por conexión de stream.
# Lista los hilos de la sesión y elige un hijo: los hilos hijos tienen un
# parent_thread_id no nulo, y el parent_thread_id del hilo principal es 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'
)
# El stream del hilo hijo acepta el mismo parámetro event_deltas[] que el
# stream de la sesión. Codifica los corchetes con porcentaje (%5B%5D) y entrecomilla la URL.
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)
# El evento almacenado en búfer es el registro autoritativo; renderiza su contenido.
printf '\n'
jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
printf '\n'
;;
session.thread_status_idle)
break
;;
esac
done
exec {stream}<&-El bucle de lectura termina en session.thread_status_idle, el evento que se emite cuando termina el turno del hilo de sesión y el hilo queda inactivo.
Las vistas previas están optimizadas para la capacidad de respuesta. Construye teniendo en cuenta estas restricciones:
agent.message almacenado en búfer sigue llegando completo. Nunca trates una vista previa acumulada como definitiva.agent.message que tu vista previa estaba esperando. No hay forma de volver a solicitar los deltas perdidos.agent.thinking solo de inicio: Una vista previa de agent.thinking emite solo el event_start como señal de que ha comenzado un bloque de pensamiento; no le siguen eventos event_delta.event_start y event_delta existen solo en el stream en vivo. No aparecen en el historial de eventos de la sesión (GET /v1/sessions/{session_id}/events) ni en el historial de eventos de ningún hilo de sesión.Si el stream no se comporta como esperas:
| Lo que ves | Lo que significa |
|---|---|
Un stream con eventos almacenados en búfer pero sin event_start ni event_delta | La conexión que estás leyendo no optó por ellos (event_deltas[] se aplica por conexión, no por sesión), o el turno nunca tocó el hilo que estás recibiendo por streaming. Las vistas previas tienen alcance de hilo, así que lista los hilos de la sesión (GET /v1/sessions/{session_id}/threads) para encontrar cuál se ejecutó. |
| Un 404 en la URL del stream | La ruta o un ID es incorrecto, o la solicitud no lleva ningún encabezado beta de managed-agents. Los endpoints de hilos están restringidos por beta, así que sin el encabezado no existen. |
Un 400 que menciona event_deltas | Solo se aceptan agent.message y agent.thinking. |
Cuando el agente invoca una herramienta personalizada:
agent.custom_tool_use que contiene el nombre de la herramienta y la entrada.session.status_idle que contiene stop_reason: requires_action. Los IDs de los eventos bloqueantes están en el arreglo stop_reason.event_ids.user.custom_tool_result por cada una, pasando el ID del evento en el parámetro custom_tool_use_id junto con el contenido del resultado.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:
# Busca el evento de uso de herramienta personalizada y ejecútalo
tool_event = events_by_id[event_id]
result = call_tool(tool_event.name, tool_event.input)
# Envía el resultado de vuelta
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":
breakCuando una política de permisos requiere confirmación antes de que se ejecute una herramienta:
agent.tool_use o agent.mcp_tool_use.session.status_idle que contiene stop_reason: requires_action. Los IDs de los eventos bloqueantes están en el arreglo stop_reason.event_ids.user.tool_confirmation por cada uno, pasando el ID del evento en el parámetro tool_use_id. Establece result en "allow" o "deny". Usa deny_message para explicar una denegación.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:
# Aprueba la llamada a herramienta pendiente
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.tool_confirmation",
"tool_use_id": event_id,
"result": "allow",
},
],
)
case "end_turn":
breakLas sesiones persisten entre interacciones. El historial de la conversación se conserva a menos que la sesión se elimine explícitamente. Cuando una sesión queda inactiva, se crea un punto de control de su sandbox, conservando el estado completo del sandbox, incluido el sistema de archivos, los paquetes instalados y cualquier archivo que el agente haya creado. Esto te permite reanudar limpiamente tras la inactividad.
Para reanudar una sesión, envíale un evento user.message como de costumbre:
# En producción, pasa el ID almacenado de la sesión que quieres reanudar.
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.
YAMLUna sesión creada con un presupuesto se pausa en lugar de gastar de más. Cuando el costo de lista registrado de la sesión alcanza el límite, la plataforma pausa cada hilo antes de su siguiente solicitud al modelo, y la sesión queda inactiva con un stop_reason de budget_reached en lugar de terminar. La solicitud que llevó el total más allá del límite se ejecuta hasta completarse, por lo que el list_cost reportado por la instantánea session.usage puede mostrar un valor igual o ligeramente superior al límite. En el stream, la pausa llega como tres eventos, en orden:
session.thread_status_idle con stop_reason: budget_reached, para cada hilo a medida que se pausa.session.usage, una instantánea del uso acumulado de la sesión y del costo de lista registrado.session.status_idle con stop_reason: budget_reached. El evento session.usage siempre precede inmediatamente a este estado inactivo.Un hilo cuya solicitud final cruza el límite y a la vez completa su turno reporta end_turn en su propio evento session.thread_status_idle mientras la sesión sigue reportando budget_reached; básate en el stop_reason a nivel de sesión para detectar la pausa.
Mientras la sesión está en su límite, solo acepta los eventos que resuelven trabajo ya en curso: user.tool_confirmation, user.tool_result, user.custom_tool_result y user.interrupt. Cualquier evento que iniciaría trabajo nuevo, incluido user.message, se rechaza con un error 400 que menciona esa lista. Cuando una sesión tiene tanto un hilo esperando una solicitud de herramienta como un hilo pausado en el límite, el stop_reason a nivel de sesión es requires_action, no budget_reached: resolver la solicitud no desencadena una solicitud al modelo, así que respóndela como de costumbre.
Ningún evento reanuda una sesión pausada en su límite. En su lugar, actualiza el presupuesto de la sesión: cambiar el límite a cualquier valor por encima del costo de lista consumido, o eliminar el presupuesto actualizando la sesión con "budget": null, reanuda automáticamente el trabajo pausado. Consulta Presupuestos de sesión para ver cómo se registra el costo de lista y la semántica completa de actualización del presupuesto.
Envía un evento system.message para darle al agente contexto privilegiado a nivel de sistema que se aplica al turno que lo acompaña y a todos los turnos posteriores. A diferencia del campo system en la definición del agente (que establece la "system prompt" (indicación del sistema) de nivel superior), el contenido de system.message se agrega al contexto de sistema de la sesión como un turno role: "system" en lugar de reemplazar esa indicación. Úsalo cuando el agente necesite orientación actualizada a nivel de sistema a mitad de sesión: una persona diferente, restricciones revisadas o contexto obtenido en tiempo de ejecución que deba moldear el comportamiento del modelo en adelante.
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."
YAMLMientras la sesión está inactiva con stop_reason: requires_action, un system.message se acepta solo cuando va después de un evento de resultado de herramienta en la misma solicitud; enviado por sí solo o con un user.message, se rechaza hasta que se resuelvan los eventos de herramienta pendientes. content acepta de 1 a 1000 elementos de texto.
El objeto de sesión incluye un campo usage con el uso acumulado de la sesión: recuentos de tokens, uso de herramientas del servidor, tiempo activo y el costo de lista registrado. Obtén la sesión después de que quede inactiva para leer los totales más recientes.
{
"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 informa los tokens de entrada no almacenados en caché y output_tokens informa el total de tokens de salida en todas las llamadas al modelo de la sesión. El campo cache_read_input_tokens informa los tokens leídos desde la caché de prompts, y el objeto cache_creation desglosa los tokens de creación de caché por duración de la caché (ephemeral_5m_input_tokens y ephemeral_1h_input_tokens). Las entradas de caché usan un TTL de 5 minutos de forma predeterminada, por lo que los turnos consecutivos dentro de esa ventana se benefician de las lecturas de caché, que reducen el costo por token.
list_cost es el consumo acumulado de la sesión valorado a las tarifas de lista públicas, como un número entero de centavos en una cadena, con un código de moneda. active_seconds es el tiempo acumulado durante el cual la sesión tuvo al menos un hilo en ejecución; la actividad superpuesta de hilos concurrentes se cuenta una sola vez, a diferencia del active_seconds del objeto stats de la sesión, que suma el tiempo activo propio de cada hilo. Esta cifra deduplicada es la duración sobre la que se calcula el costo de tiempo de ejecución de la sesión. server_tool_use cuenta las solicitudes de herramientas ejecutadas en el servidor para fines de precios: las solicitudes de búsqueda web se incluyen en el costo de lista por solicitud, y las solicitudes de obtención web (web fetch) no tienen cargo por solicitud y no se miden, por lo que web_fetch_requests muestra 0. El usage propio de cada hilo de sesión también incluye list_cost y active_seconds. Las cifras por hilo se redondean de forma independiente y excluyen el costo de tiempo de ejecución de la sesión, por lo que no suman exactamente el list_cost de la sesión; la cifra de la sesión es la autoritativa.
No tienes que consultar periódicamente la sesión para observar estos totales. El evento session.usage lleva la misma instantánea acumulada (el objeto usage, más el budget de la sesión, que es null cuando la sesión no tiene ninguno) en el stream de la sesión y en el historial de eventos. Se emite en las transiciones a inactividad en lugar de con un temporizador: la sesión emite uno inmediatamente antes de quedar inactiva, cualquiera que sea el motivo de detención, y uno cuando un hilo se pausa al alcanzar un presupuesto de sesión. Por lo tanto, un lector del stream ve el costo final de un turno, o del trabajo que alcanzó un presupuesto, sin una consulta adicional.
Para aplicar un límite de gasto, establece un presupuesto de sesión en lugar de consultar el uso y detener la sesión tú mismo. La plataforma calcula el precio del consumo de la sesión de forma continua y pausa cada hilo antes de su siguiente solicitud al modelo una vez que el costo de lista de la sesión alcanza el límite; consulta Alcanzar un presupuesto de sesión para ver cómo se refleja esto en el stream.
La Claude Console proporciona una vista de línea de tiempo visual de tus sesiones de agente. Navega a la sección Claude Managed Agents en la Console para ver:
session.errorWas this page helpful?