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」跑通了;要 可运维 ,需把运行史从 「几条字符串」 升级为 「可关联、可重放、可审计、可生命周期管理的事件模型」 ,并让磁盘格式成为该模型的 投影 而非全部真相。


相关推荐
极客老王说Agent10 小时前
2026实战指南:如何用智能体实现药品不良反应报告的自动录入?
人工智能·ai·chatgpt
九皇叔叔10 小时前
Ubuntu 22.04 版本常用设置
linux·运维·ubuntu
lulu121654407811 小时前
Claude Code项目大了响应慢怎么办?Subagents、Agent Teams、Git Worktree、工作流编排四种方案深度解析
java·人工智能·python·ai编程
kishu_iOS&AI11 小时前
Openclaw -> Hermes —— 初体验
ai·openclaw·hermes
小辉同志11 小时前
215. 数组中的第K个最大元素
数据结构·算法·leetcode··快速选择
Ares-Wang11 小时前
Flask》》 Flask-Bcrypt 哈希加密
后端·python·flask
老星*11 小时前
AI选股核心设计思路
java·ai·开源·软件开发
kongba00711 小时前
项目打包 Python Flask 项目发布与打包专家 提示词V1.0
开发语言·python·flask
kyriewen1112 小时前
智能体走向“企业操作系统”,Google 扔出五把钥匙
科技·ai·googlecloud
杨云龙UP12 小时前
ODA登录ODA Web管理界面时提示Password Expired的处理方法_20260423
linux·运维·服务器·数据库·oracle