Claude Platform Docs
Managed Agents将工作委派给智能体

会话事件流

发送事件、流式传输响应,并在执行过程中中断或重定向您的会话。

与 Claude Managed Agents 的通信是基于事件的。您向智能体发送用户事件,并接收返回的智能体事件和会话事件以跟踪状态。

事件类型

事件在两个方向上流动。

  • 用户事件系统事件是您发送给智能体的内容:user.* 事件启动会话并在其进行过程中引导它;system.message 追加系统级上下文,该上下文适用于随附的轮次以及所有后续轮次。
  • 会话事件跨度事件智能体事件会发送给您,以便您观察会话状态和智能体进度。选择加入的流连接还会接收事件增量

会话、跨度、智能体、用户和系统事件类型字符串遵循 {domain}.{action} 命名约定。仅限流的增量预览事件(event_startevent_delta)是例外。完整目录请参阅参考文档中的事件类型Webhook 事件类型是独立的,其中一些名称与流中的名称不同(例如,session.status_idled 而非 session.status_idle)。

每个持久化事件都包含一个 processed_at 时间戳,在事件处理完成时设置。对于您发送的事件,当事件仍在较早事件之后排队时,processed_at 为 null。例外情况是 user.define_outcomeuser.custom_tool_resultuser.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.",
                },
            ],
        },
    ],
)

该调用在事件排队后立即返回,而中断的 processed_at 在智能体应用它之前保持为 null。正在进行的模型响应会立即停止。当工具调用正在运行时,中断可能需要更长时间才能应用,在此之前会话保持 running 状态。随后 user.interrupt 事件出现在流中,被中断的轮次以 session.status_idle 事件结束。其 stop_reasonend_turn,与自行完成的轮次的值相同;没有专门针对中断的停止原因。智能体以您在中断后发送的 user.message 开始其下一轮次。

事件增量

默认情况下,智能体的响应文本以缓冲的 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.messageagent.thinking;任何其他值都会返回 400 错误,包含超过 100 个值的请求也是如此。子智能体的预览出现在该子智能体自己的线程流上。

当被预览的事件开始时,流会发出一个 event_start,携带即将到来的事件的类型和 id

{
  "type": "event_start",
  "event": {
    "type": "agent.message",
    "id": "sevt_01abc..."
  }
}

对于 agent.message,开始事件之后是携带增量文本的 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_startevent_delta 没有自己的 idprocessed_at。它们携带的唯一标识符是它们所预览事件的 id

累加与对账

每个支持事件增量的 SDK 都包含一个累加器辅助工具,为您处理 index 记账。Go、Java、Ruby 和 C# 辅助工具还会按事件的 id 为累加中的预览建立键;使用 Python、TypeScript 和 PHP 辅助工具时,您需要自己维护该映射,并将每个增量合并到其 id 对应的条目中。当您需要自定义记账时,手动模式在每种语言中同样适用:将其应用于生成的事件类型。

在手动模式中,将预览视为草稿缓冲区,将缓冲事件视为记录。以 (event_id, index) 作为缓冲区的键。按每个模型请求进行对账:一个轮次以单个 session.status_running 事件开始,然后在正常完成的轮次中,每个模型请求按顺序产生 span.model_request_startevent_start、若干 event_delta 事件、缓冲的 agent.message,最后是 span.model_request_end(位于跨度事件选项卡中)。在线路上,这是该序列中被预览的部分,与连接的其他缓冲事件交错出现:

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 行对每个文本片段重复一次。在每个事件到达时处理它:

  1. 收到 event_start 时,记下所宣告的 id。标识符始终一致:event_start.event.id、每个 event_delta.event_id 以及缓冲的 agent.messageid 都是相同的值。
  2. 收到每个 event_delta 时,将 delta.content.text 追加到 (event_id, delta.index) 处的条目,并渲染累积的文本。某个 index 的第一个增量会创建该条目。
  3. 当缓冲的 agent.message 到达时,按 id 匹配它,丢弃累加的预览,并改为渲染该消息的内容。
  4. 收到 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_startevent_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。没有办法重新请求错过的增量。
  • **单线程,仅文本:**预览涵盖连接正在读取的线程上的助手文本。工具使用、工具结果、MCP 结果以及任何其他会话线程上的活动绝不会在该连接上被预览。
  • 仅开始的 agent.thinkingagent.thinking 预览仅发出 event_start 作为思考块已开始的信号;之后不会有 event_delta 事件。
  • 从不持久化:event_startevent_delta 仅存在于实时流中。它们不会出现在会话的事件历史(GET /v1/sessions/{session_id}/events)或任何会话线程的事件历史中。

