DSH plugin 会话日志怎么扩展?DeepSeek Harness 的 SessionEventMap 与投影
DSH plugin 扩展会话日志,不是去改一份存储的消息数组,而是往一份仅追加的事件日志里写入类型化事件,再让消息历史从日志派生(来源)。 本文面向 DSH插件 / DeepSeek插件 作者,讲清「日志是唯一真源」这条底座、如何用声明合并往 SessionEventMap 加事件与 ignorable 的拒绝语义,以及插件拥有的消息投影与 ctx.sessionProjections 的读写契约;会话存在哪、怎么查的原理见 会话持久化解析,事件分发机制见 插件事件,这里只讲开发者要实现的规范。
DSH plugin 会话日志的唯一真源与不变量
Session 是一份由类型化 SessionEvent 组成的仅追加日志,是 agent 完整交互历史的唯一真源;LLM 消息历史从日志派生而来,从不单独存储(来源)。 三条不变量决定了你能怎么写:
- 序号连续 —
seq是日志中的单调递增位置,遵循seq = log.length的连续性契约;time为 epoch 毫秒。SessionSeq/SessionLogOffset是只加编译期品牌的类型,只接纳非负安全整数并拒绝负零,算术结果须按预期角色重新接纳。 - 数据必须是无损 JSON —
Session.append用isJsonValue运行时校验所有事件数据,拒绝BigInt、函数、symbol、undefined、负零、非有限数、循环引用、稀疏数组以及Map/Set/Date/类实例等奇异对象。所以非可序列化的meta会在源头被拒,而不是在后端刷盘时才炸。 - append 返回的是日志快照 —
append(type, data, opts)返回已记录事件(分配好的seq/time加上进入日志的data快照),读回event.data看到的是已记录值,绝不是调用方仍在改的输入。
写坏的事件在 append 处就失败,因为事件日志是持久真源。追加一旦进入日志即视为提交:观察者失败会被逐监听器记录并隔离,不会改变返回值。日志如何落盘(持久化 seam、后端、崩溃恢复)是 persistence 的关注点。
DSH plugin 扩展 SessionEventMap:声明合并与 ignorable
插件通过声明合并(declaration merging)往 SessionEventMap 声明额外事件类型;SessionEventType = keyof SessionEventMap,因此合并进来的类型会自动成为合法的事件类型(来源)。 两个官方先例:
- 压缩 seam 添加了
compaction/start/compaction/summary/compaction/end。 @deepseek-ai/dsh-hook-protocol为钩子桥接添加了仅记录日志的hook/invoked/hook/result,两者通过handlerId关联。
以上都是仅日志事件:不是 SurfaceEventType(不携带 surfaceOp,不参与派生历史)。事件所有方自己决定它们属于一个开放的执行轮次,还是可以独立位于轮次之间,并在自己的不变量配套插件中强制所需关系。如果同一个事件族要组装成一个 Conversation 节点,则该族的每条 start、update、result、resource 或 interruption 事件都必须携带或独立推导出同一个稳定业务 id——这样客户端无须靠相邻关系猜归属(节点装配见 会话节点)。
SessionEvent<T> 是基于 type 的真正可辨识联合(而非独立的 type/data 联合),所以 switch (event.type) 能直接收窄 event.data、无需类型断言。其中 ignorable 的语义值得单独记牢(来源):
ignorable: true表示读者可安全跳过一个它不认识的type。- 缺省表示必需:读者遇到不认识且没有该标记的事件,必须拒绝重建会话,而不是静默丢弃——因为一个未识别的必需事件可能改变后续日志的解释方式。
- 写方只在纯信息记录上设
true;默认必需意味着「忘记标记会过度拒绝」(小麻烦),而非「静默恢复一个被掏空的会话」(大事故)。
DSH plugin 消息投影与 ctx.sessionProjections
会修改既有消息内容的插件,用 @messageProjection 标记事件声明,并通过 ctx.sessions.registerMessageProjection() 注册一个纯处理器(来源)。 SessionMessageProjection 声明它解释的 type,并实现 project(event, context) 返回以原始事件 seq 为键的不可变消息映射;其契约是:校验完整持久决策后再返回任何更新,保留消息身份、发布不可变副本且不修改输入,无法应用时抛错。
Session 在提交前用处理器校验完整决策、应用不可变消息更新并推进 contentGeneration。缺少处理器时拒绝操作(包括恢复与独立折叠),卸载已经使用的处理器后也会拒绝读取缓存。独立读取器把处理器显式传给 foldSurface(events, projections),并通过 deriveEventMessage() 应用 projectedMessages,从而与缓存永不产生分歧。
Session.surface 返回会话稳定的只读 SessionSurface 视图,暴露 nodes、replaceGeneration(位置替换计数)与 contentGeneration(替换加插件拥有消息变更的计数),调用方可以观察成员关系与替换代次,但不能调用校验。deriveMessages() 沿着 surfaceOp 维护的有序序列走,把日志派生成模型看到的 Message[]——它是缓存的(纯尾部增长 O(新节点),替换或投影触发重建)且冻结的(每次返回新数组,引用共享的深冻结消息)。
架构层还有一条独立的投影 seam:dsh-session-projection 提供 ctx.sessionProjections,已注册单元增量折叠已提交事件,host 消费方通过 stateOf() 读取单个类型化状态,载体通过 snapshot() 批量取得裁剪后的客户端视图(来源)。两条硬性约定:
- host 读取方要么在激活时要求该服务,要么在注册表或必需 key 缺席时明确失败——不许猜。
- 贡献方可以保留
ctx.inject(['sessionProjections'], ...)注册,但不能为缺失的 host 值静默提供默认值(agent loop 会为读取方注册共享的turnBoundary状态)。
写完自检三项:新增事件是否确定了它是仅日志还是会投影为消息;ignorable 是否只标在纯信息记录上;消息投影处理器是否保证纯函数与不可变返回。想找会话类插件的社区实现,你可以在 DSH Plugin Hub 检索;扩展点归属判据见 功能→机制映射。
常见问题
DSH plugin 通过 TypeScript 声明合并(declaration merging)扩展 SessionEventMap:在插件里把新的键加进 SessionEventMap 接口,再调用 session.append(type, data) 写入。这类事件默认是仅日志事件——不是 SurfaceEventType,不携带 surfaceOp、也不参与派生历史;压缩 seam 的 compaction/start、compaction/summary、compaction/end 与 dsh-hook-protocol 的 hook/invoked、hook/result 都是这样加的。
DSH plugin 在 SessionEvent 上设置 ignorable: true 表示读者在不认识该 type 时可以安全跳过;缺省则表示必需。遇到不认识且没有该标记的事件时,读者必须拒绝重建会话而不是静默丢弃,因为一个未识别的必需事件可能改变后续日志的解释方式。写方只在纯信息记录上设 true,默认必需意味着忘记标记会过度拒绝,而不是静默恢复一个被掏空的会话。
DSH plugin 中只有 SurfaceEventType 子集会投影为模型消息,即 system/message、user/message、assistant/message、tool/result;只有这些变体携带 surfaceOp 与 sourceEventSeqs。turn/*、step/*、assistant/attempt 以及插件所属的仅日志事件都是结构信息,不会投影为消息。Session.deriveMessages() 正是沿着 surfaceOp 维护的有序序列走,把日志派生成模型看到的 Message[]。
DSH plugin 用 @messageProjection 标记事件声明,并通过 ctx.sessions.registerMessageProjection() 注册一个纯处理器 SessionMessageProjection:它声明 type 并实现 project(event, context) 返回以原始事件 seq 为键的不可变消息映射。Session 在提交前用处理器校验完整决策、应用不可变更新并推进 contentGeneration;缺少处理器时拒绝操作(包括恢复与独立折叠),卸载已使用的处理器后也会拒绝读取缓存。
DSH plugin 的 ctx.sessionProjections 由 dsh-session-projection 提供:已注册单元增量折叠已提交事件,host 消费方通过 stateOf() 读取单个类型化状态,载体通过 snapshot() 批量取得裁剪后的客户端视图。host 读取方要么在激活时要求该服务,要么在注册表或必需 key 缺席时明确失败;贡献方可以保留 ctx.inject(['sessionProjections'], ...) 注册,但不能为缺失的 host 值静默提供默认值。
相关术语
- SessionEventMap(会话事件词表)
- SessionEventMap 是 DeepSeek Harness 中仅追加事件类型的词表,可通过声明合并扩展,收录 turn/start、turn/end、step/start、step/end、user/message、developer/message、system/message、assistant/message、assistant/attempt、tool/call、tool/result、request/header、request/context、session/end-seed 等成员;Session 的完整交互历史以它为准。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/session
- SurfaceEventType
- SurfaceEventType 是事件类型中产生消息的子集——system/message、user/message、assistant/message、tool/result;只有这些变体在 SessionEvent 上携带 surfaceOp 与 sourceEventSeqs,是派生模型历史的唯一来源,其余类型不携带 surface 元数据。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/session
- SessionMessageProjection(消息投影)
- SessionMessageProjection 是对一个插件拥有、会修改既有消息内容的事件的纯解释:声明它所解释的 type,并实现 project(event, context) 返回以原始事件 seq 为键的不可变消息映射;它必须在校验完整持久决策后再返回更新,保留消息身份且不修改输入。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/session
- ctx.sessionProjections
- ctx.sessionProjections 是 dsh-session-projection 提供的投影 seam:已注册单元增量折叠已提交事件,host 消费方用 stateOf() 读取单个类型化状态,载体用 snapshot() 批量取得裁剪后的客户端视图;贡献方不得为缺失的 host 值静默提供默认值。— https://deepseek-harness.github.io/deepseek-harness/reference/
来源
- DeepSeek Harness 官方文档 - 会话· deepseek-ai
- DeepSeek Harness 官方文档 - 架构总览(投影 seam)· deepseek-ai