Event-Sourced Session:AI Agent 的“会话即事件流“设计 DeepSeek Harness 系列第四篇。本文分析 Harness 为什么把 Agent 会话设计为 append-only 事件流而非大多数框架采用的 mutable message list。问题mutable state 的脆弱性主流 Agent 框架如何管理会话# LangChain 风格messages[]messages.append(HumanMessage(hello))responsellm.invoke(messages)messages.append(response)# messages 是可变数组任何代码都能插入/修改/删除这种方式的问题状态和日志可以分离内存中的messages和落盘的日志是两个独立数据需要手动同步replay 不可靠如果中间经过 compaction压缩、fork分叉重放得到的状态可能和当时不一致并发不安全多个插件同时操作messages需要显式锁遥测是事后补丁trace/logging 是额外加上去的而非结构性保证Harness 的解法来自一个经典后端模式Event Sourcing。核心设计Session Append-only Event Log// 不是这样interfaceSession{messages:Message[]// mutable}// 而是这样interfaceSession{events:readonlySessionEvent[]// append-only, deep-frozenappend(type,data):SessionEvent// 唯一写入路径deriveMessages():Message[]// 从事件流投射}一个Session是一个只能追加的类型化事件流——单一事实源。模型看到的 message history 是从这个流派生derive出来的而非独立存储。这是架构决策笔记中的原话A mutable message array with events fired as notifications — simpler, but state and log can diverge; with event-sourcing the log IS the state, so divergence is structurally impossible.事件词汇表SessionEventMap是 merge-extensible 的 TypeScript 接口interfaceSessionEventMap{turn/start:{turn:number}turn/end:{turn:number;reason:TurnEndReason}step/start:{turn:number;step:number}step/end:{turn:number;step:number}user/message:UserMessageassistant/chunk:{turn:number;step:number;chunk:StreamChunk}assistant/message:{turn:number;step:number;message:AssistantMessage;usage?:TokenUsage}tool/call:{turn:number;step:number;callId:CallId;name:string;arguments:string}tool/result:{turn:number;step:number;message:ToolResultMessage;error?;meta?}request/header:{header:EpochHeader;reason:RequestHeaderReason}request/context:RequestContexttodo/write:{todos:TodoItem[]}session/end-seed:Recordstring,never// ...plugins 可以通过 declaration merging 追加新事件类型}每个事件携带typediscriminated union 的 tagseq单调递增序列号 log.lengthtimeepoch 毫秒data事件载荷ignorable?标记这个事件是否可以被不认识它的 reader 跳过关键约束事件一旦 append就是 deep-frozen、不可变的。Session.append()做一次递归 JSON 校验 深拷贝 freeze之后没有任何代码路径可以修改已记录的历史。三种事件类型类别事件作用Surface表面user/message,assistant/message,tool/result产生模型可见 messageStructural结构turn/*,step/*,assistant/chunk标记边界、保留 replay 精度Log-only仅日志request/header,request/context,todo/write记录运行时状态不投射为 message只有Surface事件参与deriveMessages()投射。其他事件虽然在日志中但模型永远看不到。派生机制deriveMessages()// 投射规则简化functionderiveEventMessage(event:SessionEvent):Message|null{switch(event.type){caseuser/message:return{role:user,content:event.data.content}caseassistant/message:// 空内容的 assistant message 被跳过max-tokens 截断时的占位if(isEmpty(event.data.message.content))returnnullreturn{role:assistant,content:event.data.message.content}casetool/result:return{role:user,content:[toolResultBlock(event.data)]}default:returnnull// 非 surface 事件不产生 message}}deriveMessages()的特性缓存每个 surface node 只投射一次后续调用 O(new nodes)Frozen返回的Message[]引用是新的但内部 Message 对象是共享的 frozen 值一致性因为从同一份不可变日志投射不可能出现两个消费者看到不一样的历史Surface 与 Compaction当上下文太长需要压缩时压缩不是修改原始事件——而是追加一个新的 surface 事件带有replace操作typeSurfaceOp|append// 正常追加|{op:replace;start:number;end:number}// 替换一段表面// 压缩结果是一个新的 assistant/messagesurfaceOp { op: replace, start: 5, end: 42 }// 它取代了 seq 5~42 的 surface 节点但原始事件仍然在日志中这意味着压缩是可审计的——你可以看到是哪次压缩替换了哪些节点原始数据永不丢失——UI 回放、遥测分析仍然可以读到全部历史模型只看到压缩后的 surface——deriveMessages()自动跳过被 replace 的节点“模型可见 ⟺ 已记录” 不变式这是 Harness 最硬的一条运行时约束任何到达模型请求的内容必须能从会话日志重建。实际代码中有 runtime invariant 断言这一点。如果你写了一个插件想给模型注入内容你必须先 append 一个 session event——不能绕过日志直接塞进 message list。// 正确通过 agent.inject() 注入它会 append 一个 user/message 事件agent.inject({content:workspace has changed,source:{kind:context}})// 错误直接修改 message 数组在 Harness 中不可能因为 deriveMessages 是纯投射session.messages.push(...)// 不存在这个 API这条不变式带来的保证场景传统框架HarnessResume恢复会话从文件加载 messages希望和当时一样从事件流重新 derive结构性一致Fork分叉会话深拷贝 messagesseed 原日志前缀新 session 从中 deriveReplay回放额外的 trace 系统日志本身就是完整 replay 源Telemetry另一套数据采集session/event 广播就是遥测源调试猜测当时模型看到了什么100% 确定因为日志就是它看到的持久化是插件关注点Session 本身是纯内存的——它不关心怎么落盘。持久化是独立的 Capability Seamdsh-session (Service Definition) — 内存 event log dsh-session-persistence (Service Definition) — 持久化接口 dsh-session-jsonl (Provider) — JSONL Zstandard 压缩 dsh-session-sqlite (Provider) — SQLite 后端持久化插件通过session/event同步通知异步缓冲写入在 turn 结束时的session/flushcheckpoint 保证已落盘。关键设计append 是同步的热路径不阻塞 I/O持久化插件在后台 write-behind。与 LangChain Memory 的对比维度LangChain MemoryHarness Session数据模型Mutable message listAppend-only event log压缩修改 messages in-place追加 replace surface event持久化需要手动 save/load插件自动 write-behindFork深拷贝seed 前缀Replay需要额外系统结构性保证类型安全运行时编译时discriminated union可扩展子类覆写declaration merging 追加事件类型并发需要锁append 是唯一写入路径不需要锁request/header让每个请求可重建除了 message history模型请求还包含 system prompt、tool schemas、call config。这些也被事件化request/header:{header:{config:LlmCallConfig// model, temperature, max_tokens...system?:string// rendered system prompttools?:ToolSchema[]// assembled tool schemas}reason:initial|resume|change}每次请求前如果 header 发生变化就追加一个request/header事件。这意味着从任意一段日志前缀可以完整重建那次请求的完整 payload——包括当时的 system prompt 是什么、tool 列表是什么、用的什么模型。实际效果Session Fork// Fork 用原 session 的事件日志前缀作为种子创建新 sessionconstchildctx.sessions.fork(parentSession,boundarySeq)// child.events[0..boundarySeq] 来自 parent// child 之后的 append 不影响 parentCompaction上下文压缩// Compaction 不删除事件——它追加一个 summary 事件取代一段 surfacesession.append(assistant/message,summaryData,{surfaceOp:{op:replace,start:oldStart,end:oldEnd},sourceEventSeqs:[oldStart,...,oldEnd],// 记录哪些事件被替换})// deriveMessages() 之后只看到 summary但原始事件仍在日志中Telemetry 采集// 任何插件都可以监听 session/event 做实时遥测ctx.on(session/event,(session,event){if(event.typeassistant/messageevent.data.usage){metrics.recordTokenUsage(event.data.usage)}})工程代价Event Sourcing 不是免费的派生开销随日志增长deriveMessages()缓存缓解但长 session 仍有 O(n) 的首次投射所有新的模型可见输入都需要事件化想给模型注入一个新类型的内容先设计SessionEventMap的新成员事件格式变更是破坏性的SESSION_FORMAT_VERSION目前为 0格式变更拒绝旧日志调试时要理解事件 → 投射的间接性不是直接看 messages要先看 events 再理解投射规则但对于一个 Agent 运行时来说replay 可靠性和审计能力的价值远超这些代价。参考链接Session 子系统文档持久化文档Agent 生命周期DeepSeek Harness 系列文章第四篇Event-Sourced Session本文

