claw-code 源码分析:Transcript / Session Store——智能体「运行史」数据结构怎样才算可运维?

涉及源码src/transcript.pysrc/session_store.pysrc/query_engine.pysrc/main.py;与 result/03.mdresult/05.md 中的会话/拒绝语义相连。


1. 「运行史」在运维眼里要解决什么

运维(含 SRE、平台、合规、客服排障)关心的不是「有一段聊天」,而是能否在 不重现当时模型 的前提下回答:

  • 谁在什么时间、以什么身份、在什么环境 触发了会话?
  • 每一轮 用户输入、助手输出、工具调用请求/结果、拒绝原因 是否可顺序重放
  • 用量与计费 是否与某次请求可对账?
  • 失败/越权 能否按 session_id / trace_id 跨服务关联
  • 数据保留、脱敏、导出、删除 是否有稳定 schema 与生命周期?

下面先还原 claw-code 当前 的 Transcript 与 Session Store 设计,再对照上述问题给出 可运维 数据结构应具备的要素。


2. 当前实现:TranscriptStore(进程内)

python 复制代码
# 6:23:src/transcript.py
@dataclass
class TranscriptStore:
    entries: list[str] = field(default_factory=list)
    flushed: bool = False

    def append(self, entry: str) -> None:
        self.entries.append(entry)
        self.flushed = False

    def compact(self, keep_last: int = 10) -> None:
        if len(self.entries) > keep_last:
            self.entries[:] = self.entries[-keep_last:]

    def replay(self) -> tuple[str, ...]:
        return tuple(self.entries)

    def flush(self) -> None:
        self.flushed = True

语义

  • entries仅追加字符串 ;在本仓库中与 QueryEnginePort.mutable_messages 在成功路径上 同步追加同一 prompt (见下文 submit_message)。
  • flushed布尔标记append 置假,flush 置真;不表示「已 fsync 到对象存储」 ,只表示调用过 flush_transcript()
  • compact尾部截断 ,不是语义摘要;与 mutable_messages 共用 compact_after_turns 阈值。

学习点(移植期合理) :极小 API,便于单测与教学。
运维缺口 :无 角色 (user/assistant/tool)、无 时间戳 、无 id 、无 版本 ;无法区分「用户原话」与「系统注入」;截断后 不可恢复 早期轮次。


3. 当前实现:StoredSession 与磁盘 JSON

python 复制代码
# 8:35:src/session_store.py
@dataclass(frozen=True)
class StoredSession:
    session_id: str
    messages: tuple[str, ...]
    input_tokens: int
    output_tokens: int


DEFAULT_SESSION_DIR = Path('.port_sessions')


def save_session(session: StoredSession, directory: Path | None = None) -> Path:
    target_dir = directory or DEFAULT_SESSION_DIR
    target_dir.mkdir(parents=True, exist_ok=True)
    path = target_dir / f'{session.session_id}.json'
    path.write_text(json.dumps(asdict(session), indent=2))
    return path


def load_session(session_id: str, directory: Path | None = None) -> StoredSession:
    target_dir = directory or DEFAULT_SESSION_DIR
    data = json.loads((target_dir / f'{session_id}.json').read_text())
    return StoredSession(
        session_id=data['session_id'],
        messages=tuple(data['messages']),
        input_tokens=data['input_tokens'],
        output_tokens=data['output_tokens'],
    )

持久化触发链QueryEnginePort.persist_session()flush_transcript()save_session(...)

python 复制代码
140:150:src/query_engine.py
    def persist_session(self) -> str:
        self.flush_transcript()
        path = save_session(
            StoredSession(
                session_id=self.session_id,
                messages=tuple(self.mutable_messages),
                input_tokens=self.total_usage.input_tokens,
                output_tokens=self.total_usage.output_tokens,
            )
        )
        return str(path)

hydratefrom_saved_sessionmessages 填回 mutable_messagesTranscriptStore.entries,并标记 flushed=True

