DeepSeek Harness 源码解析系列
第 3 讲:事件系统(Session/Agent/Capability events)
基于 DeepSeek Harness 源码 · 2026-08-18
一、事件驱动架构总览
DeepSeek Harness 的核心理念是"everything is a plugin"(万物皆插件)。插件之间如何通信?答案是事件系统。整个框架围绕三层事件构建:
🔹 Cordis 事件总线 — 底层事件分发引擎,5 种 dispatch 模式
🔹 Session Events — 会话级事件,append-only 日志,事件溯源(event sourcing)
🔹 Capability Events — 能力层事件,工具调用、审批、提问等业务信号
理解事件系统是理解整个 Harness 的关键——Session 不存储消息,只存储事件,消息是从事件日志中派生出来的。
二、Cordis 事件总线:5 种分发模式
Cordis 是 Harness 的底层插件框架(vendored 在 vendor/cordis/)。它的事件总线 EventsService 提供 5 种分发策略,每种对应不同的语义:
📄 vendor/cordis/src/events.ts (第 25-32 行)
/**
* Event dispatch strategy used by the event service.
*
* `emit` runs synchronous listeners without awaiting them, `parallel` awaits
* all listeners together, `serial` awaits them in order until one bails,
* `bail` stops on the first synchronous bail value, and `waterfall` composes
* listeners around a final `next` callback.
*/
export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'| 模式 | 语义 | 典型场景 |
|---|---|---|
emit | 同步调用,不等待返回 | session/event 通知、日志 |
parallel | 并发等待所有 listener | session/flush 持久化检查点 |
serial | 串行等待,首个 bail 值终止 | 权限检查、拦截器链 |
bail | 同步串行,首个非空值终止 | internal/listener 注册拦截 |
waterfall | 中间件链,next() 传递 | internal/update 配置更新 |
每种模式对应 ctx 上的同名方法:
📄 vendor/cordis/src/events.ts (第 43-97 行, 节选)
declare module './context.ts' {
export interface Context {
/** 同步广播,忽略返回值 */
emit (name: K, ...args: Parameters ): void
/** 并发等待所有 listener */
parallel (name: K, ...args: Parameters ): Promise
/** 串行等待,首个 bail 值终止 */
serial (name: K, ...args: Parameters ): Promisify<ReturnType >
/** 同步 bail */
bail (name: K, ...args: Parameters ): ReturnType
/** 中间件链,next() 传递 */
waterfall (name: K, ...args: Parameters ): ReturnType
/** 注册 listener,fiber 卸载时自动移除 */
on (name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
/** 一次性 listener */
once (name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
}
}关键设计:listener 注册在 fiber.effect() 上,fiber 卸载时自动清理——这是 Cordis 的生命周期管理,避免了内存泄漏。
📄 vendor/cordis/src/events.ts (第 254-259 行)
register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
const method = options.prepend ? 'unshift' : 'push'
return this.ctx.fiber.effect(() => {
hooks[method]({ ctx: this.ctx, callback, ...options })
return () => this.unregister(hooks, callback)
}, label)
}作用域过滤(scope filtering):listener 可以设置 global: true 跳过过滤,默认只接收同 scope 内的事件。这实现了多 agent 隔离——agent A 的 session 事件不会泄露给 agent B。
三、Session Events:事件溯源的核心
Harness 的 Session 不是传统的"消息列表",而是一个append-only 事件日志。所有状态变化都通过追加事件来表达,消息历史从事件日志中派生。这就是经典的event sourcing(事件溯源)模式。
3.1 四个 Session 生命周期事件
📄 packages/core/session/src/index.ts (第 37-87 行)
declare module '@deepseek-ai/cordis' {
interface Events {
/**
* 创建公告:session 发布时触发。同步抛出可 veto 并回滚;
* detach 请求在 dispatch 期间被延迟。
* @mode emit
*/
'session/created'(this: Scoped<Session>, session: Session): void
/**
* 销毁通知:session 离开 store 时触发一次,包括发布回滚。
* @mode emit
*/
'session/disposed'(this: Scoped<Session>, session: Session): void
/**
* 追加 feed:post-commit 的 fire-and-forget 通知。
* listener 在事件 commit 后运行,失败被隔离。
* @mode emit
*/
'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void
/**
* 持久化检查点:所有 listener 并行运行,调用者等待全部完成。
* @mode parallel
*/
'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void
}
}注意 @mode 注解:session/event 用 emit(不阻塞热路径),session/flush 用 parallel(等待所有持久化插件完成)。这是性能与可靠性的平衡——追加不阻塞,flush 要等。
3.2 SessionEventMap:48 种事件类型
所有合法的事件类型由 KNOWN_SESSION_EVENT_TYPES 声明:
📄 packages/core/session/src/known-event-types.ts (完整列表)
export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([
'agent-preset/selected',
'agent/inbox/spliced',
'approval/asked',
'approval/decided',
'approval/policy',
'assistant/chunk',
'assistant/message',
'command/done',
'command/run',
'compaction/end',
'compaction/prune',
'compaction/start',
'compaction/summary',
'feedback/record',
'goal/change',
'hook/invoked',
'hook/result',
'llm/retry',
'llm/retry-started',
'permission/preset',
'plan/mode',
'request/context',
'request/header',
'sandbox/mode',
'schedule/change',
'session/end-seed',
'session/title',
'session/title-llm-request',
'step/end',
'step/start',
'subagent/descriptor',
'todo/write',
'tool-workflow/agent-end',
'tool-workflow/agent-start',
'tool-workflow/run-end',
'tool-workflow/run-start',
'tool/call',
'tool/code-dispatch',
'tool/code-dispatch-start',
'tool/result',
'turn/end',
'turn/start',
'user/message',
'web/deepseek-search-llm-request',
])这些事件可按领域分类:
| 领域 | 事件类型 | 数量 |
|---|---|---|
| Turn/Step 生命周期 | turn/start, turn/end, step/start, step/end | 4 |
| LLM 消息 | user/message, assistant/message, assistant/chunk | 3 |
| 工具管道 | tool/call, tool/result, tool/code-dispatch, tool/code-dispatch-start | 4 |
| 请求元数据 | request/header, request/context | 2 |
| 审批/权限 | approval/asked, approval/decided, approval/policy, permission/preset | 4 |
| 压缩/内存管理 | compaction/start, compaction/end, compaction/summary, compaction/prune | 4 |
| 目标/计划 | goal/change, plan/mode, todo/write | 3 |
| Subagent/Workflow | subagent/descriptor, tool-workflow/* (4种) | 5 |
| Hook/命令/调度 | hook/invoked, hook/result, command/run, command/done, schedule/change | 5 |
| Session 元数据 | session/end-seed, session/title, session/title-llm-request, agent-preset/selected, agent/inbox/spliced | 5 |
| 其他 | feedback/record, sandbox/mode, llm/retry, llm/retry-started, web/deepseek-search-llm-request | 5 |
3.3 事件追加与深度冻结
每个事件追加时经历严格的验证和冻结流程:
📄 packages/core/session/src/index.ts (第 604-655 行)
append (
type: T,
data: SessionEventMap[T],
...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : []
): SessionEvent {
// 1. JSON 序列化验证 — 非 JSON 安全数据直接拒绝
const dataSnapshot = snapshotJsonValue(data)
if (dataSnapshot === undefined) {
throw new Error(`session event "${type}" carries non-JSON-serializable data`)
}
// 2. Surface 元数据验证
const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata)
// 3. 构建不可变事件对象
const event = deepFreeze({
type,
seq: this.log.length,
time: Date.now(),
data: dataSnapshot,
...(surfaceMetadataSnapshot as { surfaceOp?: unknown; sourceEventSeqs?: unknown }),
} as unknown as SessionEvent )
// 4. Surface 状态机验证
this.surfaceManager.validateNext(event as SessionEvent)
// 5. 推入日志,通知 observer
this.log.push(event as SessionEvent)
this.eventsSnapshot = undefined
if (callbacks !== undefined && entry !== undefined) {
invokeContainedSessionObservers(entry.emitCtx, 'session/event', entry.id, callbackArgs, callbacks)
}
return event
}关键设计要点:
🔹 JSON 安全性:所有事件数据必须无损耗 JSON 序列化,BigInt/function/symbol/undefined 等被拒绝
🔹 深度冻结:deepFreeze() 使事件不可变,防止后续篡改
🔹 连续序列号:seq = this.log.length,保证 seq 从 0 开始连续递增
🔹 Observer 隔离:listener 失败被 invokeContainedSessionObservers 捕获,不影响事件 commit
🔹 热路径不阻塞:append 完成后 observer 在 finally 块外执行,持久化插件异步缓冲
四、Ordered Surface:消息派生机制
Harness 中最精妙的设计之一是ordered surface(有序表面)。事件日志中包含所有事件(turn 边界、chunk、元数据等),但 LLM 消息历史只从"表面事件"派生:
📄 packages/core/session/src/types.ts (节选)
/** 只有这三种事件类型产生 LLM 消息,可出现在 ordered surface 上 */
export type SurfaceEventType =
| 'user/message'
| 'assistant/message'
| 'tool/result'
/** Surface 操作:append 追加到尾部,replace 替换一段 */
export type SurfaceOp =
| 'append'
| { op: 'replace'; start: number; end: number }SurfaceManager 维护一个有序节点列表,每个节点是一个 surface 事件的 seq 号。消息派生时遍历 surface 而非全量日志:
📄 packages/core/session/src/index.ts (第 726-747 行)
deriveMessages(): Message[] {
const surface = this.surface
const nodes = surface.nodes
const generation = surface.replaceGeneration
if (generation !== this.derivedGeneration) {
// replace 操作后重建缓存
this.derived = []
this.derivedNodes = 0
this.derivedGeneration = generation
}
for (const seq of nodes.slice(this.derivedNodes)) {
// 每个 surface 节点只投影一次
const msg = this.deriveEventMessage(this.log[seq]!)
if (msg) this.derived.push(msg)
}
this.derivedNodes = nodes.length
return [...this.derived]
}Surface 与 Event Log 的关系
Event Log (append-only): seq 0: turn/start ← 不在 surface 上 seq 1: user/message ✅ ← surface node #0 (append) seq 2: step/start ← 不在 surface 上 seq 3: assistant/chunk ← 不在 surface 上(原始 token) seq 4: tool/call ← 不在 surface 上 seq 5: assistant/message ✅ ← surface node #1 (append) seq 6: tool/result ✅ ← surface node #2 (append) seq 7: step/end ← 不在 surface 上 seq 8: turn/end ← 不在 surface 上 Ordered Surface: [0, 1, 2] (node indices into event log) → deriveMessages() 返回 [user_msg, assistant_msg, tool_result]
Compaction 场景:当上下文过长需要压缩时,replace 操作替换 surface 中的一段节点为压缩后的摘要节点,而原始事件日志保持不变(可审计、可回放)。
五、Agent 生命周期事件
Agent 的 turn/step 生命周期通过事件日志记录,形成完整的审计轨迹:
📄 packages/core/session/src/types.ts (TurnEndReasonMap)
/** Turn 结束原因,merge-extensible 类型 */
export interface TurnEndReasonMap {
completed: { kind: 'completed' }
aborted: { kind: 'aborted'; reason: TurnEndCancelCause }
blocked: { kind: 'blocked' }
error: { kind: 'error'; error: LlmFailure }
'max-tokens': { kind: 'max-tokens' }
interrupted: { kind: 'interrupted' }
}
/** 取消原因 */
export type AgentCancelCause =
| { readonly kind: 'user' }
| { readonly kind: 'parent' }
| { readonly kind: 'hook'; readonly reason: string }
| { readonly kind: 'disposed' }关键设计:TurnEndReasonMap 是 merge-extensible 的——插件可以通过 TypeScript declaration merging 添加新的结束原因,而无需修改核心代码。这体现了 Cordis 插件框架的开放封闭原则。
Turn-Step 嵌套结构
Agent Loop 事件流示例
Turn #1:
turn/start { turn: 1 }
┌─ Step #1:
│ step/start { turn: 1, step: 1 }
│ user/message { id: "msg-1", role: "user", content: [...], source: {...} }
│ assistant/chunk { turn: 1, step: 1, chunk: {...} } (×N)
│ assistant/message { turn: 1, step: 1, message: {...}, usage: {...} }
│ tool/call { turn: 1, step: 1, callId: "call-1", name: "read_file", arguments: "..." }
│ tool/result { turn: 1, step: 1, message: {...} }
│ assistant/message { turn: 1, step: 1, message: {...} }
│ step/end { turn: 1, step: 1 }
└─ (Step #2, #3 ... if tool calls continue)
turn/end { turn: 1, reason: { kind: 'completed' } }六、API 层事件流:MuxFrame 与 HostFrame
客户端通过两条逻辑流接收事件:
6.1 Mux 流(多路复用)
Mux 流聚合所有 session 的事件,通过 MuxFrame 联合类型传递:
📄 packages/host/apiproxy/src/api/events.ts (第 69-108 行)
export type MuxFrame =
| { type: 'session/event'; sessionId: SessionId; event: SessionEvent; view?: ToolEventView }
| { type: 'session/subscribed'; sessionId: SessionId; lastSeq: number }
| { type: 'approval/requested'; sessionId: SessionId; approvalId: ApprovalRequestId; toolName: string; ... }
| { type: 'approval/resolved'; sessionId: SessionId; approvalId: ApprovalRequestId; outcome: ApprovalOutcome }
| { type: 'question/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] }
| { type: 'question/resolved'; sessionId: SessionId; questionRpcId: RpcId; outcome: 'answered' | 'cancelled' }
| { type: 'session/queue'; sessionId: SessionId; items: QueuedInboxItem[] }
| { type: 'session/jobs'; sessionId: SessionId; jobs: JobView[] }
| { type: 'session/projection'; sessionId: SessionId; key: string; value: unknown; seq: number }
| { type: 'stream/error'; error: RpcError }关键设计:审批和提问(approval/question)不经过 session event log——它们是瞬态交互,只在 Mux 流上传递。这避免了将 UI 交互状态污染到持久化的事件日志中。
6.2 Host 流
Host 流传递全局级别的事件:
📄 packages/host/apiproxy/src/api/events.ts (第 127-155 行)
export type HostFrame =
| { type: 'host/session-added'; sessionId: SessionId; blank: boolean; parentSessionId?: SessionId; origin?: 'subagent'; cwd?: string; agentPreset?: string }
| { type: 'host/session-removed'; sessionId: SessionId }
| { type: 'host/session-status'; sessionId: SessionId; running: boolean }
| { type: 'host/agent-error'; sessionId: SessionId; message: string }
| { type: 'host/workspace-changed'; workspace: WorkspaceView }
| { type: 'host/workspace-removed'; workspaceId: WorkspaceView['workspaceId'] }
| { type: 'host/workspace-order-changed'; workspaceIds: WorkspaceView['workspaceId'][] }
| { type: 'host/archived-sessions-changed'; archivedSessionIds: SessionId[] }
| { type: 'host/remote-event'; event: string; args: JsonValue[] }
| { type: 'stream/error'; error: RpcError }host/remote-event 是一个通配转发通道——白名单内的 Cordis 事件原样转发给客户端,不经过投影或重命名。白名单由 @deepseek-ai/dsh-api-remotes 的 API_REMOTE_FORWARDED_EVENTS 控制。
七、事件格式版本与兼容性
事件日志的持久化格式由 SESSION_FORMAT_VERSION 控制:
📄 packages/core/session/src/types.ts (第 30-55 行)
/**
* 磁盘格式版本,写入每个 SessionHeader。
* 当前为 0(未发布状态),无兼容性保证。
*
* 版本判断标准:WRITER 发出的变化,而非 READER 能接受的。
* 只有结构性变化才 bump:header shape、event envelope、
* core event semantics、surface mechanism。
* 新增普通 event type 不 bump — 用 SessionEvent.ignorable 保护。
*/
export const SESSION_FORMAT_VERSION = 0版本策略要点:
🔹 版本号是单调整数,无 major/minor 分割
🔹 Bump 决策看写入者(writer),不看读取者(reader)
🔹 新增事件类型不需要 bump——ignorable: true 标记让旧运行时无视未知事件
🔹 完整升级机制记录在 Agent Note 中
八、架构总结
事件系统三层架构
┌─────────────────────────────────────────────┐ │ Layer 3: API 事件流 (MuxFrame / HostFrame) │ │ ┌──────────────┐ ┌──────────────────┐ │ │ │ Mux Stream │ │ Host Stream │ │ │ │ 多session聚合 │ │ 全局状态 │ │ │ │ 审批/提问 │ │ workspace │ │ │ │ projection │ │ remote-event │ │ │ └──────┬───────┘ └────────┬─────────┘ │ ├─────────┼───────────────────┼───────────────┤ │ Layer 2: Session Events (48 types) │ │ ┌──────────────┐ ┌──────────────────┐ │ │ │ Surface │ │ Log-only │ │ │ │ user/msg │ │ turn/step │ │ │ │ assistant │ │ request/header │ │ │ │ tool/result │ │ compaction │ │ │ └──────┬───────┘ └────────┬─────────┘ │ ├─────────┼───────────────────┼───────────────┤ │ Layer 1: Cordis EventBus (5 dispatch modes) │ │ emit │ parallel │ serial │ bail │ waterfall │ │ ┌──────────────────────────────────────┐ │ │ │ Fiber lifecycle + scope filtering │ │ │ └──────────────────────────────────────┘ │ └─────────────────────────────────────────────┘
设计哲学总结:
🔹 事件溯源:Session 是 append-only 日志,消息从事件派生,而非直接存储
🔹 不可变性:每个事件深度冻结,日志不可篡改
🔹 插件可扩展:TurnEndReasonMap merge-extensible,未知事件通过 ignorable 标记兼容
🔹 性能隔离:热路径(append)不阻塞,持久化异步缓冲,observer 失败被隔离
🔹 作用域安全:多 agent 通过 scope filtering 隔离,事件不跨 agent 泄露
📚 系列导航
← 第 2 讲:Profile、Bundle 与 Preset 组合机制
→ 第 4 讲:Turn flow 与 Agent 生命周期
关注公众号「AI技术推荐官」获取更多源码解析内容
夜雨聆风