构建会话浏览器:使用Claude Agent SDK管理会话历史

本教程面向有编程基础的开发者,深入讲解如何利用Claude Agent SDK的本地会话管理功能,构建类似Claude Code Desktop的会话侧边栏。内容涵盖会话的创建、列出、读取、重命名、标签过滤、分叉与恢复,以及删除等核心操作。通过实际代码示例,展示如何高效管理代理产生的JSONL转录文件,并探讨了软删除、分页、跨主机会话等高级话题,帮助开发者掌握构建AI产品用户界面的关键工程实践。

技术背景

在将AI代理作为产品(如桌面应用、IDE扩展或聊天机器人)发布时,用户最迫切的需求之一就是会话历史管理。他们希望查看过去的对话、继续之前的讨论,或在不丢失原始上下文的情况下分支探索新方向。然而,代理循环本身只解决了单次交互的问题,而产品体验的另一半——会话管理——往往被忽视。Claude Agent SDK将每次对话以JSONL格式转录到磁盘,并提供了一组函数来读取和组织这些转录文件,使得开发者无需手动解析文件或遍历目录,即可构建功能完整的会话侧边栏。本教程旨在填补这一空白,帮助开发者掌握这些关键原语,从而在Agent SDK之上构建出用户友好的界面。

核心原理

Claude Agent SDK的会话管理基于本地文件系统存储。每次调用query()时,对话内容会以JSONL格式追加到~/.claude/projects/<encoded-cwd>/<session-id>.jsonl文件中。cwd选项决定了会话归属的项目目录,所有使用相同cwd的调用共享同一个存储桶。SDK提供的会话管理函数(如list_sessions、get_session_messages等)直接读取这些文件,不启动代理子进程,也不进行API调用,因此性能高效且独立于代理运行状态。核心设计决策包括:使用文件头部和尾部切片而非完整解析来快速获取元数据;通过追加操作实现重命名和标签,避免文件重写;分叉通过复制文件并重新映射消息ID实现,原始会话保持不变。

架构分析

本教程的架构围绕本地会话存储层展开,该层由Claude Agent SDK提供,位于~/.claude/projects/目录下。上层是会话管理API,包括list_sessions、get_session_info、get_session_messages、rename_session、tag_session、fork_session和delete_session。这些API封装了文件操作,为UI层提供数据。UI层可以是任何前端框架,通过调用这些API(直接或通过后端服务)来渲染会话列表、消息视图和操作按钮。数据流为:代理运行产生转录文件 -> 会话管理API读取文件返回结构化数据 -> UI展示给用户。关键设计决策包括:使用cwd隔离项目,便于按项目管理会话;分页支持(limit/offset)确保长列表性能;软删除模式(通过标签)避免误删数据。

完整实战教程

当你将代理作为产品(桌面应用、IDE扩展、内部聊天机器人)发布时,用户首先要求的就是侧边栏。他们想看到上周二的对话,跳回去继续,或者在不丢失原始对话的情况下分支到新方向。代理循环只是产品的一半;另一半是会话管理。

Claude Agent SDK 将每次对话写入磁盘上的 JSONL 转录文件。它还提供了一组函数,用于读取这些转录文件并组织它们,因此你可以构建侧边栏,而无需编写文件解析器或手动遍历 ~/.claude/projects/。

通过本教程,你将能够:

这是 Claude Code Desktop 和 VS Code 扩展中会话侧边栏背后的模式。同样的原语适用于你希望在 Agent SDK 之上构建的任何 UI。

先决条件

在遵循本指南之前,请确保您具备:

所需知识

所需工具

设置

安装所需的依赖项。会话管理功能在 claude-agent-sdk v0.1.51 中引入。

%%capture
%pip install -U "claude-agent-sdk>=0.1.51" python-dotenv pandas

从 .env 加载你的 API 密钥并配置模型。这里我们使用 Haiku,因为演示会话较短,我们希望它们便宜且快速;在实际产品中,你可以选择适合你的代理的任何模型。

import os
from pathlib import Path

from dotenv import load_dotenv

load_dotenv()

MODEL = "claude-haiku-4-5"