运维相关隐患

  1. DEFAULT_SESSION_DIR 为相对路径 .port_sessions:依赖 进程当前工作目录 ,不同服务启动方式会导致会话文件散落或「找不到」,生产应 显式传入绝对路径或配置save_session 已支持 directory,但 persist_session / load_session 未暴露给 CLI 以外的配置层)。
  2. 整文件覆盖写 :无 WAL、无并发锁;多进程写同一 session_id 会互相覆盖。
  3. 无 schema 版本字段:演进字段时难以做迁移。
  4. 敏感内容messages 若为真实用户 prompt,落盘即 PII/密钥风险,需加密、脱敏或存引用 ID 而非原文------当前无任何封装。

4. 与 QueryEnginePort 的状态关系:双缓冲与一致性

成功处理一轮时:

python 复制代码
# 91:95:src/query_engine.py
        self.mutable_messages.append(prompt)
        self.transcript_store.append(prompt)
        self.permission_denials.extend(denied_tools)
        self.total_usage = projected_usage
        self.compact_messages_if_needed()
python 复制代码
129:132:src/query_engine.py
    def compact_messages_if_needed(self) -> None:
        if len(self.mutable_messages) > self.config.compact_after_turns:
            self.mutable_messages[:] = self.mutable_messages[-self.config.compact_after_turns :]
        self.transcript_store.compact(self.config.compact_after_turns)

现状mutable_messagestranscript_store.entries 内容同步截断 ;持久化只写 mutable_messages。设计上两者在内存中应 始终一致 (否则 replay_user_messages 与落盘会分叉------当前实现靠同一套 append/compact 维持)。

未进入运行史的数据(对运维很重要):

  • 每轮 TurnResult.output(助手侧摘要/JSON);
  • matched_commands / matched_tools / permission_denials 明细
  • stop_reason
  • 工具调用的请求体与返回(成功或失败)。

因此磁盘上的 JSON 不是完整对话录 ,而是 「用户轮次文本 + 伪 token 累计」 的瘦身快照,适合移植演示,不足以单独支撑事故复盘。


5. 流式事件与运行史:协议 vs 持久化

stream_submit_message 在内存中 yield 结构化事件(含 transcript_size),但 这些事件默认不落盘

python 复制代码
# 122:127:src/query_engine.py
        yield {
            'type': 'message_stop',
            'usage': {'input_tokens': result.usage.input_tokens, 'output_tokens': result.usage.output_tokens},
            'stop_reason': result.stop_reason,
            'transcript_size': len(self.transcript_store.entries),
        }

可运维做法 :若产品前端消费 SSE,运维侧应把 同一事件流 (或等价日志)追加写入 只增不改的 event log (文件/队列/OLAP),而不是仅依赖最终 StoredSession 快照。


6. 怎样才算「可运维」的数据结构------分层建议

6.1 事件日志(append-only,真相源)

每条记录建议至少包含:

字段 作用
event_id / 单调序号 有序、可去重
session_id 会话聚合
trace_id 跨服务
ts (RFC3339) 时间线、与指标对齐
type user_message / assistant_message / tool_call / tool_result / permission_denial / error ...
payload 结构化 JSON(可引用 blob 存大体裁)
schema_version 迁移

claw-code 的 TurnResult 与流式 dict 是 向该形态过渡的中间层

6.2 会话快照(derived,可丢可重建)

StoredSession 类角色适合作为 从 event log 物化的缓存 (用于快速 hydrate),而不是 唯一数据源 ;字段可含 last_event_idchecksumupdated_at

6.3 角色与多说话者

智能体运行史至少 user / assistant / tool 三类;当前仅存 user 侧 prompt 字符串。可运维模型常用 统一 MessageTurn ,内含 rolecontenttool_calls[]

6.4 工具与审计

每次 tool 调用应有 call_id ,与 PermissionDenial、计费、下游日志 同键 (参见 result/05.md)。拒绝、超时、部分成功都应是 独立事件,而非挤在一条字符串里。