预览故障排查

如果流的行为不符合您的预期:

您看到的情况含义
流中有缓冲事件但没有 event_startevent_delta您正在读取的连接没有选择加入(event_deltas[] 按连接而非按会话生效),或者该轮次从未触及您正在流式传输的线程。预览是线程作用域的,因此请列出会话的线程(GET /v1/sessions/{session_id}/threads)以找出哪个线程运行过。
流 URL 返回 404路径或某个 ID 有误,或者请求完全没有携带 managed-agents beta 标头。线程端点受 beta 门控,因此没有该标头时它们不存在。
指明 event_deltas 的 400 错误仅接受 agent.messageagent.thinking

其他场景

处理自定义工具调用

当智能体调用自定义工具时:

  1. 会话发出一个包含工具名称和输入的 agent.custom_tool_use 事件。
  2. 会话以包含 stop_reason: requires_actionsession.status_idle 事件暂停。阻塞事件 ID 位于 stop_reason.event_ids 数组中。
  3. 在您的系统中执行工具,并为每个工具发送一个 user.custom_tool_result 事件,在 custom_tool_use_id 参数中传递事件 ID 以及结果内容。
  4. 一旦所有阻塞事件都得到解决,会话将转换回 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

工具确认

权限策略要求在工具执行前进行确认时:

  1. 会话发出一个 agent.tool_useagent.mcp_tool_use 事件。
  2. 会话以包含 stop_reason: requires_actionsession.status_idle 事件暂停。阻塞事件 ID 位于 stop_reason.event_ids 数组中。
  3. 为每个事件发送一个 user.tool_confirmation 事件,在 tool_use_id 参数中传递事件 ID。将 result 设置为 "allow""deny"。使用 deny_message 解释拒绝原因。
  4. 一旦所有阻塞事件都得到解决,会话将转换回 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_reachedstop_reason 进入空闲状态,而不是终止。使总额超过上限的那个请求会运行至完成,因此 session.usage 快照报告的 list_cost 可能显示为等于或略微超过上限。在流上,暂停以三个事件的形式按顺序到达:

  1. 带有 stop_reason: budget_reachedsession.thread_status_idle,每个线程暂停时各发出一个。
  2. session.usage,会话累计用量和跟踪的标价成本的快照。
  3. 带有 stop_reason: budget_reachedsession.status_idlesession.usage 事件始终紧接在此空闲事件之前。

如果某个线程的最后一个请求既越过了上限又完成了其轮次,则该线程在其自己的 session.thread_status_idle 事件上报告 end_turn,而会话仍报告 budget_reached;请以会话级 stop_reason 为依据来检测暂停。

当会话处于上限时,它仅接受用于结清已在进行中工作的事件:user.tool_confirmationuser.tool_resultuser.custom_tool_resultuser.interrupt。任何会启动新工作的事件(包括 user.message)都会被拒绝,并返回指明该列表的 400 错误。当会话同时有一个线程在等待工具询问、另一个线程在上限处暂停时,会话级 stop_reasonrequires_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_tokensephemeral_1h_input_tokens)细分缓存创建令牌数。缓存条目默认使用 5 分钟的 TTL,因此在该时间窗口内连续进行的轮次可以受益于缓存读取,从而降低每令牌成本。