# All demo sessions live under this project directory. Using a dedicated
# cwd keeps the demo isolated from your real Claude Code sessions.
# Note: this path resolves relative to the kernel's working directory
# (claude_agent_sdk/ when launched per the README).
DEMO_DIR = str(Path("session_browser_demo").resolve())
os.makedirs(DEMO_DIR, exist_ok=True)
print(f"Demo project dir: {DEMO_DIR}")

第一部分:创建一些会话进行管理

会话管理函数读取query()(或Claude Code CLI)已经写入的转录。为了有内容可浏览,我们首先运行三个简短的对话并捕获它们的会话ID。

cwd选项告诉SDK这个对话属于哪个项目目录。转录文件存放在~/.claude/projects/<encoded-cwd>/<session-id>.jsonl下,因此所有使用相同cwd的调用最终都会归入同一个桶。这个桶就是之后list_sessions(directory=...)读取的内容。

我们禁用工具并将每次运行限制为一轮,以保持令牌使用量最小。

from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query


async def run_one_turn(prompt: str) -> str:
    """Run a single-turn conversation and return its session_id."""
    opts = ClaudeAgentOptions(
        model=MODEL,
        cwd=DEMO_DIR,
        max_turns=1,
        allowed_tools=[],  # text-only, no tool loop
    )
    session_id = None
    async for msg in query(prompt=prompt, options=opts):
        if isinstance(msg, ResultMessage):
            session_id = msg.session_id
            preview = (msg.result or "")[:80]
            print(f"[{session_id[:8]}] {preview}...")
    if session_id is None:
        raise RuntimeError("No ResultMessage received; check API key and SDK version.")
    return session_id

三个提示词,三个会话。在实际产品中,这些就是你的用户向智能体提出的任何请求。

prompts = [
    "Give me three name ideas for a CLI tool that manages git worktrees.",
    "Explain the difference between a mutex and a semaphore in one paragraph.",
    "Write a haiku about merge conflicts.",
]

demo_session_ids = []
for p in prompts:
    sid = await run_one_turn(p)
    demo_session_ids.append(sid)

print(f"\nCreated {len(demo_session_ids)} sessions.")

第二部分:列出并检查会话

构建会话列表

list_sessions() 扫描项目的记录目录,并返回每个会话的元数据,按最新排序。它读取文件统计信息以及每个记录的开头和结尾,因此即使目录中有数百个文件也能保持快速。不会生成子进程,也不会进行 API 调用。

每个 SDKSessionInfo 都包含你在选择器中渲染一行所需的信息:显示摘要、最后修改时间戳、git 分支、工作目录,以及你设置的任何自定义标题或标签。

from datetime import datetime

import pandas as pd

from claude_agent_sdk import SDKSessionInfo, list_sessions

sessions = list_sessions(directory=DEMO_DIR)

# Render as a table. In a real UI this would be your sidebar component.
rows = []
for s in sessions:
    rows.append(
        {
            "id": s.session_id[:8],
            "summary": (s.summary[:50] + "...") if len(s.summary) > 50 else s.summary,
            "modified": datetime.fromtimestamp(s.last_modified / 1000).strftime("%H:%M:%S"),
            "branch": s.git_branch or "-",
            "tag": s.tag or "-",
        }
    )

pd.DataFrame(rows)

对于较长的历史记录,可通过传递 limit 和 offset 参数进行分页。会话选择器通常先加载第一页,然后在用户滚动时获取更多内容:

page_2 = list_sessions(directory=DEMO_DIR, limit=20, offset=20)

如果你的应用已经存储了会话 ID(例如,在你自己数据库中与用户记录一起存储),并且你只需要那一行数据,那么 get_session_info() 比列出所有内容更节省资源。

from claude_agent_sdk import get_session_info

info = get_session_info(demo_session_ids[0], directory=DEMO_DIR)

print(f"Session:      {info.session_id}")
print(f"Summary:      {info.summary}")
print(f"First prompt: {info.first_prompt}")
print(f"Created:      {datetime.fromtimestamp(info.created_at / 1000)}")
print(f"Size:         {info.file_size:,} bytes")

读取会话消息

当用户在侧边栏点击某个会话时,你会将其对话加载到主视图中。get_session_messages() 从记录中重建消息链,并按顺序返回用户和助手的轮次。与列表函数一样,它也是纯文件读取:代理无需运行。

