本页目录24
- 循环核心是:接收 prompt → Claude 评估并可能请求工具调用 → SDK 执行工具并把结果喂回 → 重复直到 Claude 给出无工具调用的文本响应 → 返回 ResultMessage
- 五种核心消息类型:SystemMessage、AssistantMessage、UserMessage、StreamEvent、ResultMessage,其中 ResultMessage 的 subtype 字段决定任务是否成功以及 result 字段是否可用
- 可用 max_turns/maxTurns 和 max_budget_usd/maxBudgetUsd 限制回合数与花费上限,命中限制会返回对应的 error_max_turns / error_max_budget_usd 结果
- permission_mode/permissionMode 提供 default、acceptEdits、plan、dontAsk、auto、bypassPermissions 六种模式,决定工具调用是否需要人工批准
- 上下文窗口在会话内不会重置,系统提示词与工具定义等静态内容会被 prompt cache;临近上限时 SDK 会自动压缩(compaction),可通过 CLAUDE.md 指令、PreCompact 钩子或手动发送 /compact 定制
- 子代理(subagent)以全新对话上下文启动,只把最终结果作为工具结果返回给父级,是控制上下文膨胀的关键手段
本文是对 Claude Agent SDK 官方文档页面「How the agent loop works」的中文整理,完整与最新内容以原文为准:https://code.claude.com/docs/en/agent-sdk/agent-loop
概述
Agent SDK 让你可以在自己的应用中嵌入 Claude Code 的自主智能体循环(agent loop)。该 SDK 是一个独立的包,提供了对工具、权限、成本限制和输出的编程控制。
TypeScript 和 Python 两种 SDK 都自带原生的 Claude Code 二进制文件,因此大多数安装不需要单独安装 Claude Code(需要单独安装的情况见 quickstart 的安装说明)。
启动一个 agent 时,SDK 会运行与 Claude Code 相同的执行循环:Claude 评估你的 prompt,调用工具执行动作,接收结果,并重复此过程直到任务完成。本文解释该循环内部发生了什么,以便你能有效地构建、调试和优化你的 agent。
循环概览
每个 agent 会话都遵循相同的周期:
- 接收 prompt。 Claude 接收你的 prompt,以及系统提示词、工具定义和对话历史。SDK 会产出一条 subtype 为
"init"的SystemMessage,其中包含会话元数据。 - 评估并响应。 Claude 评估当前状态并决定如何继续。它可能以文本响应、请求一个或多个工具调用,或两者兼有。SDK 产出一条
AssistantMessage,包含文本内容和任何工具调用请求。 - 执行工具。 SDK 运行每个被请求的工具并收集结果。每一组工具结果会反馈给 Claude 用于下一步决策。你可以使用 hooks 来拦截、修改或阻止工具调用的执行。
- 重复。 步骤 2 和 3 循环往复。每个完整周期称为一个 turn(回合)。Claude 会持续调用工具并处理结果,直到它产出一个不包含工具调用的响应。
- 返回结果。 SDK 产出最后一条不含工具调用的文本
AssistantMessage,随后产出一条ResultMessage,包含最终文本、token 用量、成本和会话 ID。
一个简单的问题(「这里有哪些文件?」)可能只需一两个回合调用 Glob 并返回结果。一个复杂任务(「重构 auth 模块并更新测试」)则可能跨多个回合链式调用数十次工具,读取文件、编辑代码、运行测试,Claude 会根据每次结果调整方案。
回合(Turn)与消息
一个 turn 是循环内的一次往返:Claude 产出包含工具调用的输出,SDK 执行这些工具,结果自动反馈给 Claude。这个过程不会把控制权交还给你的代码。回合会持续进行,直到 Claude 产出不含工具调用的输出,此时循环结束并返回最终结果。
以 prompt「Fix the failing tests in auth.ts」为例,一个完整会话大致如下:
SDK 首先把 prompt 发给 Claude,并产出一条含会话元数据的 SystemMessage。随后循环开始:
- Turn 1: Claude 调用
Bash运行npm test。SDK 产出一条带工具调用的AssistantMessage,执行命令,然后产出一条UserMessage,内容是输出(三个失败)。 - Turn 2: Claude 对
auth.ts和auth.test.ts调用Read。SDK 返回文件内容并产出一条AssistantMessage。 - Turn 3: Claude 调用
Edit修复auth.ts,然后调用Bash重新运行npm test。三个测试全部通过。SDK 产出一条AssistantMessage。 - 最后一个回合: Claude 产出纯文本响应,不含工具调用:「Fixed the auth bug, all three tests pass now.」SDK 产出最后一条含该文本的
AssistantMessage,随后产出一条ResultMessage,包含相同文本以及成本和用量信息。
这一共是四个回合:三个含工具调用,一个纯文本回应。
可以用 max_turns / maxTurns 给循环设置上限,该值只统计含工具调用的回合。例如上例中 max_turns=2 会在编辑步骤之前就停止。也可以用 max_budget_usd / maxBudgetUsd 基于花费阈值限制回合。
若不设限制,循环会一直运行到 Claude 自行完成为止;这对范围明确的任务没问题,但在开放式 prompt(如「改进这个代码库」)上可能运行很久。为生产环境的 agent 设置预算是一个好的默认做法。选项参考见下文的「回合与预算」。
消息类型
循环运行期间,SDK 会产出一串消息流。每条消息都带有一个 type,标识它来自循环的哪个阶段。五种核心类型是:
-
SystemMessage: 会话生命周期事件。subtype字段用于区分:"init":本次运行的会话元数据。当SessionStart或Setup钩子在会话启动期间运行时,其 hook 生命周期消息会先于init消息到达"compact_boundary":在压缩(compaction)之后触发"informational":来自循环的纯文本状态提示"worker_shutting_down":由于宿主退出或 Remote Control 断开连接,循环将在当前回合结束后终止
在 TypeScript 中,除
"init"外的每个 subtype 在SDKMessage联合类型中都是独立的类型,而不是SDKSystemMessage的子类型。 -
AssistantMessage: 在 Claude 每次响应后产出,包括最后一条纯文本响应。包含该回合的文本内容块和工具调用块。 -
UserMessage: 在每次工具执行后产出,携带发回给 Claude 的工具结果内容。你在循环中途流式发送的任何用户输入也会产出此类型。 -
StreamEvent: 仅在启用了 partial messages 时产出,包含原始 API 流式事件(文本增量、工具输入片段)。 -
ResultMessage: 标志 agent 循环结束。包含最终文本结果、token 用量、成本和会话 ID。检查subtype字段以判断任务是成功还是触发了限制。少量后续系统事件(如prompt_suggestion)可能在它之后到达,因此应遍历流直到结束,而不是在拿到 result 后就 break。
这五种类型覆盖了 agent 循环的完整生命周期。两种 SDK 还会产出诸如速率限制状态、任务通知等可观测性事件,这些不是驱动循环所必需的。完整列表见 Python 和 TypeScript 各自的消息类型参考。
处理消息
你需要处理哪些消息取决于你在构建什么:
- 仅需最终结果: 处理
ResultMessage以获取输出、成本,以及任务是成功还是触发了限制。 - 进度更新: 处理
AssistantMessage以查看 Claude 每个回合在做什么,包括调用了哪些工具。 - 实时流式输出: 启用 partial messages(Python 中为
include_partial_messages,TypeScript 中为includePartialMessages)以实时获取StreamEvent消息。
如何检查消息类型因 SDK 而异:
- Python: 用
isinstance()对照从claude_agent_sdk导入的类来检查消息类型(例如isinstance(message, ResultMessage))。 - TypeScript: 检查字符串字段
type(例如message.type === "result")。AssistantMessage和UserMessage把原始 API 消息包装在.message字段中,因此内容块位于message.message.content,而不是message.content。
示例:检查消息类型并处理结果
import asyncio
from claude_agent_sdk import query, AssistantMessage, ResultMessage
async def main():
try:
async for message in query(prompt="Summarize this project"):
if isinstance(message, AssistantMessage):
print(f"Turn completed: {len(message.content)} content blocks")
if isinstance(message, ResultMessage):
if message.subtype == "success":
print(message.result)
else:
print(f"Stopped: {message.subtype}")
except Exception as error:
# A single-shot query() raises after yielding an error result. If the
# failure was an error result, the error subtype branches above have
# already run; connection or process failures yield no result message.
print(f"Session ended with an error: {error}")
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
try {
for await (const message of query({ prompt: "Summarize this project" })) {
if (message.type === "assistant") {
console.log(`Turn completed: ${message.message.content.length} content blocks`);
}
if (message.type === "result") {
if (message.subtype === "success") {
console.log(message.result);
} else {
console.log(`Stopped: ${message.subtype}`);
}
}
}
} catch (error) {
// A single-shot query() throws after yielding an error result. If the
// failure was an error result, the error subtype branches above have
// already run; connection or process failures yield no result message.
console.log(`Session ended with an error: ${error}`);
}
工具执行
工具赋予你的 agent 采取行动的能力。没有工具,Claude 只能用文本响应。有了工具,Claude 可以读取文件、运行命令、搜索代码、与外部服务交互。
内置工具
SDK 内置了与 Claude Code 相同的工具:
| 类别 | 工具 | 作用 |
|---|---|---|
| 文件操作 | Read、Edit、Write | 读取、修改和创建文件 |
| 搜索 | Glob、Grep | 按模式查找文件、用正则搜索内容 |
| 执行 | Bash | 运行 shell 命令、脚本、git 操作 |
| Web | WebSearch、WebFetch | 搜索网页、抓取并解析页面 |
| 发现 | ToolSearch | 按需动态查找并加载工具,而不是预先加载全部工具 |
| 编排 | Agent、Skill、AskUserQuestion、TaskCreate、TaskUpdate | 派生子代理、调用 skill、询问用户、跟踪任务 |
对于不提供任务跟踪工具的模型,Claude Code 仅在你显式选择启用(opt in)时才提供 TaskCreate 和 TaskUpdate。
除内置工具外,你还可以:
- 用 MCP servers 连接外部服务(数据库、浏览器、API)
- 用自定义工具处理器定义自定义工具
- 通过 setting sources 加载项目级 skill,以复用工作流
工具权限
Claude 根据任务决定调用哪些工具,但你控制这些调用是否被允许执行。你可以自动批准特定工具、完全阻止某些工具,或要求对所有工具进行审批。三个选项协同起作用来决定运行什么:
allowed_tools/allowedTools自动批准列出的工具。一个只读 agent 的允许列表若为["Read", "Glob", "Grep"],这些工具会在不提示的情况下运行。未列出的工具仍然可用,但需要权限。disallowed_tools/disallowedTools阻止列出的工具,无论其他设置如何。工具运行前规则的检查顺序见 Permissions 文档。permission_mode/permissionMode控制你想要多少人工监督。SDK 会按固定顺序,将当前模式与你的 allow/deny 规则一起评估,见「权限评估方式」部分。可用模式见下文「权限模式」。
你还可以用类似 "Bash(npm *)" 的规则对单个工具进行更细粒度的限定,仅允许特定命令。完整规则语法见 Permissions 文档。
工具被拒绝时,Claude 会收到一条拒绝消息作为工具结果,通常会尝试不同的方法或报告无法继续。
并行工具执行
当 Claude 在单个回合中请求多个工具调用时,两种 SDK 都可能并发或顺序执行它们,具体取决于工具类型。只读工具(如 Read、Glob、Grep,以及标记为只读的 MCP 工具)可以并发运行。会修改状态的工具(如 Edit、Write、Bash)会顺序执行以避免冲突。
自定义工具默认顺序执行。要为自定义工具启用并行执行,需要在其 annotations 中设置 readOnlyHint。TypeScript 和 Python SDK 都使用这个来自 MCP SDK 的字段名。
控制循环如何运行
你可以限制循环的回合数、花费、Claude 推理的深度,以及工具是否需要审批才能运行。这些都是 ClaudeAgentOptions(Python)/ Options(TypeScript)上的字段。
回合与预算
| 选项 | 控制什么 | 默认值 |
|---|---|---|
Max turns(max_turns / maxTurns) | 最大工具调用往返次数 | 无限制 |
Max budget(max_budget_usd / maxBudgetUsd) | 停止前的最大花费 | 无限制 |
任一限制被触发时,SDK 会返回一条带相应错误 subtype(error_max_turns 或 error_max_budget_usd)的 ResultMessage。如何检查这些 subtype 见「处理结果」部分,语法见 ClaudeAgentOptions / Options。
预算上限也覆盖子代理(subagent):它们的花费计入总额。一旦花费达到上限,再派生一个子代理会失败,报错 Budget limit reached,Claude Code 会停止仍在运行的后台子代理。这些上限强制行为需要 Claude Code v2.1.217 或更高版本。
使用流式输入(streaming input)时,如果你在某个回合仍在运行时发送一条消息,该消息会在该回合因 max-turns 限制结束时保持排队,并作为一个拥有自己 max-turns 限制的新回合启动。
推理强度(Effort level)
effort 选项控制 Claude 应用多少推理。更低的 effort 级别每回合使用更少的 token,降低成本。并非所有模型都支持 effort 参数,支持情况见官方 Effort 文档。
| 级别 | 行为 | 适用场景 |
|---|---|---|
"low" | 最少推理,快速响应 | 文件查找、列目录 |
"medium" | 均衡推理 | 常规编辑、标准任务 |
"high" | 深入分析 | 重构、调试 |
"xhigh" | 扩展推理深度 | 编码与 agentic 任务;在 Fable 5、Opus 4.7+ 和 Sonnet 5 上推荐 |
"max" | 最大推理深度 | 需要深度分析的多步骤问题 |
如果不设置 effort,两种 SDK 都不会设置该参数,而是沿用模型的默认行为。
说明:
effort是在每次响应内用延迟和 token 成本换取推理深度。Extended thinking(扩展思考)是一个独立功能,会产出thinking内容块;ThinkingConfig上的display字段(Python/TypeScript)控制你是否接收其文本。两者相互独立:你可以在启用 extended thinking 的同时设置effort: "low",也可以在不启用它的情况下设置effort: "max"。
对于做简单、范围明确任务(如列文件或运行单个 grep)的 agent,使用更低的 effort 以降低成本和延迟。可以在顶层 query() 选项中为整个会话设置 effort,也可以在 AgentDefinition 的 effort 字段上针对单个子代理覆盖会话级设置。
权限模式(Permission mode)
权限模式选项(Python 中为 permission_mode,TypeScript 中为 permissionMode)控制 agent 在使用工具前是否需要请求批准:
| 模式 | 行为 | 使用场景 |
|---|---|---|
"default" | 未被 allow 规则覆盖的工具会触发你的 canUseTool 回调;没有回调则默认拒绝 | 带有自定义审批回调的交互式应用 |
"acceptEdits" | 自动批准文件编辑和常见文件系统命令(mkdir、touch、mv、cp 等);其他 Bash 命令遵循默认规则 | 你信任 Claude 的编辑并想要更快的迭代,比如原型开发或在隔离目录中工作时 |
"plan" | Claude 探索和规划,但不编辑你的源文件;文件编辑永远不会自动批准,会通过你的 canUseTool 回调提示 | 你希望 Claude 提出变更方案而不实际执行,比如代码审查,或需要在变更执行前先批准的场景 |
"dontAsk" | 从不提示。被权限规则预先批准的工具会运行;其他一律拒绝。AskUserQuestion、被组织设为 ask 的连接器工具,以及被标记 requiresUserInteraction 的 MCP 工具,即使你已允许,也会被拒绝 | 你希望为无头(headless)agent 提供固定、明确的工具范围,并倾向于硬拒绝而不是默认在 canUseTool 缺失时静默处理 |
"auto" | 使用模型分类器来批准或拒绝权限提示 | 仍希望在工具使用上保留安全护栏的自主 agent |
"bypassPermissions" | 运行所有被允许的工具而不询问,除了被明确 ask 规则匹配的工具、被组织设为 ask 的连接器工具,以及需要用户交互的工具。跨会话消息安全防护措施仍然适用。TypeScript SDK 中还需要在 options 中设置 allowDangerouslySkipPermissions: true。在 Unix 上以 root 运行时不可用 | CI、容器或其他隔离环境 |
对于交互式应用,建议使用 "default" 配合工具审批回调来展示审批提示。对于开发机上的自主 agent,"acceptEdits" 会自动批准文件编辑和常见文件系统命令(mkdir、touch、mv、cp 等),同时仍将其他 Bash 命令置于 allow 规则的把关之下。"bypassPermissions" 应仅保留给 CI、容器或其他隔离环境使用。
模型(Model)
如果不设置 model,SDK 会使用 Claude Code 的默认模型,该默认值取决于你的认证方式和订阅情况。可显式设置(例如 model="claude-sonnet-5")以固定特定模型,或使用更小的模型来获得更快、更便宜的 agent。可用的模型 ID 见官方 models 文档。
上下文窗口
上下文窗口是会话期间 Claude 可用的信息总量。它在会话内的各回合之间不会重置。一切都会累积:系统提示词、工具定义、对话历史、工具输入和工具输出。在各回合间保持不变的内容(系统提示词、工具定义、CLAUDE.md)会被自动进行 prompt caching,从而降低重复前缀的成本和延迟。
什么会消耗上下文
SDK 中各组件对上下文的影响如下:
| 来源 | 何时加载 | 影响 |
|---|---|---|
| 系统提示词 | 每次请求 | 固定的小开销,始终存在 |
| CLAUDE.md 文件 | 会话开始时,通过 settingSources | 每次请求都携带完整内容(但会被 prompt-cache,因此只有第一次请求承担完整成本) |
| 工具定义 | 每次请求;MCP schema 默认延迟加载 | 内置工具 schema 每次请求都会加载。Tool search 默认延迟加载 MCP 工具 schema,在不支持的模型和某些平台上会回退为提前加载 |
| 对话历史 | 随回合累积 | 每个回合都会增长:prompt、响应、工具输入、工具输出 |
| Skill 描述 | 会话开始时,通过 setting sources | 简短摘要;完整内容仅在被调用时加载 |
大的工具输出会消耗大量上下文。读取一个大文件或运行输出冗长的命令,可能在单个回合中就消耗数千个 token。上下文会随回合累积,因此调用工具较多的长会话比短会话累积明显更多的上下文。
自动压缩(Compaction)
当上下文窗口接近上限时,SDK 会自动压缩对话:将较早的历史总结,以腾出空间,同时保留最近的交流和关键决策。压缩发生时,SDK 会在消息流中产出一条 type: "system"、subtype: "compact_boundary" 的消息(Python 中是 SystemMessage;TypeScript 中是独立的 SDKCompactBoundaryMessage 类型)。
压缩会用摘要替换较早的消息,因此对话早期的具体指示可能不会被保留。持久性规则应放在 CLAUDE.md 中(通过 settingSources 加载),而不是放在初始 prompt 中,因为 CLAUDE.md 内容会在每次请求中被重新注入。
你可以通过以下几种方式自定义压缩行为:
- CLAUDE.md 中的摘要指令: 压缩器会像读取其他上下文一样读取你的 CLAUDE.md,因此可以加入一段告诉它在摘要时应保留什么内容。压缩器按意图匹配,因此该段的标题可以自由命名。
PreCompact钩子: 在压缩发生前运行自定义逻辑,例如归档完整的会话记录。该钩子接收一个trigger字段(manual或auto)。- 手动压缩: 将
/compact作为 prompt 字符串发送以按需触发压缩。以这种方式发送的命令属于普通的 SDK 输入。
示例:CLAUDE.md 中的摘要指令
在项目的 CLAUDE.md 中添加一段,告诉压缩器要保留什么。标题名称没有特殊含义,可用任意清晰的标签。
# Summary instructions
When summarizing this conversation, always preserve:
- The current task objective and acceptance criteria
- File paths that have been read or modified
- Test results and error messages
- Decisions made and the reasoning behind them
保持上下文高效
对长时间运行的 agent 的几点策略:
- 用子代理处理子任务。 每个子代理都以全新对话开始(没有先前的消息历史,但会加载自己的系统提示词和项目级上下文,如 CLAUDE.md)。它看不到父级的回合,只有它的最终响应会作为工具结果返回给父级。主 agent 的上下文只会因这条摘要而增长,而不是完整的子任务记录。
- 对工具保持精简。 每个工具定义都会占用上下文空间。用
AgentDefinition的tools字段,把子代理限定在其所需的最小工具集内。 - 留意 MCP server 的开销。 MCP tool search 默认延迟加载 MCP 工具 schema 并按需加载。当 tool search 关闭或已回退为提前加载时,每个 MCP server 会把所有工具 schema 加进每次请求,因此几个工具很多的 server 就可能在 agent 开始工作前消耗大量上下文。
- 对常规任务使用更低的 effort。 对只需要读文件或列目录的 agent,将 effort 设为
"low",以降低 token 用量和成本。
会话与连续性
每次与 SDK 的交互都会创建或延续一个会话(session)。从 ResultMessage.session_id(两种 SDK 都可用)获取会话 ID 以便之后恢复。TypeScript SDK 还在 init SystemMessage 上直接暴露该字段;Python 中它嵌套在 SystemMessage.data 里。
恢复会话时,先前回合的完整上下文会被恢复:已读取的文件、已执行的分析、已采取的动作。你也可以 fork 一个会话,以分支出不同的方案而不修改原会话。
要跨无状态容器或 serverless 主机恢复会话,可传入一个 session_store / sessionStore 适配器,让 SDK 把会话记录镜像到你自己的后端,供另一台主机恢复。Claude Code 子进程仍会先写入本地磁盘。
说明: 在 Python 中,
ClaudeSDKClient会在多次调用间自动处理会话 ID。
处理结果(Result)
循环结束时,ResultMessage 会告诉你发生了什么并给出输出。subtype 字段(两种 SDK 都有)是检查终止状态的主要方式。
| Result subtype | 发生了什么 | result 字段是否可用? |
|---|---|---|
success | Claude 正常完成任务 | 是 |
error_max_turns | 在完成前触发了 maxTurns 限制 | 否 |
error_max_budget_usd | 在完成前触发了 maxBudgetUsd 限制 | 否 |
error_during_execution | 循环被错误中断(例如 API 故障或请求被取消) | 否 |
error_max_structured_output_retries | 在配置的重试次数内没有产出有效的结构化输出:每次尝试都未通过校验,或某次模型 fallback 撤回了已完成的输出且没有后续成功重试 | 否 |
result 字段包含最终文本输出,仅在 success 变体中存在,因此读取前务必先检查 subtype。
所有 result subtype 都携带 total_cost_usd、usage、num_turns 和 session_id,以便即使出错后也能追踪成本并恢复会话。需要注意两点:
- 会话崩溃后,最终结果是一条
error_during_execution,其成本字段可能被清零,stop_reason为null,进程会在产出该消息后退出。 - 在 Python 中,
total_cost_usd、usage和model_usage的类型是可选的,读取前需检查它们不是None。
usage 字段仅覆盖主 agent 循环。若需要整棵调用树的 token 和成本统计,请使用 modelUsage(Python 中为 model_usage)。
说明: 当查询以错误结果结束时:
- 单次调用的
query()会先产出最终结果消息,然后抛出一个包含失败文本的错误,如Reached maximum number of turns。这个抛出是有意为之的;如果你的代码需要在其后继续执行,请用 try 块包裹循环。底层的 Claude Code 进程也会以非零码退出。- 流式输入(streaming input)会话会保持存活,你可以继续发送消息,除非发生会话崩溃——那会产出最终的
error_during_execution结果并退出进程。
result 还包含一个 stop_reason 字段(TypeScript 中为 string | null,Python 中为 str | None),表示模型在最后一个回合为何停止生成。常见取值有 end_turn(模型正常完成)、max_tokens(达到输出 token 上限)和 refusal(模型拒绝了该请求)。对于循环产生的错误结果,stop_reason 携带循环结束前最后一次 assistant 响应的值;而会话崩溃后 Claude Code 合成的结果中,stop_reason 为 null。
要检测拒绝(refusal),检查 stop_reason === "refusal"(TypeScript)或 stop_reason == "refusal"(Python)。
钩子(Hooks)
Hooks 是在循环特定节点触发的回调:工具运行前、返回后、agent 完成时等等。一些常用的钩子:
| Hook | 何时触发 | 常见用途 |
|---|---|---|
PreToolUse | 工具执行前 | 校验输入、阻止危险命令 |
PostToolUse | 工具返回后 | 审计输出、触发副作用 |
UserPromptSubmit | prompt 被发送时 | 向 prompt 中注入额外上下文 |
Stop | agent 完成时 | 校验结果、保存会话状态 |
SubagentStart / SubagentStop | 子代理派生或完成时 | 跟踪并汇总并行任务结果 |
PreCompact | 上下文压缩前 | 在摘要前归档完整会话记录 |
Hooks 运行在你的应用进程中,而不是在 agent 的上下文窗口内,因此不会消耗上下文。Hooks 还可以使循环短路:一个拒绝工具调用的 PreToolUse 钩子会阻止该调用执行,Claude 会收到拒绝消息而不是执行结果。
两种 SDK 都支持上述所有事件。TypeScript SDK 还包含一些 Python 尚不支持的额外事件。
综合示例
以下示例把本文的关键概念整合进一个用于修复失败测试的 agent。它为 agent 配置了允许工具列表(自动批准,使 agent 自主运行)、项目设置,以及回合数和推理强度上的安全限制。循环运行期间,它捕获会话 ID 以便后续恢复,处理最终结果,并打印总成本。
由于单次调用的 query() 会在产出错误结果后抛出异常,循环被包裹在 try 块中,以便命中限制时脚本能干净退出。
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def run_agent():
session_id = None
try:
async for message in query(
prompt="Find and fix the bug causing test failures in the auth module",
options=ClaudeAgentOptions(
allowed_tools=[
"Read",
"Edit",
"Bash",
"Glob",
"Grep",
], # Listing tools here auto-approves them (no prompting)
setting_sources=[
"project"
], # Load CLAUDE.md, skills, hooks from current directory
max_turns=30, # Prevent runaway sessions
effort="high", # Thorough reasoning for complex debugging
),
):
# Handle the final result
if isinstance(message, ResultMessage):
session_id = message.session_id # Save for potential resumption
if message.subtype == "success":
print(f"Done: {message.result}")
elif message.subtype == "error_max_turns":
# Agent ran out of turns. Resume with a higher limit.
print(f"Hit turn limit. Resume session {session_id} to continue.")
elif message.subtype == "error_max_budget_usd":
print("Hit budget limit.")
else:
print(f"Stopped: {message.subtype}")
if message.total_cost_usd is not None:
print(f"Cost: ${message.total_cost_usd:.4f}")
except Exception as error:
# A single-shot query() raises after yielding an error result. If the
# failure was an error result, the error subtype branches above have
# already run; connection or process failures yield no result message.
print(f"Session ended with an error: {error}")
asyncio.run(run_agent())
import { query } from "@anthropic-ai/claude-agent-sdk";
let sessionId: string | undefined;
try {
for await (const message of query({
prompt: "Find and fix the bug causing test failures in the auth module",
options: {
allowedTools: ["Read", "Edit", "Bash", "Glob", "Grep"], // Listing tools here auto-approves them (no prompting)
settingSources: ["project"], // Load CLAUDE.md, skills, hooks from current directory
maxTurns: 30, // Prevent runaway sessions
effort: "high" // Thorough reasoning for complex debugging
}
})) {
// Save the session ID to resume later if needed
if (message.type === "system" && message.subtype === "init") {
sessionId = message.session_id;
}
// Handle the final result
if (message.type === "result") {
if (message.subtype === "success") {
console.log(`Done: ${message.result}`);
} else if (message.subtype === "error_max_turns") {
// Agent ran out of turns. Resume with a higher limit.
console.log(`Hit turn limit. Resume session ${sessionId} to continue.`);
} else if (message.subtype === "error_max_budget_usd") {
console.log("Hit budget limit.");
} else {
console.log(`Stopped: ${message.subtype}`);
}
console.log(`Cost: $${message.total_cost_usd.toFixed(4)}`);
}
}
} catch (error) {
// A single-shot query() throws after yielding an error result. If the
// failure was an error result, the error subtype branches above have
// already run; connection or process failures yield no result message.
console.log(`Session ended with an error: ${error}`);
}
agent 成功完成时,示例会打印一行 Done: 及 agent 对修复的摘要,随后打印类似 Cost: $0.0312 的一行。
后续步骤
理解了这个循环之后,根据你在构建什么,可以继续阅读:
- 还没运行过 agent? 从 quickstart 开始,安装 SDK 并看一个端到端运行的完整示例。
- 准备接入你的项目? 加载 CLAUDE.md、skills 和文件系统钩子,让 agent 自动遵循你的项目约定。
- 在构建交互式 UI? 启用 streaming,在循环运行时实时展示文本和工具调用。
- 需要更严格地控制 agent 能做什么? 用 permissions 锁定工具访问,并用 hooks 在工具调用执行前审计、阻止或转换它们。
- 运行长时间或高成本的任务? 把独立的工作交给 subagents,以保持主上下文精简。
- 要部署为服务? 参见 Hosting the Agent SDK 了解容器和 serverless 部署指南,以及 Session storage 了解如何把会话持久化到你自己的后端。