本页目录14
- 会话持久化的是「对话历史」(prompt、工具调用、工具结果、回复),不包含文件系统状态;要快照/回滚文件改动需用 file checkpointing
- 单次 query() 调用内 agent 会自主完成多轮,权限确认和 AskUserQuestion 都在循环内处理,不会中断调用——只有跨多个 query() 调用共享上下文时才需要会话管理
- continue 会自动找到当前目录下最近一次会话(无需记录 ID);resume 需要显式传入 session_id,适用于多用户或需要恢复非最近会话的场景
- fork 会基于原会话历史创建一个带有新 session_id 的独立会话,原会话历史保持不变,可用于在不丢失原线索的情况下尝试新方向
- TypeScript SDK 已在 0.3.142 移除实验性 V2 session API(createSession() 的 send/stream 模式),应使用 query() 及本文所述的会话选项
- 跨主机恢复会话时,建议使用 SessionStore adapter 镜像 transcript,或迁移 .jsonl 会话文件,而非依赖默认的本地磁盘会话
本文是对 Claude Agent SDK 官方文档「Work with sessions」页面的中文整理,完整与最新内容以原文为准:https://code.claude.com/docs/en/agent-sdk/sessions
什么是会话(session)
会话(session)是 SDK 在 agent 工作过程中累积的对话历史,包含你的 prompt、agent 发起的每一次工具调用、每一个工具结果,以及每一次回复。SDK 会自动将其写入磁盘,以便之后返回继续。
返回一个会话意味着 agent 拥有此前的完整上下文:已经读取过的文件、已经完成的分析、已经做出的决策。你可以借此追加提问、从中断中恢复,或分支出去尝试不同的方案。
注意:会话持久化的是对话,不是文件系统。要对 agent 所做的文件改动进行快照和回滚,请使用 file checkpointing。
本文涵盖:如何为你的应用选择合适的方案、自动追踪会话的 SDK 接口、如何手动捕获 session ID 并使用 resume 与 fork,以及跨主机恢复会话时需要了解的内容。
选择方案
需要多少会话管理取决于应用的形态。当你需要发送多个应该共享上下文的 prompt 时,才涉及会话管理。在单次 query() 调用内部,agent 已经会自主完成所需的多轮交互,权限确认和 AskUserQuestion 都是在循环内处理的(不会结束这次调用)。
| 你在构建什么 | 应该使用什么 |
|---|---|
| 一次性任务:单个 prompt,无需追问 | 无需额外处理,一次 query() 调用即可 |
| 单进程内的多轮对话 | ClaudeSDKClient(Python)或 continue: true(TypeScript)。SDK 会自动为你追踪会话,无需处理任何 ID |
| 进程重启后从上次停下的地方继续 | continue_conversation=True(Python)/ continue: true(TypeScript)。恢复该目录下最近的会话,无需 ID |
| 恢复某个特定的历史会话(而非最近一次) | 捕获 session ID 并传给 resume |
| 在不丢失原方案的前提下尝试另一种思路 | Fork 该会话 |
| 无状态任务,不希望写入任何磁盘内容 | 设置 persistSession: false(仅 TypeScript)。会话只在调用期间存在于内存中。Python 中可在 env 选项里设置 CLAUDE_CODE_SKIP_PROMPT_HISTORY 来抑制 transcript 写入 |
continue、resume、fork
continue、resume、fork 都是设置在 query() 上的选项字段(Python 中为 ClaudeAgentOptions,TypeScript 中为 Options)。
continue 和 resume 都是接续一个已有会话并在其基础上追加内容,区别在于如何找到该会话:
- continue:找到当前目录下最近的一次会话。你无需追踪任何东西,适合应用同一时间只运行一个对话的场景。
- resume:传入一个具体的 session ID。你需要自行追踪该 ID,适用于存在多个会话的场景(例如多用户应用中每个用户一个会话),或需要返回到非最近一次会话的场景。
fork 则不同:它会创建一个新会话,该会话以原会话历史的副本作为起点。原会话保持不变。用 fork 可以在保留「回退」选项的同时尝试另一个方向。
自动会话管理
两种 SDK 都提供了能跨调用自动追踪会话状态的接口,你无需手动传递 ID。适用于单进程内的多轮对话。
Python:ClaudeSDKClient
ClaudeSDKClient 会在内部处理 session ID。每次调用 client.query() 都会自动接续同一个会话。调用 client.receive_response() 来迭代当前 query 的消息。将 client 作为异步上下文管理器使用,连接的建立与关闭会自动处理;也可以手动调用 connect() 与 disconnect()。
下面的示例针对同一个 client 运行了两次查询:第一次让 agent 分析某个模块;第二次让它重构该模块。由于两次调用都通过同一个 client 实例,第二次查询自动拥有第一次的完整上下文,无需任何显式的 resume 或 session ID:
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
AssistantMessage,
ResultMessage,
TextBlock,
)
def print_response(message):
"""Print only the human-readable parts of a 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:
# First query: client captures the session ID internally
await client.query("Analyze the auth module")
async for message in client.receive_response():
print_response(message)
# Second query: automatically continues the same session
await client.query("Now refactor it to use JWT")
async for message in client.receive_response():
print_response(message)
asyncio.run(main())
每次查询都会打印 agent 的文本回复,随后是结果消息中的一行状态,例如 [done: success, cost: $0.0042]。
关于何时使用 ClaudeSDKClient 而非独立的 query() 函数,详见 Python SDK reference。
TypeScript:continue: true
TypeScript SDK 没有类似 Python ClaudeSDKClient 那样持有会话的 client 对象。相反,在每次后续的 query() 调用上传入 continue: true,SDK 就会自动找到并使用当前目录下最近的会话,无需追踪 ID。
下面的示例进行了两次独立的 query() 调用。第一次创建一个新会话;第二次设置了 continue: true,告诉 SDK 查找并恢复磁盘上最近的会话。agent 在第二次调用中拥有第一次调用的完整上下文:
import { query } from "@anthropic-ai/claude-agent-sdk";
// First query: creates a new session
try {
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);
}
}
} catch (error) {
// A single-shot query() throws after yielding an error result,
// so the follow-up query below still runs.
console.error(`Session ended with an error: ${error}`);
}
// Second query: continue: true resumes the most recent session
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 session API(提供
createSession()及send/stream模式)已在 TypeScript Agent SDK 0.3.142 中移除。请改用query()函数以及本文所述的会话选项。
通过 query() 使用会话选项
捕获 session ID
resume 与 fork 都需要 session ID。可以从结果消息(Python 中为 ResultMessage,TypeScript 中为 SDKResultMessage)的 session_id 字段中读取,无论成功还是出错该字段都会存在。在 TypeScript 中,该 ID 还会更早地作为 init SystemMessage 上的直接字段出现;在 Python 中它嵌套在 SystemMessage.data 内。
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
session_id = None
try:
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)
except Exception as error:
# A single-shot query() raises after yielding an error result. If the
# failure was an error result, the loop above already captured session_id;
# connection or process failures yield no result message, so session_id stays None.
print(f"Session ended with an error: {error}")
print(f"Session ID: {session_id}")
return session_id
session_id = asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
let sessionId: string | undefined;
try {
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);
}
}
}
} catch (error) {
// A single-shot query() throws after yielding an error result. If the
// failure was an error result, the loop above already captured sessionId;
// connection or process failures yield no result message, so sessionId stays undefined.
console.error(`Session ended with an error: ${error}`);
}
console.log(`Session ID: ${sessionId}`);
查询完成后,脚本会打印 agent 的回复,随后打印类似 Session ID: 5b3f2c1a-8d4e-4f6b-9a7c-2e1d0f9b8a6c 的一行。在后续小节中,你会把这个 ID 传给 resume。
按 ID 恢复(resume)
将 session ID 传给 resume 即可回到该特定会话,agent 会带着此前的完整上下文继续。常见的 resume 场景:
- 在已完成的任务基础上追问。agent 已经完成了某项分析,现在你希望它基于该分析采取行动,而无需重新读取文件。
- 从限制中恢复。第一次运行以
error_max_turns或error_max_budget_usd结束(参见 Handle the result),用更高的限制 resume。在单次query()调用中,SDK 会在产出该错误结果后抛出异常,因此需先捕获该异常再 resume。 - 重启进程。你在关闭前捕获了 ID,现在希望恢复该对话。
下面的示例在捕获 session ID 一节的基础上,用一个追问 prompt 恢复了该会话。由于是 resume,agent 已经在上下文中拥有此前的分析结果:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
session_id = "..." # The ID you captured in the previous example
async def main():
# Earlier session analyzed the code; now build on that analysis
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)
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
const sessionId = "..."; // The ID you captured in the previous example
// Earlier session analyzed the code; now build on that analysis
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);
}
}
你应该会看到一个基于此前分析继续展开的回复,而不是从零开始。这就说明 agent 恢复了会话且保留了此前的上下文。
提示:Claude Code 将会话存储在
~/.claude/projects/<encoded-cwd>/*.jsonl下。如果设置了CLAUDE_CONFIG_DIR环境变量,则应在$CLAUDE_CONFIG_DIR/projects/下查找。要找到会话所在目录,将工作目录绝对路径中所有非字母数字字符替换为-:/Users/me/proj会变成-Users-me-proj。如果转换后的目录名超过 200 个字符,Claude Code 会截断该名称并附加哈希,因此在projects/下查找时应匹配转换后名称的前 200 个字符。你可以从任意工作目录 resume:
- 跨目录查找:Claude Code 会在当前项目目录之外搜索该 ID;具体查找顺序及重复副本的处理方式参见 Resume a session。
- 仅限同一台机器:会话文件仍必须存在于当前这台机器上。
如果你在
CLAUDE_CONFIG_DIR之外还设置了CLAUDE_CODE_PROJECT_DIR_NAME,则改为在projects/下按该名称查找(需要 TypeScript Agent SDK v0.3.234 或更高版本)。在 v2.1.223 之前,查找范围仅限于当前项目目录及其 git worktree;绑定了更旧 CLI 的 SDK 版本仍保持这一旧行为。
要跨机器或在无服务器环境中 resume 会话,需通过 SessionStore adapter 将 transcript 镜像到共享存储。
Fork:探索备选方案
Fork 会创建一个新会话,该会话以原会话历史的副本作为起点,并从该点开始分叉。fork 出来的会话拥有自己的 session ID;原会话的 ID 和历史保持不变。最终你会得到两个可以分别独立 resume 的会话。
注意:Fork 分支的是对话历史,而不是文件系统。如果被 fork 出的 agent 编辑了文件,那些改动是真实存在的,并且对任何在同一目录下工作的会话都可见。要分支并回滚文件改动,请使用 file checkpointing。
下面的示例基于捕获 session ID一节:你已经在 session_id 中分析过某个 auth 模块,现在希望在不丢失 JWT 相关线索的前提下探索 OAuth2 方案。第一段代码 fork 该会话并捕获 fork 出的 ID(forked_id);第二段代码恢复原始 session_id 以继续 JWT 方向的对话。此时你会拥有两个指向两条独立历史的 session ID:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
session_id = "..." # The ID you captured in the previous example
async def main():
# Fork: branch from session_id into a new session
forked_id = None
try:
async for message in query(
prompt="Instead of JWT, outline how OAuth2 would work for the auth module",
options=ClaudeAgentOptions(
resume=session_id,
fork_session=True,
max_turns=5,
),
):
if isinstance(message, ResultMessage):
forked_id = message.session_id # The fork's ID, distinct from session_id
if message.subtype == "success":
print(message.result)
except Exception as error:
# A single-shot query() raises after yielding an error result. If the
# failure was an error result, forked_id was already captured by the
# loop above; connection or process failures yield no result message.
print(f"Session ended with an error: {error}")
print(f"Forked session: {forked_id}")
# Original session is untouched; resuming it continues the JWT thread
try:
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)
except Exception as error:
# A single-shot query() raises after yielding an error result.
print(f"Session ended with an error: {error}")
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
const sessionId = "..."; // The ID you captured in the previous example
// Fork: branch from sessionId into a new session
let forkedId: string | undefined;
try {
for await (const message of query({
prompt: "Instead of JWT, outline how OAuth2 would work for the auth module",
options: {
resume: sessionId,
forkSession: true,
maxTurns: 5
}
})) {
if (message.type === "system" && message.subtype === "init") {
forkedId = message.session_id; // The fork's ID, distinct from sessionId
}
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
} catch (error) {
// A single-shot query() throws after yielding an error result. If the
// failure was an error result, forkedId was already captured by the loop
// above; connection or process failures yield no result message.
console.error(`Session ended with an error: ${error}`);
}
console.log(`Forked session: ${forkedId}`);
// Original session is untouched; resuming it continues the JWT thread
try {
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);
}
}
} catch (error) {
// A single-shot query() throws after yielding an error result.
console.error(`Session ended with an error: ${error}`);
}
你应该会看到 forkedId 与原始 session ID 不同。恢复原始会话仍会继续 JWT 方向的讨论,这说明 fork 并没有修改原始历史。
涉及的会话相关选项字段
以下字段均在原文中以代码示例形式出现,类型据字面量用法标注,原文未给出默认值处均留空:
| 字段(TypeScript / Python) | 类型 | 默认值 | 说明 |
|---|---|---|---|
resume(TS)/ resume(Python ClaudeAgentOptions) | string(session ID) | 恢复指定的会话 ID,agent 携带该会话此前的完整上下文继续 | |
continue: true(TS)/ continue_conversation=True(Python) | boolean | 恢复当前目录下最近的一次会话,无需追踪 ID | |
forkSession(TS)/ fork_session(Python) | boolean | 与 resume 搭配使用,基于原会话历史副本创建一个新会话(新 session ID),原会话不受影响 | |
persistSession: false(仅 TypeScript,Options) | boolean | 会话仅存在于本次调用的内存中,不写入磁盘 | |
CLAUDE_CODE_SKIP_PROMPT_HISTORY(Python,通过 env 选项设置,参见 env-vars) | 环境变量 | 抑制 transcript 写入磁盘,Python 中用于实现类似 persistSession: false 的效果 | |
session_id(结果消息字段,Python ResultMessage、TypeScript SDKResultMessage) | string | 无论成功或出错都会存在;TypeScript 中在 init SystemMessage 上也可直接读到,Python 中嵌套在 SystemMessage.data 内 |
跨主机恢复会话
会话文件是本地存储在创建它的那台机器上的。要在不同主机(CI worker、临时容器、无服务器环境)上恢复会话,可以选择以下方式:
-
传入一个 session store。挂载
sessionStore/session_storeadapter,让 SDK 将 transcript 镜像到你自己的后端,以便其他主机恢复。该 store 的查找键来自工作目录,因此 resume 时使用的cwd需要与原始运行时一致。 -
迁移会话文件。将第一次运行产生的
~/.claude/projects/<encoded-cwd>/<session-id>.jsonl持久化保存,并在新主机~/.claude/projects/下的任意目录中恢复该文件,然后再调用resume。Claude Code 会在当前项目目录之外搜索该 ID;具体查找顺序及重复副本处理方式参见 Resume a session。v2.1.223 之前,查找范围仅限于当前项目目录及其 git worktree,绑定了旧版 CLI 的 SDK 仍保持该行为。
-
不依赖会话恢复。将所需结果(分析输出、决策、文件 diff)捕获为应用状态,再作为 prompt 的一部分传入一个全新的会话。这种方式通常比搬运 transcript 文件更可靠。
会话枚举与管理函数
两种 SDK 都提供了用于枚举磁盘上会话及读取其消息的函数,可用于构建自定义的会话选择器、清理逻辑或 transcript 查看器:
| 语言 | 函数 | 说明 |
|---|---|---|
| TypeScript | listSessions() | 枚举磁盘上的会话 |
| TypeScript | getSessionMessages() | 读取指定会话的消息 |
| Python | list_sessions() | 枚举磁盘上的会话 |
| Python | get_session_messages() | 读取指定会话的消息 |
两种 SDK 还提供了用于查询和修改单个会话的函数,可用于按标签组织会话或为会话赋予可读的标题:
| 语言 | 函数 | 说明 |
|---|---|---|
| Python | get_session_info() | 查询单个会话的信息 |
| Python | rename_session() | 重命名会话 |
| Python | tag_session() | 给会话打标签 |
| TypeScript | getSessionInfo() | 查询单个会话的信息 |
| TypeScript | renameSession() | 重命名会话 |
| TypeScript | tagSession() | 给会话打标签 |
相关资源
- How the agent loop works:理解会话内的轮次(turn)、消息与上下文累积
- File checkpointing:快照并回滚会话内 agent 所做的文件改动
- Python
ClaudeAgentOptions:Python 完整的会话相关选项参考 - TypeScript
Options:TypeScript 完整的会话相关选项参考