与 Claude Managed Agents 的通信是基于事件的。您向智能体发送用户事件,并接收返回的智能体事件和会话事件以跟踪状态。
事件在两个方向上流动。
user.* 事件启动会话并在其进行过程中引导它;system.message 追加系统级上下文,该上下文适用于随附的轮次以及所有后续轮次。会话、span、智能体、用户和系统事件类型字符串遵循 {domain}.{action} 命名约定。仅限流的增量预览事件(event_start、event_delta)是例外。完整目录请参阅参考文档中的事件类型。
每个持久化事件都包含一个 processed_at 时间戳,该时间戳在事件处理完成时设置。对于您发送的事件,当事件仍在较早事件之后排队时,processed_at 为 null。例外情况是 user.define_outcome、user.custom_tool_result 和 user.tool_result,它们在接收时即被处理,并在回显时已填充 processed_at。
发送 user.message 事件以启动或继续智能体的工作:
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",
},
],
},
],
)发送 user.interrupt 事件以在执行过程中停止智能体,然后跟进一个 user.message 事件来重定向它:
# 智能体当前正在分析文件...
# 用新的指令中断:
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.",
},
],
},
],
)智能体确认中断并切换到新任务。被中断的轮次以一个 session.status_idle 事件结束,其 stop_reason 为 end_turn,与自行完成的轮次的值相同;没有专门针对中断的停止原因。
默认情况下,智能体的响应文本以缓冲的 agent.message 事件形式到达流,每个事件仅在产生它的模型请求完成后才发出。"Event deltas"(事件增量)让您可以在模型仍在生成文本时,以实时预览的形式增量渲染该文本。预览并不是响应本身:预览是一种尽力而为的显示辅助,而缓冲的 agent.message 始终是权威记录。忽略预览的客户端仍会收到完整、正确的流。
预览按每个流连接选择加入。将 event_deltas[] 查询参数添加到您正在读取的流中,对每个您希望预览的事件类型重复一次。由于 [] 是 shell 通配符模式,每当您在 shell 中构建请求时请为 URL 加引号;示例中将方括号百分号编码为 %5B%5D,这同样有效。两个流端点都接受该参数:位于 GET /v1/sessions/{session_id}/events/stream 的会话级流,以及每个会话线程自己的流,位于 GET /v1/sessions/{session_id}/threads/{thread_id}/stream。接受的值为 agent.message 和 agent.thinking;任何其他值都会返回 400 错误,包含超过 100 个值的请求也是如此。子智能体的预览出现在该子智能体自己的线程流上。
当被预览的事件开始时,流会发出一个 event_start,携带即将到来的事件的类型和 id:
{
"type": "event_start",
"event": {
"type": "agent.message",
"id": "sevt_01abc..."
}
}对于 agent.message,start 之后跟随携带增量文本的 event_delta 事件。每个增量在 event_id 中指明它所扩展的事件,并在 delta.index 中指明它所扩展的内容块:
{
"type": "event_delta",
"event_id": "sevt_01abc...",
"delta": {
"type": "content_delta",
"index": 0,
"content": {
"type": "text",
"text": "Here is the summary"
}
}
}当 agent.thinking 事件被预览时,仅发出 event_start。之后不会有 event_delta 事件,并且结束该预览的缓冲 agent.thinking 事件不携带任何思考内容;它是一个进度信号,而不是内容载体。
与持久化事件不同,event_start 和 event_delta 没有自己的 id 或 processed_at。它们携带的唯一标识符是它们所预览事件的 id。
每个支持事件增量的 SDK 都包含一个累加器辅助工具,为您处理 index 的簿记工作。Go、Java、Ruby 和 C# 的辅助工具还会按事件的 id 为正在累加的预览建立键;使用 Python、TypeScript 和 PHP 的辅助工具时,您需要自己维护该映射,并将每个增量合并到其 id 对应的条目中。当您需要自定义簿记时,手动模式在每种语言中同样适用:将其应用于生成的事件类型即可。
在手动模式中,将预览视为草稿缓冲区,将缓冲事件视为记录。以 (event_id, index) 作为缓冲区的键。按每个模型请求进行对账:一个轮次以单个 session.status_running 事件开始,然后在正常完成的轮次中,每个模型请求按顺序产生 span.model_request_start、event_start、若干 event_delta 事件、缓冲的 agent.message,最后是 span.model_request_end(位于 Span 事件选项卡中)。在线路上,这是该序列中被预览的部分,与该连接的其他缓冲事件交错出现:
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": [...]}event_delta 行对每个文本片段重复一次。在每个事件到达时进行处理:
event_start 时,记下所宣告的 id。标识符始终一致:event_start.event.id、每个 event_delta.event_id 以及缓冲的 agent.message 的 id 都是同一个值。event_delta 时,将 delta.content.text 追加到 (event_id, delta.index) 处的条目,并渲染累积的文本。某个 index 的第一个增量会创建该条目。agent.message 到达时,按 id 匹配它,丢弃已累加的预览,改为渲染该消息的内容。span.model_request_end 时,关闭任何尚未被其缓冲事件对账的预览。不会再有针对它的增量到来。如果轮次出错或被中断,缓冲事件可能永远不会到达;但 span.model_request_end 仍然会到达。该模式所依赖的保证:
(event_id, index) 为键拼接预览的增量,会得到缓冲事件中 content[index].text 的一个前缀(是前缀,不一定是完整文本,因为在负载下增量可能被丢弃)。event_id 最多发出一个 event_start,并且缓冲事件是该连接为该 id 传递的最后一项内容。# 预览快照,以事件 id 为键。accumulate_managed_agents_event 将每个
# event_start / event_delta 折叠为一个 agent.message 快照;缓冲的
# agent.message 会将其替换。
previews: dict[str, BetaManagedAgentsAgentMessageEvent] = {}
# 在此连接上启用 agent.message 预览
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":
# 缓冲事件才是正式记录:它会替换并关闭预览
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":
# 不会再有增量到来。关闭所有其
# 缓冲事件从未到达的预览。
for event_id in previews:
print(f"span.model_request_end closing preview for {event_id}")
previews.clear()
case "session.status_idle":
break在多智能体会话中,每个会话线程在 GET /v1/sessions/{session_id}/threads/{thread_id}/stream 都有自己的事件流,并且它接受相同的 event_deltas[] 参数和相同的值。预览在设计上是线程作用域的:一个连接只预览它正在读取的线程。子线程的预览在该子线程自己的流上传递,绝不会交叉发布到会话级流,会话级流的预览始终限定于主线程。要在模型生成时观看子智能体的文本,请打开该子智能体的线程流。
线程流的路径很容易弄错:它是 /threads/{thread_id}/stream,而不是 /events/stream(后者仅存在于会话级别),并且不存在 /threads/{thread_id}/events/stream 端点。
预览事件本身不会改变。event_start 和 event_delta 在线程流上的形状与在会话级流上相同,累加与对账模式按原样适用。唯一的调整是簿记:为每个流连接运行一个累加器实例。
# 列出会话的线程并选择一个子线程:子线程带有非空的
# parent_thread_id,而主线程的 parent_thread_id 为 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'
)
# 子线程的流接受与会话流相同的 event_deltas[] 参数。
# 对方括号进行百分号编码(%5B%5D)并为 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)
# 缓冲的事件是权威记录;渲染其内容。
printf '\n'
jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
printf '\n'
;;
session.thread_status_idle)
break
;;
esac
done
exec {stream}<&-读取循环在收到 session.thread_status_idle 时退出,该事件在会话线程的轮次完成且线程进入空闲状态时发出。
预览针对响应速度进行了调优。请基于以下约束进行构建:
agent.message 仍会完整到达。切勿将已累加的预览视为最终结果。agent.message。没有办法重新请求错过的增量。agent.thinking:agent.thinking 预览仅发出 event_start 作为思考块已开始的信号;之后不会有 event_delta 事件。event_start 和 event_delta 仅存在于实时流上。它们不会出现在会话的事件历史(GET /v1/sessions/{session_id}/events)或任何会话线程的事件历史中。如果流的行为不符合您的预期:
| 您看到的情况 | 含义 |
|---|---|
流中有缓冲事件但没有 event_start 或 event_delta | 您正在读取的连接没有选择加入(event_deltas[] 按连接生效,而非按会话),或者该轮次从未触及您正在流式传输的线程。预览是线程作用域的,因此请列出会话的线程(GET /v1/sessions/{session_id}/threads)以找出运行的是哪一个。 |
| 流 URL 返回 404 | 路径或某个 ID 有误,或者请求完全没有携带 managed-agents beta 标头。线程端点受 beta 门控,因此没有该标头时它们不存在。 |
指明 event_deltas 的 400 错误 | 仅接受 agent.message 和 agent.thinking。 |
当智能体调用自定义工具时:
agent.custom_tool_use 事件。stop_reason: requires_action 的 session.status_idle 事件暂停。阻塞事件的 ID 位于 stop_reason.event_ids 数组中。user.custom_tool_result 事件,在 custom_tool_use_id 参数中传递事件 ID 以及结果内容。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:
# 查找自定义工具使用事件并执行它
tool_event = events_by_id[event_id]
result = call_tool(tool_event.name, tool_event.input)
# 将结果发送回去
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":
break当权限策略要求在工具执行前进行确认时:
agent.tool_use 或 agent.mcp_tool_use 事件。stop_reason: requires_action 的 session.status_idle 事件暂停。阻塞事件的 ID 位于 stop_reason.event_ids 数组中。user.tool_confirmation 事件,在 tool_use_id 参数中传递事件 ID。将 result 设置为 "allow" 或 "deny"。使用 deny_message 解释拒绝原因。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:
# 批准待处理的工具调用
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.tool_confirmation",
"tool_use_id": event_id,
"result": "allow",
},
],
)
case "end_turn":
break会话在交互之间持久存在。除非会话被显式删除,否则对话历史会被保留。当会话进入空闲状态时,其沙箱会被创建检查点,保留完整的沙箱状态,包括文件系统、已安装的软件包以及智能体创建的任何文件。这使您可以从不活动状态干净地恢复。
要恢复会话,请像往常一样向其发送 user.message 事件:
# 在生产环境中,传入您想要恢复的会话的已存储 ID。
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.
YAML使用预算创建的会话会暂停而不是超支。当会话跟踪的标价成本达到上限时,平台会在每个线程的下一次模型请求之前暂停该线程,会话以 budget_reached 的 stop_reason 进入空闲状态,而不是终止。使总额超过上限的那个请求会运行至完成,因此 session.usage 快照报告的 list_cost 可能显示为等于或略微超过上限。在流上,暂停以三个事件的形式按顺序到达:
stop_reason: budget_reached 的 session.thread_status_idle,每个线程暂停时各发出一个。session.usage,会话累计用量和跟踪的标价成本的快照。stop_reason: budget_reached 的 session.status_idle。session.usage 事件始终紧接在此空闲事件之前。如果某个线程的最后一个请求既越过了上限又完成了其轮次,则该线程在自己的 session.thread_status_idle 事件上报告 end_turn,而会话仍报告 budget_reached;请以会话级的 stop_reason 为依据来检测暂停。
当会话处于上限时,它只接受用于结清已在进行中工作的事件:user.tool_confirmation、user.tool_result、user.custom_tool_result 和 user.interrupt。任何会启动新工作的事件(包括 user.message)都会被拒绝,并返回一个指明该列表的 400 错误。当会话同时有一个线程在等待工具询问、另一个线程在上限处暂停时,会话级的 stop_reason 是 requires_action,而不是 budget_reached:结清该询问不会触发模型请求,因此请像往常一样响应它。
没有任何事件可以恢复在上限处暂停的会话。相反,请更新会话的预算:将上限更改为高于已消耗标价成本的任何值,或通过使用 "budget": null 更新会话来移除预算,都会自动恢复暂停的工作。有关标价成本的跟踪方式以及完整的预算更新语义,请参阅会话预算。
发送 system.message 事件,为智能体提供特权的系统级上下文,该上下文适用于随附的轮次以及所有后续轮次。与智能体定义上的 system 字段(用于设置顶层系统提示)不同,system.message 的内容会作为一个 role: "system" 轮次追加到会话的系统上下文中,而不是替换该提示。当智能体在会话中途需要更新的系统级指导时使用它:不同的角色设定、修订后的约束,或在运行时获取的、应当影响模型后续行为的上下文。
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."
YAML当会话以 stop_reason: requires_action 处于空闲状态时,system.message 仅在同一请求中跟随在工具结果事件之后时才会被接受;单独发送或与 user.message 一起发送时,它会被拒绝,直到待处理的工具事件得到解决。content 接受 1–1000 个文本项。
会话对象包含一个 usage 字段,其中记录了该会话的累计用量:令牌计数、服务器工具使用、活跃时间以及跟踪的标价成本。在会话进入空闲状态后获取该会话,即可读取最新的总计数据。
{
"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 报告未缓存的输入令牌数,output_tokens 报告会话中所有模型调用的输出令牌总数。cache_read_input_tokens 字段报告从提示缓存中读取的令牌数,而 cache_creation 对象按缓存生命周期(ephemeral_5m_input_tokens 和 ephemeral_1h_input_tokens)细分缓存创建令牌数。缓存条目默认使用 5 分钟的 TTL,因此在该时间窗口内连续进行的轮次可以受益于缓存读取,从而降低每令牌成本。
list_cost 是会话按公开标价计算的累计消耗,以字符串形式表示的整数美分数,并附带货币代码。active_seconds 是会话中至少有一个线程在运行的累计时间;并发线程的重叠活动只计算一次,这与会话 stats 对象中的 active_seconds 不同,后者是将每个线程各自的活跃时间相加。这个去重后的数值是会话运行时成本计价所依据的时长。server_tool_use 统计用于计价的服务器执行的工具请求数:网页搜索请求按每次请求计入标价成本,而网页抓取请求不收取每次请求费用且不计量,因此 web_fetch_requests 显示为 0。每个会话线程自身的 usage 也包含 list_cost 和 active_seconds。每个线程的数值是独立四舍五入的,并且不包含会话的运行时间成本,因此它们的总和不会精确等于会话的 list_cost;会话级别的数值才是权威数值。
您无需轮询会话来观察这些总计数据。session.usage 事件在会话流和事件历史中携带相同的累计快照(usage 对象,加上会话的 budget,当会话没有预算时该值为 null)。它在空闲状态转换时发出,而不是按定时器发出:无论停止原因是什么,会话都会在进入空闲状态之前立即发出一次,并在线程因会话预算而暂停时发出一次。因此,流读取器无需额外获取即可看到一个轮次的最终成本,或触及预算的工作的最终成本。
要强制执行支出限制,请设置会话预算,而不是自行轮询用量并停止会话。平台会持续对会话的消耗进行计价,一旦会话的标价成本达到上限,就会在每个线程发出下一次模型请求之前将其暂停;有关这在流上的表现,请参阅达到会话预算。
Claude Console 为您的智能体会话提供可视化时间线视图。导航到 Console 中的 Claude Managed Agents 部分即可查看:
session.error 事件传达Was this page helpful?