Appearance
Agent SDK 自动将每次 query() 的对话历史写入磁盘,你可以在后续调用中使用 continue(自动续接最近会话)、resume(按 ID 恢复指定会话)或 fork(从历史分叉出新会话)来保持上下文。捕获 session_id 在结果消息中,通过 ClaudeSDKClient(Python)或 continue: true(TypeScript)实现进程内的多轮对话。会话文件存储在 ~/.claude/projects/<编码后的cwd>/ 下,跨主机恢复需要移动文件或改用应用状态传递。
Claude Code 会话管理:resume、fork 与跨主机恢复
会话是 Agent SDK 在代理工作过程中积累的对话历史,包含你的提示词、每一次工具调用、工具结果以及响应。SDK 自动将会话写入磁盘,以便后续恢复。
恢复会话意味着代理拥有之前的全部上下文:它已经读过的文件、已经完成的分析、已经做出的决策。你可以追问问题、从中断中恢复,或者分支到另一个方向尝试不同方案。
会话持久化的是对话,而不是文件系统。要快照和回滚代理对文件所做的更改,请使用文件检查点。
本指南涵盖如何为你的应用选择合适的方式、SDK 自动跟踪会话的接口、如何手动获取会话 ID 并使用 resume 和 fork,以及跨主机恢复会话的注意事项。
选择会话管理方式
你需要多少会话管理取决于应用的场景。当你发送多个需要共享上下文的提示词时,会话管理就会发挥作用。在单次 query() 调用内,代理已经会自动进行它所需的多次回合,权限提示和 AskUserQuestion 会在循环内处理(不会结束调用)。
| 你正在构建的场景 | 推荐做法 |
|---|---|
| 一次性任务:单提示,无后续 | 什么都不用做。一个 query() 调用即可。 |
| 同一进程内的多轮对话 | 使用 ClaudeSDKClient (Python) 或 continue: true (TypeScript)。SDK 自动跟踪会话,无需处理 ID。 |
| 进程重启后接续之前的工作 | 设置 continue_conversation=True (Python) / continue: true (TypeScript)。自动恢复当前目录下最近的会话,无需 ID。 |
| 恢复某个特定的历史会话(不是最近的) | 捕获会话 ID 并传给 resume。 |
| 尝试另一种方向而不丢失原始会话 | 使用 fork 分支。 |
| 无状态任务,不希望写入磁盘(仅 TypeScript) | 设置 persistSession: false。会话仅在调用期间存在于内存中。Python 始终会持久化到磁盘。 |
continue、resume 和 fork 的区别
continue、resume 和 fork 都是 query() 的可选字段(Python 的 ClaudeAgentOptions,TypeScript 的 Options)。
continue 和 resume 都会接续现有会话并添加新内容,区别在于如何找到该会话:
- continue 会自动找到当前目录下最近的会话。你无需跟踪任何内容。适用于你的应用每次只运行一个对话的场景。
- resume 需要一个具体的会话 ID。你需要自己跟踪这个 ID。当你有多条会话(例如多用户应用中每个用户一条)或者想恢复的不是最近的会话时,必须使用 resume。
fork 则不同:它会创建一个新会话,新会话从原始会话的历史副本开始。原始会话保持不变。使用 fork 可以尝试不同的方向,同时保留回到原始会话的选项。
自动会话管理
两个 SDK 都提供了在多次调用之间自动跟踪会话状态的接口,无需手动传递 ID。适用于同一进程内的多轮对话。
Python:ClaudeSDKClient
ClaudeSDKClient 内部处理会话 ID。每次调用 client.query() 会自动延续同一个会话。通过 client.receive_response() 迭代当前查询的消息。客户端通常作为异步上下文管理器使用。
以下示例对同一个 client 运行两次查询。第一次让代理分析一个模块,第二次让代理重构该模块。因为两次调用使用同一个客户端实例,第二次查询拥有第一次的完整上下文,无需显式 resume 或会话 ID:
python
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
AssistantMessage,
ResultMessage,
TextBlock,
)
def print_response(message):
"""打印消息中人类可读的部分。"""
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(block.text)
elif isinstance(message, ResultMessage):
cost = (
f"${message.total_cost_usd:.4f}"
if message.total_cost_usd is not None
else "N/A"
)
print(f"[done: {message.subtype}, cost: {cost}]")
async def main():
options = ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Glob", "Grep"],
)
async with ClaudeSDKClient(options=options) as client:
# 第一次查询:客户端内部捕获会话 ID
await client.query("Analyze the auth module")
async for message in client.receive_response():
print_response(message)
# 第二次查询:自动延续同一个会话
await client.query("Now refactor it to use JWT")
async for message in client.receive_response():
print_response(message)
asyncio.run(main())关于何时使用 ClaudeSDKClient 与独立 query() 函数的详细信息,请参阅 Python SDK 参考。
TypeScript:continue: true
TypeScript SDK 没有类似 Python 的 ClaudeSDKClient 这样的会话持有对象。取而代之的是,在后续的每个 query() 调用中传入 continue: true,SDK 会拾取当前目录下最近的会话。无需跟踪 ID。
以下示例进行两次独立的 query() 调用。第一次创建新会话;第二次设置 continue: true,告诉 SDK 找到并恢复磁盘上最近的会话。代理拥有第一次调用的完整上下文:
typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
// 第一次查询:创建新会话
for await (const message of query({
prompt: "Analyze the auth module",
options: { allowedTools: ["Read", "Glob", "Grep"] }
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
// 第二次查询:continue: true 恢复最近的会话
for await (const message of query({
prompt: "Now refactor it to use JWT",
options: {
continue: true,
allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"]
}
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}实验性的 V2 会话 API(提供
createSession()和send/stream模式)已在 TypeScript Agent SDK 0.3.142 中移除。请使用本页描述的query()函数和会话选项。
在 query() 中使用会话选项
捕获会话 ID
resume 和 fork 都需要会话 ID。从结果消息(Python 的 ResultMessage,TypeScript 的 SDKResultMessage)的 session_id 字段读取,该字段在每次结果中都会出现,无论成功还是错误。在 TypeScript 中,会话 ID 也可更早地从初始 SystemMessage 的直接字段获得;在 Python 中它嵌套在 SystemMessage.data 内部。
python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
session_id = None
async for message in query(
prompt="Analyze the auth module and suggest improvements",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"],
),
):
if isinstance(message, ResultMessage):
session_id = message.session_id
if message.subtype == "success":
print(message.result)
print(f"Session ID: {session_id}")
return session_id
session_id = asyncio.run(main())typescript
import { query } from "@anthropic-ai/claude-agent-sdk";
let sessionId: string | undefined;
for await (const message of query({
prompt: "Analyze the auth module and suggest improvements",
options: { allowedTools: ["Read", "Glob", "Grep"] }
})) {
if (message.type === "result") {
sessionId = message.session_id;
if (message.subtype === "success") {
console.log(message.result);
}
}
}
console.log(`Session ID: ${sessionId}`);通过 ID 恢复会话(resume)
将会话 ID 传给 resume 可以恢复那个特定的会话。代理能从上次中断的地方继续,拥有完整上下文。常见的恢复场景:
- 对已完成的任务进行后续跟进。 代理已经分析过某内容,现在你需要它基于分析结果行动,无需重新读取文件。
- 从限制中恢复。 第一次运行以
error_max_turns或error_max_budget_usd结束(参见处理结果),使用更高的限制恢复。 - 重启进程。 你在关闭前捕获了会话 ID,现在想恢复对话。
以下示例从捕获会话 ID 恢复会话,并发出后续提示。因为是恢复,代理已经拥有之前分析过的上下文:
python
# 之前的会话分析过代码;现在基于该分析继续
async for message in query(
prompt="Now implement the refactoring you suggested",
options=ClaudeAgentOptions(
resume=session_id,
allowed_tools=["Read", "Edit", "Write", "Glob", "Grep"],
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)typescript
// 之前的会话分析过代码;现在基于该分析继续
for await (const message of query({
prompt: "Now implement the refactoring you suggested",
options: {
resume: sessionId,
allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"]
}
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}如果
resume调用返回的是全新的会话而非预期的历史,最常见的原因是cwd(当前工作目录)不匹配。会话存储在~/.claude/projects/<编码后的cwd>/*.jsonl下,其中<编码后的cwd>是绝对工作目录的每个非字母数字字符被替换为-的结果(例如/Users/me/proj变成-Users-me-proj)。如果你的 resume 调用从不同的目录运行,SDK 会在错误的地方查找。会话文件也必须在当前机器上存在。
要在不同机器或 serverless 环境中恢复会话,请使用 SessionStore 适配器 将会话转录镜像到共享存储。
分支探索(fork)
Fork 会创建一个新会话,该会话从原始会话的历史副本开始,但从此处开始分叉。新会话有自己的会话 ID;原始会话的 ID 和历史保持不变。最终你会得到两条可以分别恢复的独立会话。
Fork 分支的是对话历史,而不是文件系统。如果分叉后的代理编辑了文件,这些更改是真实的,任何在该目录下工作的会话都能看到。要分支并回滚文件更改,请使用文件检查点。
以下示例基于捕获会话 ID:你已经在 session_id 中分析了 auth 模块,现在想探索 OAuth2 而不丢失 JWT 方向上的线程。第一个代码块分叉会话并捕获新会话的 ID(forked_id);第二个代码块恢复原始 session_id 继续 JWT 方向。你现在有两个会话 ID,指向两条独立的历史:
python
# Fork:从 session_id 分支到新会话
forked_id = None
async for message in query(
prompt="Instead of JWT, implement OAuth2 for the auth module",
options=ClaudeAgentOptions(
resume=session_id,
fork_session=True,
),
):
if isinstance(message, ResultMessage):
forked_id = message.session_id # 新分叉的 ID,与 session_id 不同
if message.subtype == "success":
print(message.result)
print(f"Forked session: {forked_id}")
# 原始会话不变;恢复它将继续 JWT 线程
async for message in query(
prompt="Continue with the JWT approach",
options=ClaudeAgentOptions(resume=session_id),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)typescript
// Fork:从 sessionId 分支到新会话
let forkedId: string | undefined;
for await (const message of query({
prompt: "Instead of JWT, implement OAuth2 for the auth module",
options: {
resume: sessionId,
forkSession: true
}
})) {
if (message.type === "system" && message.subtype === "init") {
forkedId = message.session_id; // 新分叉的 ID,与 sessionId 不同
}
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
console.log(`Forked session: ${forkedId}`);
// 原始会话不变;恢复它将继续 JWT 线程
for await (const message of query({
prompt: "Continue with the JWT approach",
options: { resume: sessionId }
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}跨主机恢复
会话文件是创建它的机器本地的。要在另一台主机(CI 工作节点、临时容器、serverless)上恢复会话,你有两种选择:
- 移动会话文件。 将第一次运行生成的
~/.claude/projects/<编码后的cwd>/<会话ID>.jsonl持久化,然后在调用resume前将其恢复到新主机上的相同路径。cwd必须匹配。 - 不依赖会话恢复。 将你需要的结果(分析输出、决策、文件差异)作为应用状态捕获,然后将其传入新会话的提示词中。这通常比传输会话转录文件更健壮。
两个 SDK 都提供了枚举磁盘上会话并读取其消息的函数:TypeScript 的 listSessions() 和 getSessionMessages(),Python 的 list_sessions() 和 get_session_messages()。可以用它们构建自定义会话选择器、清理逻辑或转录查看器。
两个 SDK 还提供了查看和修改单个会话的函数:Python 的 get_session_info()、rename_session() 和 tag_session(),TypeScript 的 getSessionInfo()、renameSession() 和 tagSession()。可以用它们按标签组织会话或赋予人类可读的标题。
相关资源
- 代理循环工作机制:理解会话内的回合、消息和上下文积累
- 文件检查点:跨会话跟踪和回滚文件更改
- Python
ClaudeAgentOptions:完整的会话选项参考 - TypeScript
Options:完整的会话选项参考
常见问题
怎么恢复之前的会话?
使用 resume 参数传入会话 ID。会话 ID 可以从上一次 query() 的结果消息的 session_id 字段获取。确保当前工作目录与创建会话时相同,否则 SDK 会在不同路径下查找文件。
会话文件存在哪里?
会话文件存储在 ~/.claude/projects/<编码后的cwd>/<会话ID>.jsonl。其中 <编码后的cwd> 是将绝对工作目录中的非字母数字字符替换为 - 的结果,例如 /Users/me/proj 变成 -Users-me-proj。
fork 和 resume 有什么区别?
resume 接续现有会话,新内容追加到原会话中;fork 创建原始会话的副本并在此基础上继续,原会话保持不变。fork 适用于你想尝试不同方向但保留回头路的情况,而 resume 用于正常接续。