相关新闻

最新新闻

把碧蓝航线的每日重复劳动交给托管脚本,一个月能省下多少时间

把碧蓝航线的每日重复劳动交给托管脚本,一个月能省下多少时间

把碧蓝航线的每日重复劳动交给托管脚本,一个月能省下多少时间 【免费下载链接】AzurLaneAutoScript Azur Lane bot (CN/EN/JP/TW) 碧蓝航线脚本 | 无缝委托科研,全自动大世界 项目地址: https://gitcode.com/gh_mirrors/az/AzurLaneAutoScript 碧…

2026/8/15 9:37:35
猫抓浏览器扩展:网页媒体资源嗅探与M3U8流媒体下载,从此告别找不到源文件

猫抓浏览器扩展:网页媒体资源嗅探与M3U8流媒体下载,从此告别找不到源文件

猫抓浏览器扩展:网页媒体资源嗅探与M3U8流媒体下载,从此告别找不到源文件 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 深…

2026/8/15 9:37:35
销售一天解释10次的问题,可能就是最值得做AI的场景

销售一天解释10次的问题,可能就是最值得做AI的场景

你的团队是不是也这样?开会时老板拍板“我们要上智能客服”,技术团队忙活三个月,最后销售该咋干还咋干,客户该问啥还问啥。其实真正的AI切入点,根本不在所谓的智能客服、智能助手里。而是藏在销售每天被客户追问10遍的…

2026/8/15 9:37:35
游戏AI智能教练:基于模仿学习与实时推荐的战术辅助系统

游戏AI智能教练:基于模仿学习与实时推荐的战术辅助系统

1. 项目概述:当游戏AI学会“抄作业”最近在游戏圈和专利圈里,一个关于“吃鸡”游戏的专利方案引起了我的注意。简单来说,这个专利的核心思路是:让AI去“观摩”那些历史吃鸡大神们的录像,学习他们的打法套路&#xff0c…

2026/8/15 9:37:35
BetterJoy使用指南:三步把Switch手柄接入PC,畅玩Steam与模拟器

BetterJoy使用指南:三步把Switch手柄接入PC,畅玩Steam与模拟器

BetterJoy使用指南:三步把Switch手柄接入PC,畅玩Steam与模拟器 【免费下载链接】BetterJoy Allows the Nintendo Switch Pro Controller, Joycons and SNES controller to be used with CEMU, Citra, Dolphin, Yuzu and as generic XInput 项目地址: h…

2026/8/15 9:37:35
PyCharm Community 2023.3 保姆级安装与配置指南

PyCharm Community 2023.3 保姆级安装与配置指南

1. 项目概述:为什么PyCharm Community是Python开发者的首选如果你刚开始接触Python,或者正在寻找一款免费、强大且不折腾的集成开发环境,那么PyCharm Community Edition 2023.3绝对是你绕不开的选择。我用了PyCharm好几年,从学生时…

2026/8/15 9:32:35