会话日志与投影机制¶
"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 提供了两种官方持久化后端,均作为独立插件加载:
session-persistence-sqlite:- 采用 SQLite 数据库存储事件流;
- 采用单调递增的
SCHEMA_VERSION进行数据库版本迁移; - 支持多 Agent 会话的毫秒级检索、索引搜索与按轮次切片。
session-persistence-jsonl:- 每个 Session 对应一个纯文本
.jsonl文件; - 极简且对 Git 友好,适合单机开发与无依赖环境。
本章思考与自测¶
- 思考题:在
deriveMessages()投影过程中,为什么assistant/chunk事件会被忽略,而只处理assistant/message?这种设计对流式渲染和历史持久化有什么好处? - 自测题:当需要向模型注入一段全新的上下文(例如当前系统时间)时,正确的做法是直接修改
deriveMessages()函数,还是在会话流中追加一个新的SessionEvent?为什么?