事件和事件流
应用提交任务后,需要持续接收回答、工具调用和状态变化,才能判断云端执行的结果。AgentAPI 将这些记录保存在会话中,应用可以通过 SSE 实时消费,也可以查询历史。
涉及通知、写库或其他外部副作用时,应用应在自己的事务中同时记录业务状态和消费进度,并为可重试操作设置业务幂等键。
无需持续保持 SSE 连接的资源变更或会话状态通知,可在 Portal 配置 Webhook。Webhook 不转发所有会话事件;收到通知后仍需读取停止原因、回答和产物,不能把会话空闲视为任务成功。
执行结果判断
应用收到 agent.message 时可以展示回答;收到 session.status_idle 时,才知道本轮已停止。停止不一定代表任务完成,需要根据 stop_reason 决定下一步:
| 停止原因 | 应用的下一步 |
|---|---|
end_turn |
检查最终回答和任务产物,确认满足业务要求。 |
requires_action |
按 stop_reason.event_ids 找到阻塞事件,处理自定义工具或工具确认。 |
interrupted |
记录中断,检查已完成的操作;中断不会撤销外部修改。 |
budget_reached |
检查预算和部分产物,再决定是否继续。 |
retries_exhausted |
读取错误信息,解决问题后再发送输入。 |
状态与停止原因
native的人工中断也使用end_turn,需结合user.interrupt事件判断。session.error本身不代表会话已终止,应继续检查后续状态;进入terminated的会话不能继续接收任务输入。
建立事件消费循环
事件接入方式
创建会话后,保存会话 ID,并用它完成以下操作。请求路径均以 /v1/sessions/{id} 为前缀。
| 操作 | 请求方法与路径后缀 | 使用场景 |
|---|---|---|
| 发送事件 | POST /events |
提交用户消息、中断、工具结果或确认。请求返回表示事件已接收,任务结果需从后续事件读取。 |
| 订阅事件流 | GET /events/stream |
通过 SSE 展示执行进度。连接先回放历史,再推送实时事件。 |
| 读取事件历史 | GET /events |
分页读取已保存的记录,用于查询、审计或恢复应用状态。 |
消费循环应先订阅事件流,再发送用户消息,并持续处理回答和状态变化。应用重启或连接中断后,按回放和去重恢复消费。
订阅回答和执行进度
消费端先打开 SSE 连接,再提交输入。事件流按顺序回放历史并继续推送新事件;收到 session.status_idle 后,连接仍保持打开,应用根据执行结果决定后续操作。
完整的 agent.message 用于展示回答。若 Native 应用需要提前展示生成中的文字,应在发送输入前订阅增量预览;这类预览不写入历史,重连后不能补回。
Native 会话可通过 event_deltas[]=agent.message 订阅当前模型请求的文本预览,其他运行时忽略此参数:
- 正常结束:依次收到
event_start、若干event_delta和同一 ID 的完整agent.message。 - 请求失败或中断:不返回完整消息,以
span.model_request_end结束预览。 - 增量仅对当前连接有效,不写入历史,按模型输出尽力推送,不保证逐字符或逐 token。
例如,客户端可能收到以下 SSE 帧(event_delta 的数量和每段文本长度由模型服务决定):
event: event_start
data: {"type":"event_start","event":{"type":"agent.message","id":"evt_..."}}
event: event_delta
data: {"type":"event_delta","event_id":"evt_...","delta":{"type":"content_delta","index":0,"content":{"type":"text","text":"你"}}}
event: event_delta
data: {"type":"event_delta","event_id":"evt_...","delta":{"type":"content_delta","index":0,"content":{"type":"text","text":"好"}}}
event: agent.message
data: {"type":"agent.message","id":"evt_...","content":[{"type":"text","text":"你好"}]}
单轮示例的适用范围
下面的示例用于新建会话的一轮执行,在收到
session.status_idle或错误时退出。已有会话的流会先回放历史,其中的旧idle不代表新一轮已经结束。持续运行的应用应使用回放和去重示例,并按停止原因处理工具等待和错误,不能仅凭idle判定任务成功。
cURL
# 先打开流,再发送用户消息
exec {stream}< <(
curl --fail-with-body -sS -N \
"$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events/stream?event_deltas%5B%5D=agent.message" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-H "accept: text/event-stream"
)
curl --fail-with-body -sS \
"$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-d @- >/dev/null <<'EOF'
{
"events": [
{
"type": "user.message",
"input": {"parts": [{"type": "text", "text": "Summarize the repo README"}]}
}
]
}
EOF
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
agent.message)
jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
;;
session.status_idle)
break
;;
session.error)
printf '\n[Error: %s]\n' "$(jq -r '.error.message // "unknown"' <<<"$event_json")"
break
;;
esac
done
exec {stream}<&-
Python
# 先打开流,再发送用户消息
with client.beta.sessions.events.stream(session.id) as stream:
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.message",
"content": [{"type": "text", "text": "Summarize the repo README"}],
},
],
)
for event in stream:
match event.type:
case "agent.message":
for block in event.content:
if block.type == "text":
print(block.text, end="")
case "session.status_idle":
break
case "session.error":
error_message = event.error.message if event.error else "unknown"
print(f"\n[Error: {error_message}]")
break
TypeScript
// 先打开流,再发送用户消息
const stream = await client.beta.sessions.events.stream(session.id);
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.message",
content: [{ type: "text", text: "Summarize the repo README" }]
}
]
});
for await (const event of stream) {
if (event.type === "agent.message") {
for (const block of event.content) {
if (block.type === "text") {
process.stdout.write(block.text);
}
}
} else if (event.type === "session.status_idle") {
break;
} else if (event.type === "session.error") {
console.log(`\n[Error: ${event.error?.message ?? "unknown"}]`);
break;
}
}
C#
// 先打开流,然后再发送用户消息
using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserMessageEventParams
{
Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
Content =
[
new BetaManagedAgentsTextBlock
{
Type = BetaManagedAgentsTextBlockType.Text,
Text = "Summarize the repo README",
},
],
},
],
});
await foreach (var streamEvent in stream.Enumerate())
{
if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
{
foreach (var block in message.Content)
{
Console.Write(block.Text);
}
}
else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent)
{
break;
}
else if (streamEvent.Value is BetaManagedAgentsSessionErrorEvent error)
{
Console.WriteLine($"\n[Error: {error.Error?.Message ?? "unknown"}]");
break;
}
}
Go
// 先打开流,然后发送用户消息
stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
defer stream.Close()
if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: "Summarize the repo README",
},
}},
},
}},
}); err != nil {
panic(err)
}
events:
for stream.Next() {
switch event := stream.Current().AsAny().(type) {
case anthropic.BetaManagedAgentsAgentMessageEvent:
// 具体类型列表:BetaManagedAgentsTextBlock
for _, block := range event.Content {
fmt.Print(block.Text)
}
case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
break events
case anthropic.BetaManagedAgentsSessionErrorEvent:
fmt.Printf("\n[Error: %s]\n", cmp.Or(event.Error.Message, "unknown"))
break events
}
}
if err := stream.Err(); err != nil {
panic(err)
}
Java
// 先打开流,然后发送用户消息
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserMessageEventParams.builder()
.type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
.addTextContent("Summarize the repo README")
.build())
.build()
);
Iterable<BetaManagedAgentsStreamSessionEvents> events = stream.stream()::iterator;
for (var event : events) {
if (event.isAgentMessage()) {
event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text())));
} else if (event.isSessionStatusIdle()) {
break;
} else if (event.isSessionError()) {
// `message` 字段在所有错误变体中都存在;从原始 JSON 中读取它。
var errorMessage =
event.asSessionError().error()._json().orElse(null) instanceof JsonObject json
? json.values().get("message").asStringOrThrow()
: "unknown";
IO.println("\n[Error: " + errorMessage + "]");
break;
}
}
}
PHP
// 先打开流,再发送用户消息
$stream = $client->beta->sessions->events->streamStream($session->id);
$client->beta->sessions->events->send(
$session->id,
events: [
[
'type' => 'user.message',
'content' => [['type' => 'text', 'text' => 'Summarize the repo README']],
],
],
);
foreach ($stream as $event) {
match ($event->type) {
'agent.message' => array_walk(
$event->content,
static fn ($block) => $block->type === 'text' ? print($block->text) : null,
),
'session.error' => printf("\n[Error: %s]", $event->error?->message ?? 'unknown'),
default => null,
};
if ($event->type === 'session.status_idle' || $event->type === 'session.error') {
break;
}
}
$stream->close();
Ruby
# 先打开流,再发送用户消息
stream = client.beta.sessions.events.stream_events(session.id)
client.beta.sessions.events.send_(
session.id,
events: [{
type: "user.message",
content: [{type: "text", text: "Summarize the repo README"}]
}]
)
stream.each do |event|
case event.type
in :"agent.message"
event.content.each { print it.text }
in :"session.status_idle"
break
in :"session.error"
puts "\n[Error: #{event.error&.message || "unknown"}]"
break
else
# 忽略其他事件类型
end
end
提交任务输入
订阅建立后,发送 user.message 启动执行。下面提交文件性能分析任务;请求返回仅表示输入已接收,回答和执行结果仍需从事件流读取。
cURL
curl --fail-with-body -sS "$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-d @- <<'EOF'
{
"events": [
{
"type": "user.message",
"input": {"parts": [
{"type": "text", "text": "Analyze the performance of the sort function in utils.py"}
]}
}
]
}
EOF
Python
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",
},
],
},
],
)
TypeScript
await 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",
},
],
},
],
});
C#
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserMessageEventParams
{
Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
Content =
[
new BetaManagedAgentsTextBlock
{
Type = BetaManagedAgentsTextBlockType.Text,
Text = "Analyze the performance of the sort function in utils.py",
},
],
},
],
});
Go
if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: "Analyze the performance of the sort function in utils.py",
},
}},
},
}},
}); err != nil {
panic(err)
}
Java
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserMessageEventParams.builder()
.type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
.addTextContent("Analyze the performance of the sort function in utils.py")
.build())
.build());
PHP
$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',
],
],
],
],
);
Ruby
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"
}
]
}
]
)
恢复事件消费
页面刷新、网络断开或应用重启后,重新订阅同一个会话。服务端会回放历史,应用需要保存已处理的事件 ID,避免重复展示回答或重复触发业务处理:
- 打开一个新流。
- 依次消费服务端先回放的历史事件和随后到达的实时事件。
- 对每个事件按 ID 去重,再交给业务逻辑;不要仅因回放历史中的
session.status_idle而关闭连接。
下面的 loadSeenEventIds、persistSeenEventId(以及其他语言的对应函数)需由应用实现。示例持续消费回放和实时事件;页面关闭、会话终止或其他业务条件满足时,由应用关闭连接。
业务处理与消费进度
下面的展示去重示例在处理前记录 ID,崩溃时可能遗漏展示。涉及通知、写库或其他副作用时,应在应用自己的事务中同时提交业务状态、消费进度和待发送通知,并使用业务幂等键处理重试。
cURL
exec {stream}< <(
curl --fail-with-body -sS -N \
"$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events/stream" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-H "accept: text/event-stream"
)
# 从应用自己的持久化状态加载已经交给业务逻辑的事件 ID。
declare -A seen_event_ids
# SSE 会先回放历史,再继续推送实时事件。
while IFS= read -r -u "$stream" event_line; do
[[ $event_line == data:* ]] || continue
event_json=${event_line#data: }
event_id=$(jq -r '.id' <<<"$event_json")
[[ -n ${seen_event_ids[$event_id]+seen} ]] && continue
seen_event_ids[$event_id]=1
case $(jq -r '.type' <<<"$event_json") in
agent.message)
jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
;;
session.status_idle)
# 历史事件中的 idle 不代表新一轮已完成;连接继续等待。
;;
esac
done
exec {stream}<&-
Python
seen_event_ids = load_seen_event_ids(session.id) # 从应用存储恢复
with client.beta.sessions.events.stream(session.id) as stream:
# 服务端会先回放历史,然后继续推送实时事件。
for event in stream:
if event.id in seen_event_ids:
continue
seen_event_ids.add(event.id)
persist_seen_event_id(session.id, event.id)
match event.type:
case "agent.message":
for block in event.content:
if block.type == "text":
print(block.text, end="")
TypeScript
const seenEventIds = await loadSeenEventIds(session.id); // 应用自己的持久化状态
const stream = await client.beta.sessions.events.stream(session.id);
// 服务端会先回放历史,然后继续推送实时事件。
for await (const event of stream) {
if (seenEventIds.has(event.id)) continue;
seenEventIds.add(event.id);
await persistSeenEventId(session.id, event.id);
if (event.type === "agent.message") {
for (const block of event.content) {
if (block.type === "text") {
process.stdout.write(block.text);
}
}
}
}
C#
using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);
// 从应用自己的持久化状态加载已处理的事件 ID。
HashSet<string> seenEventIds = await LoadSeenEventIds(session.ID);
// 服务端会先回放历史,然后继续推送实时事件。
await foreach (var streamEvent in stream.Enumerate())
{
if (!seenEventIds.Add(streamEvent.ID))
{
continue;
}
await PersistSeenEventId(session.ID, streamEvent.ID);
if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
{
foreach (var block in message.Content)
{
Console.Write(block.Text);
}
}
}
Go
stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
defer stream.Close()
// 从应用自己的持久化状态加载已处理的事件 ID。
seenEventIDs := loadSeenEventIDs(session.ID)
// 服务端会先回放历史,然后继续推送实时事件。
for stream.Next() {
event := stream.Current()
if _, seen := seenEventIDs[event.ID]; seen {
continue
}
seenEventIDs[event.ID] = struct{}{}
persistSeenEventID(session.ID, event.ID)
switch event := event.AsAny().(type) {
case anthropic.BetaManagedAgentsAgentMessageEvent:
// 具体类型列表:BetaManagedAgentsTextBlock
for _, block := range event.Content {
fmt.Print(block.Text)
}
}
}
if err := stream.Err(); err != nil {
panic(err)
}
Java
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
// 从应用自己的持久化状态加载已处理的事件 ID。
var seenEventIds = loadSeenEventIds(session.id());
// 服务端会先回放历史,然后继续推送实时事件。
stream.stream()
.filter(event -> event._json().orElseThrow() instanceof JsonObject json
&& seenEventIds.add(json.values().get("id").asStringOrThrow()))
.filter(BetaManagedAgentsStreamSessionEvents::isAgentMessage)
.forEach(event -> event.asAgentMessage().content()
.forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text()))));
}
PHP
$stream = $client->beta->sessions->events->streamStream($session->id);
// 从应用自己的持久化状态加载已处理的事件 ID。
$seenEventIds = loadSeenEventIds($session->id);
// 服务端会先回放历史,然后继续推送实时事件。
foreach ($stream as $event) {
if (isset($seenEventIds[$event->id])) {
continue;
}
$seenEventIds[$event->id] = true;
persistSeenEventId($session->id, $event->id);
match ($event->type) {
'agent.message' => array_walk(
$event->content,
static fn ($block) => $block->type === 'text' ? print($block->text) : null,
),
default => null,
};
}
$stream->close();
Ruby
stream = client.beta.sessions.events.stream_events(session.id)
# 从应用自己的持久化状态加载已处理的事件 ID。
seen_event_ids = load_seen_event_ids(session.id)
# 服务端会先回放历史,然后继续推送实时事件。
stream.each do |event|
next unless seen_event_ids.add?(event.id)
persist_seen_event_id(session.id, event.id)
case event.type
in :"agent.message"
event.content.each { print it.text }
else
# 忽略其他事件类型
end
end
查询历史事件
需要重建对话记录或排查已结束的任务时,使用事件列表接口。列表默认分页:以下 cURL 示例持续读取 next_page,直到取完历史;SDK 应使用自动分页迭代器,或将 next_page 作为下一次请求的 page 参数。
cURL
page=""
while :; do
url="$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events"
[[ -z "$page" ]] || url+="?page=$(printf '%s' "$page" | jq -sRr @uri)"
response=$(curl --fail-with-body -sS "$url" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json")
jq -r '.data[] | "[\(.type)] \(.processed_at)"' <<<"$response"
page=$(jq -r '.next_page // empty' <<<"$response")
[[ -n "$page" ]] || break
done
Python
events = client.beta.sessions.events.list(session.id)
for event in events.data:
print(f"[{event.type}] {event.processed_at}")
TypeScript
const events = await client.beta.sessions.events.list(session.id);
for (const event of events.data) {
console.log(`[${event.type}] ${event.processed_at}`);
}
C#
var events = await client.Beta.Sessions.Events.List(session.ID);
foreach (var sessionEvent in events.Items)
{
Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}");
}
Go
events, err := client.Beta.Sessions.Events.List(ctx, session.ID, anthropic.BetaSessionEventListParams{})
if err != nil {
panic(err)
}
for _, event := range events.Data {
fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt)
}
Java
var events = client.beta().sessions().events().list(session.id());
for (var event : events.data()) {
var eventJson = event._json().orElseThrow().convert(JsonNode.class);
var processedAt = eventJson.path("processed_at");
IO.println("[" + eventJson.get("type").asText() + "] "
+ (processedAt.isTextual() ? processedAt.asText() : "null"));
}
PHP
$events = $client->beta->sessions->events->list($session->id);
foreach ($events->data as $event) {
$processedAt = ($event->processedAt ?? null)?->format(DATE_RFC3339) ?? 'null';
echo "[{$event->type}] {$processedAt}\n";
}
Ruby
events = client.beta.sessions.events.list(session.id)
events.data.each { puts "[#{it.type}] #{it.processed_at}" }
上述 SDK 片段仅展示当前页响应。读取全部历史时,需要继续按 next_page 分页请求;单次 list 调用不保证返回完整记录。
传入 types 过滤器,即可只返回指定类型的事件:
cURL
curl --fail-with-body -sS "$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events?types[]=agent.tool_use&types[]=agent.tool_result" \
-H "x-api-key: $AGENTAPI_API_KEY" \
| jq -r '.data[] | "[\(.type)] \(.processed_at)"'
Python
events = client.beta.sessions.events.list(
session.id,
types=["agent.tool_use", "agent.tool_result"],
)
for event in events.data:
print(f"[{event.type}] {event.processed_at}")
TypeScript
const events = await client.beta.sessions.events.list(session.id, {
types: ["agent.tool_use", "agent.tool_result"],
});
for (const event of events.data) {
console.log(`[${event.type}] ${event.processed_at}`);
}
C#
var events = await client.Beta.Sessions.Events.List(session.ID, new()
{
Types = ["agent.tool_use", "agent.tool_result"],
});
foreach (var sessionEvent in events.Items)
{
Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}");
}
Go
events, err := client.Beta.Sessions.Events.List(ctx, session.ID, anthropic.BetaSessionEventListParams{
Types: []string{"agent.tool_use", "agent.tool_result"},
})
if err != nil {
panic(err)
}
for _, event := range events.Data {
fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt)
}
Java
var events = client.beta().sessions().events().list(
session.id(),
EventListParams.builder()
.addType("agent.tool_use")
.addType("agent.tool_result")
.build());
for (var event : events.data()) {
event.agentToolUse().ifPresent(toolUse ->
IO.println("[" + toolUse.type() + "] " + toolUse.processedAt()));
event.agentToolResult().ifPresent(toolResult ->
IO.println("[" + toolResult.type() + "] " + toolResult.processedAt()));
}
PHP
// 在 PHP 中,通过 EventListParams 传入需要的事件类型。
Ruby
events = client.beta.sessions.events.list(
session.id,
types: ["agent.tool_use", "agent.tool_result"]
)
events.data.each { puts "[#{it.type}] #{it.processed_at}" }
调整执行中的任务
任务目标发生变化时,先用 user.interrupt 停止当前执行,再提交新要求。以下示例将文件分析改为修复指定位置的问题,合并发送中断和新消息的方式仅适用于 AgentAPI 托管的 native;其他运行时按下方警告分两步发送。
cURL
# 智能体当前正在分析文件...
# 用新的指令中断:
curl --fail-with-body -sS "$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-d @- <<'EOF'
{
"events": [
{"type": "user.interrupt"},
{
"type": "user.message",
"input": {"parts": [
{"type": "text", "text": "Instead, focus on fixing the bug in line 42."}
]}
}
]
}
EOF
Python
# 智能体当前正在分析文件...
# 用新的指令中断:
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.",
},
],
},
],
)
TypeScript
// 智能体当前正在分析文件……
// 用新的指令中断:
await 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.",
},
],
},
],
});
C#
// 智能体当前正在分析文件...
// 用新的指令进行中断:
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserInterruptEventParams
{
Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt,
},
new BetaManagedAgentsUserMessageEventParams
{
Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
Content =
[
new BetaManagedAgentsTextBlock
{
Type = BetaManagedAgentsTextBlockType.Text,
Text = "Instead, focus on fixing the bug in line 42.",
},
],
},
],
});
Go
// 智能体当前正在分析文件...
// 以新的指示中断:
if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{
{
OfUserInterrupt: &anthropic.BetaManagedAgentsUserInterruptEventParams{
Type: anthropic.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt,
},
},
{
OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: "Instead, focus on fixing the bug in line 42.",
},
}},
},
},
},
}); err != nil {
panic(err)
}
Java
// 智能体当前正在分析文件……
// 以新的指示进行中断:
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserInterruptEventParams.builder()
.type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT)
.build())
.addEvent(BetaManagedAgentsUserMessageEventParams.builder()
.type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
.addTextContent("Instead, focus on fixing the bug in line 42.")
.build())
.build());
PHP
// 智能体当前正在分析文件……
// 用新的指示中断:
$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.',
],
],
],
],
);
Ruby
# 智能体当前正在分析一个文件……
# 用新的指令中断:
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状态。
运行中的消息与中断
会话处于
running时,各运行时的处理方式不同:
- AgentAPI 托管的
native:可在同一请求提交user.interrupt+user.message,事件按序入队执行。此保证不适用于自托管外部 worker。claudecode和codex:发送user.message会返回409(agent busy)。请先发送user.interrupt,等待idle后再发送新消息。中断不会回滚已完成的工具操作或外部副作用。
被中断的执行以 session.status_idle 结束。stop_reason.type 在 native 中为 end_turn,与正常结束相同;在 claudecode 和 codex 中为 interrupted。请结合事件流中的 user.interrupt 判断中断。随后发送 user.message 可启动下一轮执行。
在原会话中继续任务
文件分析或修改结束后,可以在原会话提交测试要求。处于 idle 的会话仍能接收 user.message,AgentAPI 会尽量复用可用的运行时上下文。
上下文与产物保存
claudecode或codex的云端沙箱失效后,会话随之终止,需要新建会话。运行时上下文不保证长期留存;需要保存的产物应写入/mnt/session/outputs/,再通过 Files API 下载。
向空闲会话发送 user.message 事件可启动下一轮执行:
cURL
# 在生产环境中,请传入待恢复会话的已存储 ID。
curl --fail-with-body -sS "$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-d @- <<'EOF'
{
"events": [
{
"type": "user.message",
"input": {"parts": [
{"type": "text", "text": "Now run the tests against the changes you made earlier."}
]}
}
]
}
EOF
Python
# 通过发送新的 user.message 事件来恢复先前创建的会话。
# 在生产环境中,传入待恢复会话的已存储 ID。
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.message",
"content": [
{
"type": "text",
"text": "Now run the tests against the changes you made earlier.",
},
],
},
],
)
TypeScript
// 通过向先前创建的会话发送新的用户事件来恢复该会话。
// 在生产环境中,请传入待恢复会话的已存储 ID。
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.message",
content: [
{
type: "text",
text: "Now run the tests against the changes you made earlier.",
},
],
},
],
});
C#
// 通过 ID 恢复先前创建的会话。在生产环境中,请传入
// 创建会话时存储的会话 ID。
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserMessageEventParams
{
Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
Content =
[
new BetaManagedAgentsTextBlock
{
Type = BetaManagedAgentsTextBlockType.Text,
Text = "Now run the tests against the changes you made earlier.",
},
],
},
],
});
Go
// 恢复先前创建的会话:向其发送新的 user.message
// 事件。在生产环境中,请传入要恢复的会话的已存储 ID。
if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: "Now run the tests against the changes you made earlier.",
},
}},
},
}},
}); err != nil {
panic(err)
}
Java
// 通过 ID 恢复先前创建的会话。在生产环境中,请传入
// 创建会话时存储的会话 ID。
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserMessageEventParams.builder()
.type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
.addTextContent("Now run the tests against the changes you made earlier.")
.build())
.build());
PHP
// 通过发送新的 user.message 事件来恢复先前创建的会话。
// 在生产环境中,请传入创建会话时存储的会话 ID。
$client->beta->sessions->events->send(
$session->id,
events: [
[
'type' => 'user.message',
'content' => [
[
'type' => 'text',
'text' => 'Now run the tests against the changes you made earlier.',
],
],
],
],
);
Ruby
# 恢复会话只需向其发送下一个事件。在生产环境中,
# 请传入创建会话时存储的会话 ID。
client.beta.sessions.events.send_(
session.id,
events: [
{
type: "user.message",
content: [
{type: "text", text: "Now run the tests against the changes you made earlier."}
]
}
]
)
完成需要应用处理的操作
requires_action 表示本轮等待应用参与。先读取阻塞事件,区分是业务系统执行工具,还是批准云端工具调用,再发送对应结果。观察工具事件和执行工具是两种职责,不能将所有 agent.tool_use 都交给应用重复执行。
处理自定义工具调用
例如,任务需要通过业务系统查询数据时,可将查询声明为自定义工具。应用保存 agent.custom_tool_use 的事件 ID、工具名称和输入;会话以 requires_action 停止后,按 stop_reason.event_ids 逐个处理:
- 从已接收事件中找到对应调用,在业务系统中执行工具。
- 发送
user.custom_tool_result,将调用事件 ID 填入custom_tool_use_id,并附上结果。 - 全部阻塞事件处理完后,继续消费回到
running的会话。
cURL
exec {stream_fd}< <(curl --fail-with-body -sS -N \
"$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events/stream" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-H "accept: text/event-stream")
while IFS= read -r -u "$stream_fd" line; do
[[ $line == data:* ]] || continue
event_json="${line#data: }"
stop_reason=$(jq -r 'select(.type == "session.status_idle") | .stop_reason.type // empty' <<<"$event_json")
case "$stop_reason" in
requires_action)
while IFS= read -r event_id; do
# 执行该工具并将结果发回
result=$(call_tool "$event_id")
jq -n --arg id "$event_id" --arg result "$result" \
'{events: [{type: "user.custom_tool_result", custom_tool_use_id: $id, content: [{type: "text", text: $result}]}]}' |
curl --fail-with-body -sS \
"$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-d @-
done < <(jq -r '.stop_reason.event_ids[]' <<<"$event_json")
;;
end_turn)
break
;;
esac
done
exec {stream_fd}<&-
Python
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
TypeScript
const stream = await client.beta.sessions.events.stream(session.id);
for await (const event of stream) {
if (event.type !== "session.status_idle") continue;
if (event.stop_reason.type === "end_turn") break;
if (event.stop_reason.type !== "requires_action") continue;
for (const eventId of event.stop_reason.event_ids) {
// 查找该自定义工具使用事件并执行它
const toolEvent = eventsById.get(eventId);
if (!toolEvent) continue;
const result = await callTool(toolEvent.name, toolEvent.input);
// 将结果发送回去
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.custom_tool_result",
custom_tool_use_id: eventId,
content: [{ type: "text", text: result }],
},
],
});
}
}
C#
await foreach (var streamEvent in client.Beta.Sessions.Events.StreamStreaming(session.ID))
{
if (streamEvent.Value is not BetaManagedAgentsSessionStatusIdleEvent idle) continue;
if (idle.StopReason?.Value is BetaManagedAgentsSessionRequiresAction requiresAction)
{
foreach (var eventId in requiresAction.EventIds)
{
// 查找自定义工具使用事件并执行它
var toolEvent = eventsById[eventId];
var result = await CallTool(toolEvent.Name, toolEvent.Input);
// 将结果发送回去
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserCustomToolResultEventParams
{
Type = BetaManagedAgentsUserCustomToolResultEventParamsType.UserCustomToolResult,
CustomToolUseID = eventId,
Content =
[
new BetaManagedAgentsTextBlock
{
Type = BetaManagedAgentsTextBlockType.Text,
Text = result,
},
],
},
],
});
}
}
else if (idle.StopReason?.Value is BetaManagedAgentsSessionEndTurn)
{
break;
}
}
Go
stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
defer stream.Close()
loop:
for stream.Next() {
event, ok := stream.Current().AsAny().(anthropic.BetaManagedAgentsSessionStatusIdleEvent)
if !ok {
continue
}
switch stopReason := event.StopReason.AsAny().(type) {
case anthropic.BetaManagedAgentsSessionRequiresAction:
for _, eventID := range stopReason.EventIDs {
// 查找自定义工具使用事件并执行它
toolEvent := eventsByID[eventID]
result := callTool(toolEvent.Name, toolEvent.Input)
// 将结果发送回去
if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserCustomToolResult: &anthropic.BetaManagedAgentsUserCustomToolResultEventParams{
Type: anthropic.BetaManagedAgentsUserCustomToolResultEventParamsTypeUserCustomToolResult,
CustomToolUseID: eventID,
Content: []anthropic.BetaManagedAgentsUserCustomToolResultEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: result,
},
}},
},
}},
}); err != nil {
panic(err)
}
}
case anthropic.BetaManagedAgentsSessionEndTurn:
break loop
}
}
if err := stream.Err(); err != nil {
panic(err)
}
Java
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
stream.stream()
.filter(BetaManagedAgentsStreamSessionEvents::isSessionStatusIdle)
.map(idleEvent -> idleEvent.asSessionStatusIdle().stopReason())
.takeWhile(stopReason -> !stopReason.isEndTurn())
.filter(stopReason -> stopReason.isRequiresAction())
.flatMap(stopReason -> stopReason.asRequiresAction().eventIds().stream())
.forEach(eventId -> {
// 查找自定义工具使用事件并执行它
var toolEvent = eventsById.get(eventId);
var result = callTool(toolEvent.name(), toolEvent.input());
// 将结果发送回去
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserCustomToolResultEventParams.builder()
.type(BetaManagedAgentsUserCustomToolResultEventParams.Type.USER_CUSTOM_TOOL_RESULT)
.customToolUseId(eventId)
.addTextContent(result)
.build())
.build());
});
}
PHP
$stream = $client->beta->sessions->events->streamStream($session->id);
foreach ($stream as $event) {
if ($event->type === 'session.status_idle' && $event->stopReason) {
if ($event->stopReason->type === 'requires_action') {
foreach ($event->stopReason->eventIDs as $eventId) {
// 查找自定义工具使用事件并执行它
$toolEvent = $eventsById[$eventId];
$result = callTool($toolEvent->name, $toolEvent->input);
// 将结果发送回去
$client->beta->sessions->events->send(
$session->id,
events: [
[
'type' => 'user.custom_tool_result',
'custom_tool_use_id' => $eventId,
'content' => [['type' => 'text', 'text' => $result]],
],
],
);
}
} elseif ($event->stopReason->type === 'end_turn') {
break;
}
}
}
Ruby
client.beta.sessions.events.stream_events(session.id).each do |event|
case event
in {type: :"session.status_idle", stop_reason: {type: :requires_action, event_ids:}}
event_ids.each do |event_id|
# 查找自定义工具使用事件并执行它
tool_event = events_by_id[event_id]
result = call_tool.call(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}]
}
]
)
end
in {type: :"session.status_idle", stop_reason: {type: :end_turn}}
break
else
end
end
工具确认
当权限策略要求确认时,应用负责审批,工具仍由云端执行。收到 agent.tool_use 或 agent.mcp_tool_use 后,会话以 requires_action 停止;应用按 stop_reason.event_ids 提取调用,展示工具及参数供审批。
为每个阻塞事件发送 user.tool_confirmation:tool_use_id 填调用事件 ID,result 填 "allow" 或 "deny",拒绝时可附 deny_message。所有阻塞事件处理完后,会话回到 running。下面示例展示批准请求的发送方式;业务应用应在审批完成后发送。
cURL
exec {stream_fd}< <(curl --fail-with-body -sS -N \
"$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events/stream" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-H "accept: text/event-stream")
while IFS= read -r -u "$stream_fd" line; do
[[ $line == data:* ]] || continue
event_json="${line#data: }"
stop_reason=$(jq -r 'select(.type == "session.status_idle") | .stop_reason.type // empty' <<<"$event_json")
case "$stop_reason" in
requires_action)
while IFS= read -r event_id; do
# 批准待处理的工具调用
jq -n --arg id "$event_id" \
'{events: [{type: "user.tool_confirmation", tool_use_id: $id, result: "allow"}]}' |
curl --fail-with-body -sS \
"$AGENTAPI_BASE_URL/v1/sessions/$SESSION_ID/events" \
-H "x-api-key: $AGENTAPI_API_KEY" \
-H "content-type: application/json" \
-d @-
done < <(jq -r '.stop_reason.event_ids[]' <<<"$event_json")
;;
end_turn)
break
;;
esac
done
exec {stream_fd}<&-
Python
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
TypeScript
const stream = await client.beta.sessions.events.stream(session.id);
for await (const event of stream) {
if (event.type !== "session.status_idle") continue;
if (event.stop_reason.type === "end_turn") break;
if (event.stop_reason.type !== "requires_action") continue;
for (const eventId of event.stop_reason.event_ids) {
// 批准待处理的工具调用
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.tool_confirmation",
tool_use_id: eventId,
result: "allow",
},
],
});
}
}
C#
await foreach (var streamEvent in client.Beta.Sessions.Events.StreamStreaming(session.ID))
{
if (streamEvent.Value is not BetaManagedAgentsSessionStatusIdleEvent idle) continue;
if (idle.StopReason?.Value is BetaManagedAgentsSessionRequiresAction requiresAction)
{
foreach (var eventId in requiresAction.EventIds)
{
// 批准待处理的工具调用
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserToolConfirmationEventParams
{
Type = BetaManagedAgentsUserToolConfirmationEventParamsType.UserToolConfirmation,
ToolUseID = eventId,
Result = BetaManagedAgentsUserToolConfirmationEventParamsResult.Allow,
},
],
});
}
}
else if (idle.StopReason?.Value is BetaManagedAgentsSessionEndTurn)
{
break;
}
}
Go
stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
defer stream.Close()
loop:
for stream.Next() {
event, ok := stream.Current().AsAny().(anthropic.BetaManagedAgentsSessionStatusIdleEvent)
if !ok {
continue
}
switch stopReason := event.StopReason.AsAny().(type) {
case anthropic.BetaManagedAgentsSessionRequiresAction:
for _, eventID := range stopReason.EventIDs {
// 批准待处理的工具调用
if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserToolConfirmation: &anthropic.BetaManagedAgentsUserToolConfirmationEventParams{
Type: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsTypeUserToolConfirmation,
ToolUseID: eventID,
Result: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsResultAllow,
},
}},
}); err != nil {
panic(err)
}
}
case anthropic.BetaManagedAgentsSessionEndTurn:
break loop
}
}
if err := stream.Err(); err != nil {
panic(err)
}
Java
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
stream.stream()
.filter(BetaManagedAgentsStreamSessionEvents::isSessionStatusIdle)
.map(idleEvent -> idleEvent.asSessionStatusIdle().stopReason())
.takeWhile(stopReason -> !stopReason.isEndTurn())
.filter(stopReason -> stopReason.isRequiresAction())
.flatMap(stopReason -> stopReason.asRequiresAction().eventIds().stream())
// 批准每个待处理的工具调用
.forEach(toolUseId -> client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserToolConfirmationEventParams.builder()
.type(BetaManagedAgentsUserToolConfirmationEventParams.Type.USER_TOOL_CONFIRMATION)
.toolUseId(toolUseId)
.result(BetaManagedAgentsUserToolConfirmationEventParams.Result.ALLOW)
.build())
.build()));
}
PHP
$stream = $client->beta->sessions->events->streamStream($session->id);
foreach ($stream as $event) {
if ($event->type === 'session.status_idle' && $event->stopReason) {
if ($event->stopReason->type === 'requires_action') {
foreach ($event->stopReason->eventIDs as $eventId) {
// 批准待处理的工具调用
$client->beta->sessions->events->send(
$session->id,
events: [
[
'type' => 'user.tool_confirmation',
'tool_use_id' => $eventId,
'result' => 'allow',
],
],
);
}
} elseif ($event->stopReason->type === 'end_turn') {
break;
}
}
}
Ruby
client.beta.sessions.events.stream_events(session.id).each do |event|
case event
in {type: :"session.status_idle", stop_reason: {type: :requires_action, event_ids:}}
event_ids.each do |event_id|
# 批准待处理的工具调用
client.beta.sessions.events.send_(
session.id,
events: [
{type: "user.tool_confirmation", tool_use_id: event_id, result: "allow"}
]
)
end
in {type: :"session.status_idle", stop_reason: {type: :end_turn}}
break
else
end
end
核对用量与排查异常
用量与时长用于观察执行成本。
核对模型用量与执行时长
会话通过 stats 返回时长,通过 usage 返回所有轮次的累计模型用量。会话进入 idle 后,查询可获取基于已提交事件计算的最新汇总;运行时报告的用量明细见 span.model_request_end。
{
"id": "ses_01...",
"status": "idle",
"usage": {
"input_tokens": 2400,
"output_tokens": 350,
"cache_read_input_tokens": 1800
},
"stats": {
"active_seconds": 342.5,
"duration_seconds": 401.2
}
}
读取响应时,请区分用量与时长:
usage:累计用量,可能包含input_tokens、output_tokens、cache_read_input_tokens、cache_creation和server_tool_use;不包含active_seconds。缺失或有歧义的计数按未知处理,不应视为零。stats.active_seconds:累计执行时间。stats.duration_seconds:从创建到当前的经过时间,会话终止后冻结。span.model_request_end.model_usage:Native 中表示单次模型请求用量;CLI 运行时可能报告整次执行或线程累计用量,不能按单次请求直接累加。字段由运行时和模型决定,可能包含input_tokens、output_tokens或缓存用量。
计数至少上报一次后才会出现,客户端需兼容缺失和额外字段。完整结构见 API 通用约定。
在 Portal 定位执行问题
在 Portal 会话详情的“轨迹”中检查事件和停止原因,需要完整记录时通过“Raw JSONL”导出。界面使用说明见在 Portal 中构建。
事件字段与消费职责
实现分发逻辑时,按事件的用途选择处理方式。应用发送用户事件;AgentAPI 返回回答、工具调用、执行阶段(Span)和会话状态。
| 消费目的 | 事件 | 应用职责 |
|---|---|---|
| 展示回答 | agent.message |
提取回答内容;Native 增量预览另按 event_start、event_delta 处理。 |
| 观察云端工具 | agent.tool_use、agent.mcp_tool_use |
展示调用过程,按权限策略处理确认。 |
| 执行业务工具 | agent.custom_tool_use |
保存调用,按阻塞事件 ID 回传结果。 |
| 更新任务状态 | session.status_running、session.status_idle、session.status_terminated |
展示执行状态;停止时检查原因,终止后停止提交输入。 |
| 排查模型请求 | span.model_request_start、span.model_request_end |
关联请求阶段,检查运行时报告的用量。 |
| 记录错误 | session.error |
保存错误信息,继续检查后续状态。 |
持久化事件类型遵循 {domain}.{action} 命名约定;仅供流预览的 event_start、event_delta 例外。完整目录见事件类型。
持久化事件包含 id、type、session_id、sequence_number 和 processed_at。排队中的用户输入在接收响应中可能返回 processed_at: null;执行时,事件列表和事件流中的对应事件会带上处理时间。不要依赖事件包含 created_at。