list_cost 是会话按公开标价计算的累计消耗,以字符串形式表示的整数美分数,并附带货币代码。active_seconds 是会话中至少有一个线程在运行的累计时间;并发线程的重叠活动只计算一次,这与会话 stats 对象中的 active_seconds 不同,后者是将每个线程各自的活跃时间相加。这个去重后的数值是会话运行时成本计价所依据的时长。server_tool_use 统计用于计价的服务器执行的工具请求数:网页搜索请求按每次请求计入标价成本,而网页抓取请求不按请求收费且不计量,因此 web_fetch_requests 显示为 0。每个会话线程自身的 usage 也包含 list_costactive_seconds。每个线程的数值是独立四舍五入的,并且不包含会话的运行时间成本,因此它们的总和不会精确等于会话的 list_cost;会话级别的数值才是权威数据。

您无需轮询会话来观察这些总计数据。session.usage 事件在会话流和事件历史中携带相同的累计快照(usage 对象,加上会话的 budget,当会话没有预算时该值为 null)。它在空闲状态转换时发出,而不是按定时器发出:无论停止原因是什么,会话都会在进入空闲状态之前立即发出一次,并在线程因达到会话预算而暂停时发出一次。因此,流读取方无需额外获取即可看到一个轮次的最终成本,或触及预算的那部分工作的最终成本。

要强制执行支出限额,请设置会话预算,而不是自行轮询用量并停止会话。平台会持续对会话的消耗进行计价,一旦会话的标价成本达到上限,就会在每个线程发出下一次模型请求之前将其暂停;有关这在流中的表现,请参阅达到会话预算

Console 可观测性

Claude Console 包含一个会话查看器,无需编写任何代码即可检查智能体执行了哪些操作。在 Console 侧边栏的 Managed Agents 下,选择 Sessions 即可查看工作区中的每个会话及其状态、智能体、令牌用量、成本和创建时间,然后选择一个会话将其打开。会话查看器仅对开发者和管理员开放。它显示:

  • 时间线缩略图: 会话活动随时间变化的可缩放概览,在多智能体会话中每个线程占一条通道。选择一条通道可查看该线程,或选择一个标记可跳转到对应事件。
  • 对话记录: 按模型请求分组的对话,包括思考、工具调用及其输入和结果,以及流式传输中的消息文本。您可以筛选事件,并将其复制或下载为 JSON。
  • 检查器: 一个可调整大小的侧面板,包含会话的详细信息,分为五个标签页:
    • Session 显示会话的详细信息和元数据、其随时间累计的成本,以及在设置了会话预算时相对于预算的支出情况。
    • Events 按服务器发送的顺序列出当前线程上的每个原始事件;选择一个事件可查看其 JSON。在页面打开期间流式传输的消息还具有一个 Deltas 视图,用于查看其事件增量
    • Tools 列出会话的智能体所配置的工具,以及调用次数、失败次数和中位持续时间;选择一个工具可查看其调用,并跳转到对话记录中的某次调用。
    • Resources 列出挂载在各自容器路径下的文件代码仓库记忆存储,包括每个存储中的记忆以及本会话对它们所做的更改,此外还有智能体写入 /mnt/session/outputs 的文件,以及附加到会话智能体的技能
    • Threads 列出每个线程及其状态、上下文大小和成本。选择一个线程可查看其详细信息,例如智能体、模型、上下文用量和成本。

在会话 URL 后追加 ?event={event_id} 可在特定事件处打开该会话。

调试技巧

  • 检查会话事件: 会话错误通过 session.error 事件传达
  • 查看工具结果: 工具执行失败通常可以解释智能体的意外行为
  • 跟踪令牌用量: 监控令牌消耗以优化提示并降低成本
  • 使用系统提示: 在系统提示中添加日志记录指令,让智能体解释其推理过程
  • 排查预览问题: 如果选择启用事件增量的流未按预期运行,请参阅排查预览问题

Was this page helpful?