Claude Code 学习站

Agent SDK 会话管理:continue、resume 与 fork

介绍 Claude Agent SDK 中会话(session)的持久化机制,以及如何用 continue、resume、fork 在多轮对话、进程重启和跨主机场景中恢复或分支会话。

本页目录14
AI 摘要 · 已核查整理于 2026-08-15原文:Work with sessions(Anthropic)Agent SDK会话管理TypeScriptPython
要点速览
  • 会话持久化的是「对话历史」(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 并使用 resumefork,以及跨主机恢复会话时需要了解的内容。

选择方案

需要多少会话管理取决于应用的形态。当你需要发送多个应该共享上下文的 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)。

continueresume 都是接续一个已有会话并在其基础上追加内容,区别在于如何找到该会话:

  • 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

resumefork 都需要 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_turnserror_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)booleanresume 搭配使用,基于原会话历史副本创建一个新会话(新 session ID),原会话不受影响
persistSession: false(仅 TypeScript,Optionsboolean会话仅存在于本次调用的内存中,不写入磁盘
CLAUDE_CODE_SKIP_PROMPT_HISTORY(Python,通过 env 选项设置,参见 env-vars)环境变量抑制 transcript 写入磁盘,Python 中用于实现类似 persistSession: false 的效果
session_id(结果消息字段,Python ResultMessage、TypeScript SDKResultMessagestring无论成功或出错都会存在;TypeScript 中在 init SystemMessage 上也可直接读到,Python 中嵌套在 SystemMessage.data

跨主机恢复会话

会话文件是本地存储在创建它的那台机器上的。要在不同主机(CI worker、临时容器、无服务器环境)上恢复会话,可以选择以下方式:

  • 传入一个 session store。挂载 sessionStore / session_store adapter,让 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 查看器:

语言函数说明
TypeScriptlistSessions()枚举磁盘上的会话
TypeScriptgetSessionMessages()读取指定会话的消息
Pythonlist_sessions()枚举磁盘上的会话
Pythonget_session_messages()读取指定会话的消息

两种 SDK 还提供了用于查询和修改单个会话的函数,可用于按标签组织会话或为会话赋予可读的标题:

语言函数说明
Pythonget_session_info()查询单个会话的信息
Pythonrename_session()重命名会话
Pythontag_session()给会话打标签
TypeScriptgetSessionInfo()查询单个会话的信息
TypeScriptrenameSession()重命名会话
TypeScripttagSession()给会话打标签

相关资源