6.5 用量与对账

当前 input_tokens/output_tokens词数近似UsageSummary.add_turn),运维对账需 API 返回的真实 usage自研 tokenizer ;并记录 model_id、pricing_version

6.6 生命周期与合规

  • Retention:按租户策略 TTL 或法务要求删除。
  • PII:字段级加密、密钥轮转、访问审计。
  • 导出:GDPR 等「可携带」需稳定 schema。

6.7 运维可操作性

  • 可观测性transcript_size、落盘路径、flush 失败率、JSON 写延迟指标。
  • 配置:session 目录、是否同步 fsync、单文件大小上限、轮转策略。
  • 调试session_id 一键拉全链 event(当前仅一条 json 文件)。

7. CLI 与测试提供的「最小闭环」

python 复制代码
160:169:src/main.py
    if args.command == 'flush-transcript':
        engine = QueryEnginePort.from_workspace()
        engine.submit_message(args.prompt)
        path = engine.persist_session()
        print(path)
        print(f'flushed={engine.transcript_store.flushed}')
        return 0
    if args.command == 'load-session':
        session = load_session(args.session_id)
        print(f'{session.session_id}\n{len(session.messages)} messages\nin={session.input_tokens} out={session.output_tokens}')
        return 0

test_flush_transcript_cli_runs 断言 flushed=Truetest_load_session_cli_runsbootstrap_session 生成文件再 load-session。这是 可回归 的薄层,但 断言内容未覆盖 消息语义完整性或并发。


8. 小结

维度 claw-code 现状 可运维目标
Transcript 字符串列表 + flushed + 截断 带角色/时间/id 的事件序列或 append-only log
Session 落盘 单 JSON,用户消息 + 伪用量 版本化快照或 log 物化;含助手/工具/拒绝
路径 相对 .port_sessions 配置化绝对路径、隔离租户
并发 覆盖写 单写者或乐观锁 / 外部 store
合规 无内建 保留、加密、脱敏、导出

一句话 :当前 TranscriptStore + StoredSession 把「会话能续上、能演示 persist」跑通了;要 可运维 ,需把运行史从 「几条字符串」 升级为 「可关联、可重放、可审计、可生命周期管理的事件模型」 ,并让磁盘格式成为该模型的 投影 而非全部真相。


相关推荐
kdxiaojie3 分钟前
Linux 驱动研究 —— SDIO (1)
linux·运维·笔记·学习·sdio
码完就睡14 分钟前
数据结构——树、二叉树基础概念
数据结构·算法
码哥字节18 分钟前
Claude Code 给我打暗标后,我把团队迁到了 Qoder CLI
claude code·ai编程工具·qoder cli
如何原谅奋力过但无声20 分钟前
现代化Python工具:uv、Ruff、Pyright、Rich(只讲重点版)
vscode·python·uv
Logintern0921 分钟前
DeepAgents 主-子 Agent 的底层实现逻辑完全基于 LangGraph 的图机制
python
ZzzZZzzzZZZzzzz…21 分钟前
K8S实战:NFS+StorageClass+PV/PVC+Deployment
运维·云原生·容器·kubernetes·storageclass·linux运维·持久卷
BerrySen17828 分钟前
一个 JDBC 参数引发的架构演进:Apache SeaTunnel 如何解决数据同步中的“定时 Flush”难题
运维·自动化·zabbix
神奇霸王龙1 小时前
AgentDesk 配置 Codex+ selltoken 中转 API 实测教程(2026 年 8 月更新)
人工智能·ai·ai编程·策略模式·codex·agentdesk
码云骑士1 小时前
97-Milvus从零到生产-Standalone-vs-Cluster-索引选择-分片监控扩容
python·milvus
草莓熊Lotso1 小时前
【Linux网络】从0手写Reactor反应堆(二):完善核心细节——ET非阻塞读写、分层架构与回调机制
linux·运维·服务器·网络·c++·tcp/ip·架构