每个 SessionMessage 都有一个 type("user" 或 "assistant")、一个 uuid,以及一个与 Anthropic Messages API 格式相同的 message 字典(role、content)。

from claude_agent_sdk import get_session_messages

messages = get_session_messages(demo_session_ids[0], directory=DEMO_DIR)

for m in messages:
    role = m.type
    # content is a list of blocks; pull out the text ones
    text_parts = [
        b.get("text", "")
        for b in m.message.get("content", [])
        if isinstance(b, dict) and b.get("type") == "text"
    ]
    text = " ".join(text_parts).strip()
    print(f"[{role:>9}] {text[:100]}")

对于长时间的会话,limit 和 offset 允许你一次加载一个窗口。聊天视图可能在打开时加载最近 50 条消息,然后在用户向上滚动时获取更早的页面。偏移量按时间顺序(从最旧到最新)应用,因此第 0 页是对话的开始。

第3部分:使用标题和标签进行组织

重命名会话

自动生成的摘要适合快速浏览,但用户通常希望给会话起一个真正的名称。rename_session() 会在记录中追加一个标题条目;list_sessions() 在下次读取时会将其作为 custom_title 获取。

追加操作成本低且幂等:调用两次重命名只会让较新的标题生效。不会发生文件重写。

from claude_agent_sdk import rename_session

rename_session(demo_session_ids[0], "Worktree CLI naming brainstorm", directory=DEMO_DIR)
rename_session(demo_session_ids[2], "Haiku corner", directory=DEMO_DIR)

# Verify the titles stuck
for s in list_sessions(directory=DEMO_DIR):
    label = s.custom_title or "(auto)"
    print(f"{s.session_id[:8]}  custom_title={label!r}  summary={s.summary[:40]!r}")

标签与过滤

标签是附加到会话上的单个字符串。你可以根据产品需求使用它们进行任意分类:"archived"、"needs-review"、"favorite"。传入 None 可清除标签。

一个常见的模式是软删除:不删除转录文件,而是将其标记为 "__hidden",并在列表视图中过滤掉。数据仍然可以恢复。

from claude_agent_sdk import tag_session

# Mark two sessions as favorites, hide the other
tag_session(demo_session_ids[0], "favorite", directory=DEMO_DIR)
tag_session(demo_session_ids[2], "favorite", directory=DEMO_DIR)
tag_session(demo_session_ids[1], "__hidden", directory=DEMO_DIR)


def visible_sessions(directory: str, tag_filter: str | None = None) -> list[SDKSessionInfo]:
    """List sessions, hiding soft-deletes and optionally filtering by tag."""
    results = []
    for s in list_sessions(directory=directory):
        if s.tag == "__hidden":
            continue
        if tag_filter is not None and s.tag != tag_filter:
            continue
        results.append(s)
    return results


favorites = visible_sessions(DEMO_DIR, tag_filter="favorite")
print(f"Visible favorites: {len(favorites)}")
for s in favorites:
    print(f"  {s.session_id[:8]}  [{s.tag}]  {s.custom_title or s.summary}")

标签是单一值,不是列表。如果你需要多个维度(比如,一个状态加一个类别),可以将它们编码成一个字符串,如 "review:urgent",并在读取时解析,或者将更丰富的状态存储在你自己的数据库中,以 session_id 为键。

第4部分:分支与恢复

从现有对话分支

分支会将会话记录复制到一个新文件中,并重新映射消息ID。原始会话保持不变。这是“尝试不同方法”功能背后的原语:用户保留原始线程,并获得一个新线程进行实验。

fork_session() 写入新文件并返回其ID。它不会运行代理,因此分支会保留在磁盘上,直到您使用 query() 恢复它。

from claude_agent_sdk import fork_session

source = demo_session_ids[0]

fork = fork_session(
    source,
    directory=DEMO_DIR,
    title="Worktree CLI names (round 2)",
)

print(f"Source: {source}")
print(f"Fork:   {fork.session_id}")

# The fork starts with the same message history as the source
source_msgs = get_session_messages(source, directory=DEMO_DIR)
fork_msgs = get_session_messages(fork.session_id, directory=DEMO_DIR)
print(f"Source has {len(source_msgs)} messages, fork has {len(fork_msgs)}")

