乐于分享
好东西不私藏

DeepSeek Harness 源码-事件系统(Session/Agent/Capability events)

DeepSeek Harness 源码-事件系统(Session/Agent/Capability events)

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并发等待所有 listenersession/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/eventemit(不阻塞热路径),session/flushparallel(等待所有持久化插件完成)。这是性能与可靠性的平衡——追加不阻塞,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/end4
LLM 消息user/message, assistant/message, assistant/chunk3
工具管道tool/call, tool/result, tool/code-dispatch, tool/code-dispatch-start4
请求元数据request/header, request/context2
审批/权限approval/asked, approval/decided, approval/policy, permission/preset4
压缩/内存管理compaction/start, compaction/end, compaction/summary, compaction/prune4
目标/计划goal/change, plan/mode, todo/write3
Subagent/Workflowsubagent/descriptor, tool-workflow/* (4种)5
Hook/命令/调度hook/invoked, hook/result, command/run, command/done, schedule/change5
Session 元数据session/end-seed, session/title, session/title-llm-request, agent-preset/selected, agent/inbox/spliced5
其他feedback/record, sandbox/mode, llm/retry, llm/retry-started, web/deepseek-search-llm-request5

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' }

关键设计:TurnEndReasonMapmerge-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-remotesAPI_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技术推荐官」获取更多源码解析内容