跳转至

会话日志与投影机制

"What is visible to the model is logged." —— 模型可见即已记录。 这是 DSH 最核心的架构不变量(Invariant)。

很多 Agent 项目在开发后期都会遇到"幽灵 Bug":由于在内存中随意拼接 Prompt,导致导出的对话历史与大模型当时看到的上下文不一致,无法复现问题。DSH 采用纯粹的 事件溯源(Event Sourcing)投影函数(Projection) 彻底解决了这个痛点。


核心文件速查

路径 职责 重要度
packages/core/session/src/surface.ts deriveMessages() 核心投影算法 ⭐⭐⭐
packages/core/session/src/request-header.ts 模型请求头规范化与折叠 ⭐⭐⭐
packages/session/session-persistence-sqlite/ SQLite 追加存储与索引引擎 ⭐⭐⭐
packages/session/session-persistence-jsonl/ JSONL 轻量文件持久化 ⭐⭐

核心不变量:模型可见即已记录

DSH 在运行时执行一个强断言:抵达模型 API 的每一个字、每一个工具返回结果,都必须能够从 SessionEvent 日志流中 100% 确定性地重建出来。

  Session Log (Append-Only Events)
┌────────────────────────────────────────────────────────────┐
│ [001] turn/start        (turn_1)                           │
│ [002] step/start        (step_1)                           │
│ [003] user/message      "请检查 package.json"              │
│ [004] assistant/chunk   "好的,我"                          │
│ [005] assistant/chunk   "马上读取..."                      │
│ [006] assistant/message "好的,我马上读取..." + tool_call   │
│ [007] tool/call         read { file_path: "package.json" } │
│ [008] tool/result       { "name": "dsh", "version": "0.1" }│
│ [009] step/end          (step_1)                           │
└─────────────────────────────┬──────────────────────────────┘
                              │
                    deriveMessages() 确定性投影
                              │
                              ▼
  Standard LLM Messages Array (模型实际接收的上下文)
┌────────────────────────────────────────────────────────────┐
│ { role: 'user', content: '请检查 package.json' }            │
│ { role: 'assistant', content: '好的,我马上读取...',        │
│   tool_calls: [{ name: 'read', args: {...} }] }            │
│ { role: 'tool', tool_call_id: 'call_1', content: '{...}' } │
└────────────────────────────────────────────────────────────┘

核心算法精读:deriveMessages()

让我们看 packages/core/session/src/surface.ts 中的投影实现原理:

// packages/core/session/src/surface.ts
export function deriveMessages(events: readonly SessionEvent[]): Message[] {
  const messages: Message[] = []

  for (const event of events) {
    switch (event.type) {
      case 'user/message':
        // 将用户事件折叠为 LLM User Message
        messages.push({
          role: 'user',
          content: event.content,
        })
        break

      case 'assistant/message':
        // 将助手事件折叠为 LLM Assistant Message(含工具调用请求)
        messages.push({
          role: 'assistant',
          content: event.content,
          toolCalls: event.toolCalls,
        })
        break

      case 'tool/result':
        // 将工具返回折叠为 LLM Tool Message
        messages.push({
          role: 'tool',
          callId: event.callId,
          content: formatToolResult(event.result, event.error),
        })
        break

      case 'session/compacted':
        // 上下文压缩事件:折叠前面的历史,只保留摘要
        messages.splice(0, event.prunedCount, {
          role: 'system',
          content: `[Previous conversation summarized: ${event.summary}]`,
        })
        break
    }
  }

  return messages
}

存储引擎:SQLite vs JSONL

DSH 提供了两种官方持久化后端,均作为独立插件加载:

  1. session-persistence-sqlite
  2. 采用 SQLite 数据库存储事件流;
  3. 采用单调递增的 SCHEMA_VERSION 进行数据库版本迁移;
  4. 支持多 Agent 会话的毫秒级检索、索引搜索与按轮次切片。
  5. session-persistence-jsonl
  6. 每个 Session 对应一个纯文本 .jsonl 文件;
  7. 极简且对 Git 友好,适合单机开发与无依赖环境。

本章思考与自测

  1. 思考题:在 deriveMessages() 投影过程中,为什么 assistant/chunk 事件会被忽略,而只处理 assistant/message?这种设计对流式渲染和历史持久化有什么好处?
  2. 自测题:当需要向模型注入一段全新的上下文(例如当前系统时间)时,正确的做法是直接修改 deriveMessages() 函数,还是在会话流中追加一个新的 SessionEvent?为什么?