会话事件流
发送事件、流式响应,以及在执行过程中中断或重定向会话。
与 Claude 托管代理的通信基于事件。您向代理发送用户事件,并接收代理和会话事件来跟踪状态。
所有托管代理 API 请求都需要 managed-agents-2026-04-01 beta 头。SDK 会自动设置该 beta 头。
事件类型
事件在两个方向上流动。
- 用户事件是您发送给代理以启动会话并在会话进行中引导代理的事件。
- 会话事件、span 事件和代理事件是发送给您的,用于可观测性地了解会话状态和代理进度。
事件类型字符串遵循 {domain}.{action} 命名约定。
| 类型 | 描述 |
|---|---|
user.message | 包含文本内容的用户消息。 |
user.interrupt | 在代理执行过程中停止代理。 |
user.custom_tool_result | 对代理自定义工具调用的响应。 |
user.tool_confirmation | 当权限策略需要确认时,批准或拒绝代理或 MCP 工具调用。 |
user.define_outcome | 为代理定义一个目标以供其努力实现。 |
user.tool_result | 仅适用于使用 self_hosted 环境的会话,您的集成负责提供 agent_toolset 结果。SDK 辅助函数和 CLI 会自动完成此操作。 |
| 类型 | 描述 |
|---|---|
agent.message | 包含文本内容块的代理响应。 |
agent.thinking | 代理思考内容,与消息分开发出。 |
agent.tool_use | 代理调用预构建的代理工具(bash、文件操作等)。 |
agent.tool_result | 预构建代理工具执行的结果。 |
agent.mcp_tool_use | 代理调用 MCP 服务器工具。 |
agent.mcp_tool_result | MCP 工具执行的结果。 |
agent.custom_tool_use | 代理调用您的自定义工具之一。使用 user.custom_tool_result 事件进行响应。 |
agent.thread_context_compacted | 对话历史已压缩以适应上下文窗口。 |
agent.thread_message_received | 在多代理会话中,代理将其结果传递给协调器。 |
agent.thread_message_sent | 在多代理会话中,协调器向另一个代理发送后续消息。 |
| 类型 | 描述 |
|---|---|
session.status_running | 代理正在积极处理。 |
session.status_idle | 代理已完成当前任务,正在等待输入。包含一个 stop_reason,指示代理停止的原因。 |
session.status_rescheduled | 发生了瞬态错误,会话正在自动重试。 |
session.status_terminated | 会话因不可恢复的错误而结束。 |
session.updated | 会话更新请求更改了至少一个字段。仅包含已更改的字段。更新在下一轮生效。 |
session.error | 处理过程中发生错误。包含一个带 retry_status 的类型化 error 对象。 |
session.thread_created | 创建了一个多代理线程。 |
session.thread_status_running | 一个多代理线程开始了活动。 |
session.thread_status_idle | 一个多代理线程完成了其轮次,正在等待输入。包含 stop_reason。 |
session.thread_status_terminated | 一个多代理线程被归档或遇到终端错误。 |
每个事件都包含一个 processed_at 时间戳,指示事件在服务端记录的时间。如果 processed_at 为 null,表示事件已由运行时排队,将在前面的事件处理完成后被处理。
集成事件
发送 user.message 事件以启动或继续代理的工作:
curl -sS --fail-with-body "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d @- <<'EOF'
{
"events": [
{
"type": "user.message",
"content": [
{"type": "text", "text": "Analyze the performance of the sort function in utils.py"}
]
}
]
}
EOF
ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
events:
- type: user.message
content:
- type: text
text: Analyze the performance of the sort function in utils.py
YAML
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",
},
],
},
],
)
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",
},
],
},
],
});
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",
},
],
},
],
});
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)
}
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());
$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',
],
],
],
],
);
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 事件来重定向代理:
# Agent is currently analyzing a file...
# Interrupt with a new direction:
curl -sS --fail-with-body "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d @- <<'EOF'
{
"events": [
{"type": "user.interrupt"},
{
"type": "user.message",
"content": [
{"type": "text", "text": "Instead, focus on fixing the bug in line 42."}
]
}
]
}
EOF
# Agent is currently analyzing a file...
# Interrupt with a new direction:
ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
events:
- type: user.interrupt
- type: user.message
content:
- type: text
text: Instead, focus on fixing the bug in line 42.
YAML
# Agent is currently analyzing a file...
# Interrupt with a new 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.",
},
],
},
],
)
// Agent is currently analyzing a file...
// Interrupt with a new direction:
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.",
},
],
},
],
});
// Agent is currently analyzing a file...
// Interrupt with a new direction:
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.",
},
],
},
],
});
// Agent is currently analyzing a file...
// Interrupt with a new direction:
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)
}
// Agent is currently analyzing a file...
// Interrupt with a new direction:
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());
// Agent is currently analyzing a file...
// Interrupt with a new 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.',
],
],
],
],
);
# Agent is currently analyzing a file...
# Interrupt with a new 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."}
]
}
]
)
代理将确认中断并切换到新任务。
从会话流式传输事件以在代理工作时接收实时更新。只有在流打开后发出的事件才会被传递,因此请在发送事件之前打开流以避免竞争条件。
# Open the stream first, then send the user message
exec {stream}< <(
curl -sS -N --fail-with-body \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-H "Accept: text/event-stream"
)
curl -sS --fail-with-body \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d @- >/dev/null <<'EOF'
{
"events": [
{
"type": "user.message",
"content": [{"type": "text", "text": "Summarize the repo README"}]
}
]
}
EOF
while IFS= read -r -u "$stream" line; do
[[ $line == data:* ]] || continue
json=${line#data: }
case $(jq -r '.type' <<<"$json") in
agent.message)
jq -j '.content[] | select(.type == "text") | .text' <<<"$json"
;;
session.status_idle)
break
;;
session.error)
printf '\n[Error: %s]\n' "$(jq -r '.error.message // "unknown"' <<<"$json")"
break
;;
esac
done
exec {stream}<&-
# This workflow does not translate well to a one-off shell command.
# Use one of the SDK examples in this code group instead.
# Open the stream first, then send the user message
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":
msg = event.error.message if event.error else "unknown"
print(f"\n[Error: {msg}]")
break
// Open the stream first, then send the user message
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;
}
}
// Open the stream first, then send the user message
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({{CONTENT}}quot;\n[Error: {error.Error?.Message ?? "unknown"}]");
break;
}
}
// Open the stream first, then send the user message
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:
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)
}
// Open the stream first, then send the user message
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()
);
for (var event : (Iterable<BetaManagedAgentsStreamSessionEvents>) stream.stream()::iterator) {
if (event.isAgentMessage()) {
event.asAgentMessage().content().forEach(block -> IO.print(block.text()));
} else if (event.isSessionStatusIdle()) {
break;
} else if (event.isSessionError()) {
var msg = event.asSessionError().error()
.flatMap(err -> err._json())
.map(json -> {
Optional<Map<String, JsonValue>> obj = json.asObject();
return obj.orElseThrow().get("message").asStringOrThrow();
})
.orElse("unknown");
IO.println("\n[Error: " + msg + "]");
break;
}
}
}
// Open the stream first, then send the user message
$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();
# Open the stream first, then send the user message
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
# ignore other event types
end
end
要重新连接到现有会话而不丢失事件,请打开一个新流,然后列出完整的历史记录以建立一组已见事件 ID。跟踪实时流的同时跳过历史列表中已返回的任何事件。
exec {stream}< <(
curl -sS -N --fail-with-body \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-H "Accept: text/event-stream"
)
# Stream is open and buffering. List history before tailing live.
declare -A seen_event_ids
while IFS= read -r id; do
seen_event_ids[$id]=1
done < <(
curl -sS --fail-with-body \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" | jq -r '.data[].id'
)
# Tail live events, skipping anything already seen
while IFS= read -r -u "$stream" line; do
[[ $line == data:* ]] || continue
json=${line#data: }
id=$(jq -r '.id' <<<"$json")
[[ -n ${seen_event_ids[$id]+seen} ]] && continue
seen_event_ids[$id]=1
case $(jq -r '.type' <<<"$json") in
agent.message)
jq -j '.content[] | select(.type == "text") | .text' <<<"$json"
;;
session.status_idle)
break
;;
esac
done
exec {stream}<&-
# This workflow does not translate well to a one-off shell command.
# Use one of the SDK examples in this code group instead.
with client.beta.sessions.events.stream(session.id) as stream:
# Stream is open and buffering. List history before tailing live.
seen_event_ids = {event.id for event in client.beta.sessions.events.list(session.id)}
# Tail live events, skipping anything already seen
for event in stream:
if event.id in seen_event_ids:
continue
seen_event_ids.add(event.id)
match event.type:
case "agent.message":
for block in event.content:
if block.type == "text":
print(block.text, end="")
case "session.status_idle":
break
const seenEventIds = new Set<string>();
const stream = await client.beta.sessions.events.stream(session.id);
// Stream is open and buffering. List history before tailing live.
for await (const event of client.beta.sessions.events.list(session.id)) {
seenEventIds.add(event.id);
}
// Tail live events, skipping anything already seen
for await (const event of stream) {
if (seenEventIds.has(event.id)) continue;
seenEventIds.add(event.id);
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;
}
}
using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);
// Stream is open and buffering. List history before tailing live.
HashSet<string> seenEventIds = [];
var history = await client.Beta.Sessions.Events.List(session.ID);
await foreach (var pastEvent in history.Paginate())
{
seenEventIds.Add(pastEvent.ID);
}
// Tail live events, skipping anything already seen
await foreach (var streamEvent in stream.Enumerate())
{
if (!seenEventIds.Add(streamEvent.ID))
{
continue;
}
if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
{
foreach (var block in message.Content)
{
Console.Write(block.Text);
}
}
else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent)
{
break;
}
}
stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
defer stream.Close()
// Stream is open and buffering. List history before tailing live.
seenEventIDs := map[string]struct{}{}
history := client.Beta.Sessions.Events.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionEventListParams{})
for history.Next() {
seenEventIDs[history.Current().ID] = struct{}{}
}
if err := history.Err(); err != nil {
panic(err)
}
// Tail live events, skipping anything already seen
tail:
for stream.Next() {
event := stream.Current()
if _, seen := seenEventIDs[event.ID]; seen {
continue
}
seenEventIDs[event.ID] = struct{}{}
switch event := event.AsAny().(type) {
case anthropic.BetaManagedAgentsAgentMessageEvent:
for _, block := range event.Content {
fmt.Print(block.Text)
}
case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
break tail
}
}
if err := stream.Err(); err != nil {
panic(err)
}
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
// Stream is open and buffering. List history before tailing live.
// _json() exposes the raw event so we can read the cross-variant `id` field.
var seenEventIds = new HashSet<String>();
for (var past : client.beta().sessions().events().list(session.id()).autoPager()) {
Optional<Map<String, JsonValue>> obj = past._json().orElseThrow().asObject();
seenEventIds.add(obj.orElseThrow().get("id").asStringOrThrow());
}
// Tail live events, skipping anything already seen
for (var event : (Iterable<BetaManagedAgentsStreamSessionEvents>) stream.stream()::iterator) {
Optional<Map<String, JsonValue>> obj = event._json().orElseThrow().asObject();
if (!seenEventIds.add(obj.orElseThrow().get("id").asStringOrThrow())) continue;
if (event.isAgentMessage()) {
event.asAgentMessage().content().forEach(block -> IO.print(block.text()));
} else if (event.isSessionStatusIdle()) {
break;
}
}
}
$stream = $client->beta->sessions->events->streamStream($session->id);
// Stream is open and buffering. List history before tailing live.
$seenEventIds = [];
foreach ($client->beta->sessions->events->list($session->id)->pagingEachItem() as $event) {
$seenEventIds[$event->id] = true;
}
// Tail live events, skipping anything already seen
foreach ($stream as $event) {
if (isset($seenEventIds[$event->id])) {
continue;
}
$seenEventIds[$event->id] = true;
match ($event->type) {
'agent.message' => array_walk(
$event->content,
static fn($block) => $block->type === 'text' ? print($block->text) : null,
),
default => null,
};
if ($event->type === 'session.status_idle') {
break;
}
}
$stream->close();
stream = client.beta.sessions.events.stream_events(session.id)
# Stream is open and buffering. List history before tailing live.
seen_event_ids = Set.new
client.beta.sessions.events.list(session.id).auto_paging_each { seen_event_ids << it.id }
# Tail live events, skipping anything already seen
stream.each do |event|
next if seen_event_ids.include?(event.id)
seen_event_ids << event.id
case event.type
in :"agent.message"
event.content.each { print it.text }
in :"session.status_idle"
break
else
# ignore other event types
end
end
检索会话的完整事件历史记录:
curl -sS --fail-with-body "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
| jq -r '.data[] | "[\(.type)] \(.processed_at)"'
ant beta:sessions:events list \
--session-id "$SESSION_ID" \
--format jsonl \
--transform '{type,processed_at}'
events = client.beta.sessions.events.list(session.id)
for event in events.data:
print(f"[{event.type}] {event.processed_at}")
const events = await client.beta.sessions.events.list(session.id);
for (const event of events.data) {
console.log(`[${event.type}] ${event.processed_at}`);
}
var events = await client.Beta.Sessions.Events.List(session.ID);
foreach (var evt in events.Items)
{
Console.WriteLine({{CONTENT}}quot;[{evt.Json.GetProperty("type").GetString()}] {evt.ProcessedAt}");
}
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)
}
var events = client.beta().sessions().events().list(session.id());
for (var event : events.data()) {
var json = (Map<String, JsonValue>) event._json().orElseThrow().asObject().orElseThrow();
var type = json.get("type").asStringOrThrow();
var processedAt = json.containsKey("processed_at")
? json.get("processed_at").asStringOrThrow()
: "pending";
IO.println("[" + type + "] " + processedAt);
}
$events = $client->beta->sessions->events->list($session->id);
foreach ($events->data as $event) {
$processedAt = ($event->processedAt ?? null)?->format(DATE_RFC3339) ?? 'pending';
echo "[{$event->type}] {$processedAt}\n";
}
events = client.beta.sessions.events.list(session.id)
events.data.each { puts "[#{it.type}] #{it.processed_at}" }
传递 types 过滤器以仅返回特定事件类型:
curl -sS --fail-with-body "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true&types[]=agent.tool_use&types[]=agent.tool_result" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
| jq -r '.data[] | "[\(.type)] \(.processed_at)"'
ant beta:sessions:events list \
--session-id "$SESSION_ID" \
--type agent.tool_use \
--type agent.tool_result \
--format jsonl \
--transform '{type,processed_at}'
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}")
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}`);
}
var events = await client.Beta.Sessions.Events.List(session.ID, new()
{
Types = ["agent.tool_use", "agent.tool_result"],
});
foreach (var evt in events.Items)
{
Console.WriteLine({{CONTENT}}quot;[{evt.Json.GetProperty("type").GetString()}] {evt.ProcessedAt}");
}
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)
}
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()));
}
$events = $client->beta->sessions->events->list(
$session->id,
types: ['agent.tool_use', 'agent.tool_result'],
);
foreach ($events->data as $event) {
$processedAt = ($event->processedAt ?? null)?->format(DATE_RFC3339) ?? 'pending';
echo "[{$event->type}] {$processedAt}\n";
}
events = client.beta.sessions.events.list(
session.id,
types: ["agent.tool_use", "agent.tool_result"]
)
events.data.each { puts "[#{it.type}] #{it.processed_at}" }
附加场景
处理自定义工具调用
当代理调用自定义工具时:
- 会话发出一个
agent.custom_tool_use事件,包含工具名称和输入。 - 会话暂停并发出一个
session.status_idle事件,包含stop_reason: requires_action。阻塞事件 ID 在stop_reason.event_ids数组中。 - 在您的系统中执行工具,并为每个工具发送
user.custom_tool_result事件,在custom_tool_use_id参数中传递事件 ID 以及结果内容。 - 一旦所有阻塞事件被解决,会话将转换回
running。
exec {fd}< <(curl -sS -N --fail-with-body \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-H "Accept: text/event-stream")
while IFS= read -r -u "$fd" line; do
[[ $line == data:* ]] || continue
data="${line#data: }"
[[ $(jq -r '.type' <<<"$data") == "session.status_idle" ]] || continue
case $(jq -r '.stop_reason.type // empty' <<<"$data") in
requires_action)
while IFS= read -r event_id; do
# Look up the custom tool use event and execute it
result=$(call_tool "$event_id")
# Send the result back
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 -sS --fail-with-body \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d @-
done < <(jq -r '.stop_reason.event_ids[]' <<<"$data")
;;
end_turn)
break
;;
esac
done
exec {fd}<&-
# This workflow does not translate well to a one-off shell command.
# Use one of the SDK examples in this code group instead.
with client.beta.sessions.events.stream(session.id) as stream:
for event in stream:
if event.type == "session.status_idle" and (stop := event.stop_reason):
match stop.type:
case "requires_action":
for event_id in stop.event_ids:
# Look up the custom tool use event and execute it
tool_event = events_by_id[event_id]
result = call_tool(tool_event.name, tool_event.input)
# Send the result back
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
const stream = await client.beta.sessions.events.stream(session.id);
for await (const event of stream) {
if (event.type === "session.status_idle") {
if (event.stop_reason?.type === "requires_action") {
for (const eventId of event.stop_reason.event_ids) {
// Look up the custom tool use event and execute it
const toolEvent = eventsById[eventId];
const result = await callTool(toolEvent.name, toolEvent.input);
// Send the result back
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.custom_tool_result",
custom_tool_use_id: eventId,
content: [{ type: "text", text: result }],
},
],
});
}
} else if (event.stop_reason?.type === "end_turn") {
break;
}
}
}
await foreach (var streamEvent in client.Beta.Sessions.Events.StreamStreaming(session.ID))
{
if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent idle)
{
if (idle.StopReason?.Value is BetaManagedAgentsSessionRequiresAction requiresAction)
{
foreach (var eventId in requiresAction.EventIds)
{
// Look up the custom tool use event and execute it
var toolEvent = eventsById[eventId];
var result = await CallTool(toolEvent.Name, toolEvent.Input);
// Send the result back
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;
}
}
}
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 {
// Look up the custom tool use event and execute it
toolEvent := eventsByID[eventID]
result := callTool(toolEvent.Name, toolEvent.Input)
// Send the result back
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)
}
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
for (var event : (Iterable<BetaManagedAgentsStreamSessionEvents>) stream.stream()::iterator) {
if (!event.isSessionStatusIdle()) continue;
var stopReason = event.asSessionStatusIdle().stopReason().orElseThrow();
if (stopReason.isRequiresAction()) {
for (var eventId : stopReason.asRequiresAction().eventIds()) {
// Look up the custom tool use event and execute it
var toolEvent = eventsById.get(eventId);
var result = callTool(toolEvent.name(), toolEvent.input());
// Send the result back
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());
}
} else if (stopReason.isEndTurn()) {
break;
}
}
}
$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) {
// Look up the custom tool use event and execute it
$toolEvent = $eventsById[$eventId];
$result = callTool($toolEvent->name, $toolEvent->input);
// Send the result back
$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;
}
}
}
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|
# Look up the custom tool use event and execute it
tool_event = events_by_id[event_id]
result = call_tool.call(tool_event.name, tool_event.input)
# Send the result back
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事件。 - 会话暂停并发出一个
session.status_idle事件,包含stop_reason: requires_action。阻塞事件 ID 在stop_reason.event_ids数组中。 - 为每个工具发送
user.tool_confirmation事件,在tool_use_id参数中传递事件 ID。将result设置为"allow"或"deny"。使用deny_message来解释拒绝原因。 - 一旦所有阻塞事件被解决,会话将转换回
running。
exec {fd}< <(curl -sS -N --fail-with-body \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-H "Accept: text/event-stream")
while IFS= read -r -u "$fd" line; do
[[ $line == data:* ]] || continue
data="${line#data: }"
[[ $(jq -r '.type' <<<"$data") == "session.status_idle" ]] || continue
case $(jq -r '.stop_reason.type // empty' <<<"$data") in
requires_action)
while IFS= read -r event_id; do
# Approve the pending tool call
jq -n --arg id "$event_id" \
'{events: [{type: "user.tool_confirmation", tool_use_id: $id, result: "allow"}]}' |
curl -sS --fail-with-body \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d @-
done < <(jq -r '.stop_reason.event_ids[]' <<<"$data")
;;
end_turn)
break
;;
esac
done
exec {fd}<&-
# This workflow does not translate well to a one-off shell command.
# Use one of the SDK examples in this code group instead.
with client.beta.sessions.events.stream(session.id) as stream:
for event in stream:
if event.type == "session.status_idle" and (stop := event.stop_reason):
match stop.type:
case "requires_action":
for event_id in stop.event_ids:
# Approve the pending tool call
client.beta.sessions.events.send(
session.id,
events=[
{
"type": "user.tool_confirmation",
"tool_use_id": event_id,
"result": "allow",
},
],
)
case "end_turn":
break
const stream = await client.beta.sessions.events.stream(session.id);
for await (const event of stream) {
if (event.type === "session.status_idle") {
if (event.stop_reason?.type === "requires_action") {
for (const eventId of event.stop_reason.event_ids) {
// Approve the pending tool call
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.tool_confirmation",
tool_use_id: eventId,
result: "allow",
},
],
});
}
} else if (event.stop_reason?.type === "end_turn") {
break;
}
}
}
await foreach (var streamEvent in client.Beta.Sessions.Events.StreamStreaming(session.ID))
{
if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent idle)
{
if (idle.StopReason?.Value is BetaManagedAgentsSessionRequiresAction requiresAction)
{
foreach (var eventId in requiresAction.EventIds)
{
// Approve the pending tool call
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;
}
}
}
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 {
// Approve the pending tool call
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)
}
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
for (var event : (Iterable<BetaManagedAgentsStreamSessionEvents>) stream.stream()::iterator) {
if (!event.isSessionStatusIdle()) continue;
var stopReason = event.asSessionStatusIdle().stopReason().orElseThrow();
if (stopReason.isRequiresAction()) {
for (var eventId : stopReason.asRequiresAction().eventIds()) {
// Approve the pending tool call
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserToolConfirmationEventParams.builder()
.type(BetaManagedAgentsUserToolConfirmationEventParams.Type.USER_TOOL_CONFIRMATION)
.toolUseId(eventId)
.result(BetaManagedAgentsUserToolConfirmationEventParams.Result.ALLOW)
.build())
.build());
}
} else if (stopReason.isEndTurn()) {
break;
}
}
}
$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) {
// Approve the pending tool call
$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;
}
}
}
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|
# Approve the pending tool call
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
恢复空闲会话
会话在交互之间持久化。除非会话被明确删除,否则对话历史将被保留。当会话变为空闲时,其容器会被检查点化,保留完整的容器状态,包括文件系统、已安装的包以及代理创建的任何文件。这使您可以从非活动状态干净地恢复。
虽然会话历史在删除之前会一直保留,但检查点仅在会话最后活动后的 30 天内保留。如果您的工作流需要完整容器状态(文件、已安装的工具等)保留超过 30 天,请在检查点过期之前发送定期的 user.message 事件以重置非活动计时器。
要恢复会话,请像往常一样向其发送 user.message 事件:
# Resume a previously created session by ID
client.beta.sessions.events.send(
"sesn_01...",
events=[
{
"type": "user.message",
"content": [
{
"type": "text",
"text": "Now run the tests against the changes you made earlier.",
},
],
},
],
)
跟踪使用量
会话对象包含一个 usage 字段,带有累积的 token 统计信息。在会话变为空闲后获取会话以读取最新的总计,并使用它们来跟踪成本、强制执行预算或监控消耗。
{
"id": "sesn_01...",
"status": "idle",
"usage": {
"input_tokens": 5000,
"output_tokens": 3200,
"cache_creation_input_tokens": 2000,
"cache_read_input_tokens": 20000
}
}
input_tokens 报告未缓存的输入 token,output_tokens 报告会话中所有模型调用的总输出 token。cache_creation_input_tokens 和 cache_read_input_tokens 字段反映提示缓存活动。缓存条目使用 5 分钟的 TTL,因此在该窗口内的连续轮次可以受益于缓存读取,从而降低每 token 成本。
控制台可观测性
控制台提供代理会话的可视化时间线视图。导航到控制台中的 Claude 托管代理部分以查看:
- 会话列表 - 所有会话及其状态、创建时间和模型
- 追踪视图 - 会话内事件的时间顺序视图(内容、时间戳、token 使用量)。这些仅对开发者和管理员可访问。
- 工具执行 - 每个工具调用及其结果的详细信息
调试技巧
- 检查会话事件 - 会话错误通过
session.error事件传达 - 审查工具结果 - 工具执行失败通常能解释意外的代理行为
- 跟踪 token 使用量 - 监控 token 消耗以优化提示并降低成本
- 使用系统提示 - 在系统提示中添加日志指令,让代理解释其推理过程