要从特定点而不是完整历史进行分支,请传入 up_to_message_id。分支将包含源对话记录,直到并包括该消息。你可以通过 get_session_messages()[i].uuid 获取消息 UUID。

将分支恢复为实时查询

分支只是一个对话记录文件。要将其恢复为正在进行的对话,请将其 ID 交给 ClaudeAgentOptions.resume。代理会加载分支历史并从中继续。

resume_opts = ClaudeAgentOptions(
    model=MODEL,
    cwd=DEMO_DIR,
    max_turns=1,
    allowed_tools=[],
    resume=fork.session_id,
)

async for msg in query(
    prompt="Those were okay. Give me three more names, but punnier.",
    options=resume_opts,
):
    if isinstance(msg, ResultMessage):
        print(f"[fork {fork.session_id[:8]} resumed]")
        print(msg.result)

原始会话仍然保持不变地留在那里。再次列出,你会看到源会话和分叉会话作为独立的行,各自拥有独立的历史记录。

for s in list_sessions(directory=DEMO_DIR):
    marker = "(fork)" if s.session_id == fork.session_id else "      "
    print(f"{marker} {s.session_id[:8]}  {s.custom_title or s.summary[:50]}")

清理

delete_session() 会删除一个转录文件。这是硬删除,这就是为什么第 3 部分中的软删除标签模式通常是面向用户界面的更安全默认选择。

这里我们用它来清理演示创建的所有内容。

from claude_agent_sdk import delete_session

# Clean up every session in the demo dir, including the fork
for s in list_sessions(directory=DEMO_DIR):
    delete_session(s.session_id, directory=DEMO_DIR)
    print(f"Deleted {s.session_id[:8]}")

remaining = list_sessions(directory=DEMO_DIR)
print(f"\n{len(remaining)} session(s) remaining.")

回顾

我们基于 Agent SDK 的本地会话记录存储构建了一个会话浏览器的核心功能:

所有这些操作都是对 ~/.claude/projects/ 目录下的纯文件操作。无论代理子进程是否在运行,它们都能正常工作,并且能看到与 Claude Code CLI 写入的相同记录。

后续方向

常见问题

常见错误包括:1. 未安装正确版本的SDK(需>=0.1.51),导致函数不存在;2. cwd路径不一致,导致会话列表为空;3. 未设置API密钥,导致query()失败;4. 分页偏移量理解错误,偏移量按时间顺序从最旧开始;5. 标签是单一值,不能存储多个标签。排查方法:检查SDK版本,确认cwd路径正确,验证API密钥有效,打印list_sessions结果调试,阅读官方文档。生产环境优化:使用索引数据库(如SQLite)缓存会话元数据,避免频繁文件扫描;对大型会话使用流式读取;实现会话归档策略,定期清理旧文件;考虑跨设备同步,通过云存储或后端数据库。

企业应用扩展

本教程的会话管理功能可轻松集成到各种UI框架中,如FastAPI后端或Electron桌面应用。对于跨主机会话,可同步转录文件或将会话元数据存储到自己的数据库。TypeScript开发者可使用@anthropic-ai/claude-agent-sdk中的等价API(驼峰命名)。此外,可扩展功能包括:实现会话搜索、导出会话为其他格式、添加会话对比功能、集成用户认证和权限控制。更广阔的视角是,本教程是构建代理产品系列的一部分,其他笔记本(00-03)覆盖了代理本身的构建,而本教程专注于管理代理产生的数据,两者结合可构建完整的AI应用。

总结

本教程面向有编程基础的开发者,深入讲解如何利用Claude Agent SDK的本地会话管理功能,构建类似Claude Code Desktop的会话侧边栏。内容涵盖会话的创建、列出、读取、重命名、标签过滤、分叉与恢复,以及删除等核心操作。通过实际代码示例,展示如何高效管理代理产生的JSONL转录文件,并探讨了软删除、分页、跨主机会话等高级话题,帮助开发者掌握构建AI产品用户界面的关键工程实践。

内容来源与验证

继续系统学习

站内还有更多完整实战教程

加入会员,解锁全部教程、完整代码与持续更新内容。

加入会员
查看全部API 与工程实践教程