返回知识库
0

第三章:Cordis 框架基础

本章目标:帮助你理解 Cordis 是什么、Plugin/Service/Context 模型如何工作、ctx.effect()ctx.on() 的注册机制、Waterfall 语义的细节、以及生命周期管理。阅读本章后,你应该能编写一个简单的 Cordis 插件并理解 dsh 中每个插件的挂载原理。


3.1 什么是 Cordis

Cordis 是一个上下文驱动的插件框架,它是 DeepSeek Harness 的底层基础设施。dsh 中的每一个功能——从模型适配器到工具注册、从会话日志到 Agent 循环本身——都是通过 Cordis 插件实现的。

核心设计原则

  • 没有特权核心:你通过配置来扩展 dsh,而不是通过修改核心代码

  • Registrations are effects:所有注册都是可逆的效果

  • 声明式依赖:通过 inject 声明依赖,加载顺序由依赖关系自动推导

预览
源码

Cordis 运行时

Context

Registry Service

Events Service

Logger Service

Reflect Service

Plugin Mounting

Event Dispatch

Logging

Service Resolution

graph TB
    subgraph "Cordis 运行时"
        A[Context] --> B[Registry Service]
        A --> C[Events Service]
        A --> D[Logger Service]
        A --> E[Reflect Service]
        B --> F[Plugin Mounting]
        C --> G[Event Dispatch]
        D --> H[Logging]
        E --> I[Service Resolution]
    end

3.2 五个核心概念

3.2.1 Plugin(插件)

Plugin 是 Cordis 的基本单元。它可以是两种形式之一:

函数形式

// 最简单的插件
function myPlugin(ctx: Context) {
  // 插件逻辑
}

// 带依赖声明的插件
function myPlugin(ctx: Context) {
  // 可以访问 ctx.llm、ctx.tools 等已注册服务
}
myPlugin.inject = ['llm', 'tools']  // 声明依赖

类形式(Service 子类)

import { Service } from '@deepseek-ai/cordis'

class MyService extends Service {
  // 服务在构造时自动注册
  constructor(ctx: Context) {
    super(ctx, 'myService')  // 注册到 ctx.myService
  }
}

3.2.2 Context(上下文)

Context 是服务的仓库。每个插件通过 ctx.<key> 访问服务,而不是通过导入具体实现。

// Context 是一个代理对象
interface Context {
  /** 根上下文 */
  root: this
  /** 事件总线 */
  events: EventsService
  /** 日志服务 */
  logger: LoggerService
  /** 反射层,服务解析 */
  reflect: ReflectService
  /** 插件注册表 */
  registry: RegistryService
  // ... 通过声明合并添加的更多服务
}

// 运行时,Context 是一个 Proxy
class Context {
  constructor() {
    const self = new Proxy(this, ReflectService.handler)
    // ...
    return self
  }
}

关键理解:当你读取 ctx.tools 时,Cordis 通过 Proxy 的 get trap 来解析实际的服务实例。

3.2.3 Service(服务)

Service 是暴露在 Context 上的 API。通过 ctx.provide() 或继承 Service 类来注册。

// 通过 ctx.provide() 注册
ctx.provide('myService', {
  doSomething() { /* ... */ }
})

// 通过 Service 类注册
class ToolRegistryService extends Service {
  static readonly provide = 'tools'
  
  constructor(ctx: Context) {
    super(ctx, 'tools')
  }
  
  register(tool: ToolSchema) { /* ... */ }
}

已知的服务键

ctx 键拥有包用途
ctx.sessionscore/session会话事件日志和内存存储
ctx.systemPromptcore/system-promptPrompt 段落和工具 schema 组装
ctx.toolscore/tools带范围的工具注册表和受保护执行管道
ctx.agentscore/agentAgent 接口、活注册表和 agent/* 事件
ctx.agentLoopcore/agent-loop默认驱动实现
ctx.llmllm/llm消息和流词汇表 + 适配器接缝

3.2.4 inject(依赖声明)

通过 inject 声明服务依赖,Cordis 等待这些服务存在后才加载插件。

function agentLoop(ctx: Context) {
  // ctx.llm、ctx.tools、ctx.sessions 保证已注册
  const llm = ctx.llm
  const tools = ctx.tools
  const sessions = ctx.sessions
}

agentLoop.inject = ['llm', 'tools', 'sessions']

加载顺序由依赖关系推导,而不是手动编排。

3.2.5 Registration is an Effect(注册是效果)

所有注册都是可逆的效果。当插件卸载时,所有注册自动撤销。

// ctx.effect() 返回一个清理函数
const dispose = ctx.effect(() => {
  // 注册服务
  const service = ctx.provide('myService', { /* ... */ })
  
  // 注册事件监听
  const handler = ctx.on('some-event', (data) => { /* ... */ })
  
  // 返回清理函数
  return () => {
    service.dispose()
    handler.dispose()
  }
})

// 之后调用 dispose() 可以撤销所有注册
dispose()

3.3 事件派发模式

Cordis 提供五种事件派发模式,每种有不同的语义:

3.3.1 emit(同步广播)

同步执行所有监听者,不等待,不返回值。

ctx.emit('session/event', { type: 'user/message', data: { text: 'hello' } })

用途:通知观察者,如日志记录。

3.3.2 waterfall(瀑布/中间件)

ctx.waterfall('agent/pre-step', decision, next => {
  // decision 是当前值
  // 调用 next() 委托给下一个监听者
  // 不调用 next() 则短路
  return next({ ...decision, messages: rewritten })
})

关键规则

  • 监听者接收 (...args, next)

  • 必须调用 next() 来委托,否则短路

  • next() 返回下游结果

dsh 中的 waterfall 事件

事件用途
agent/pre-step决定模型看到什么
agent/request拦截/修改 LLM 请求
llm/stream拦截/修改模型流
tools/pre-execute工具执行前拦截
tools/execute工具执行
tools/post-execute工具执行后拦截

3.3.3 parallel(并行)

异步执行所有监听者,等待全部完成。

await ctx.parallel('some-parallel-event', data)

用途:扇出操作,如并发初始化。

3.3.4 serial(串行)

按注册顺序异步执行监听者,直到某个返回 bail 值。

const result = await ctx.serial('some-serial-event', data)

用途:有序执行,如依次尝试策略。

3.3.5 bail(短路)

同步执行监听者,直到某个返回 bail 值(非 null/undefined/false)。

const result = ctx.bail('some-bail-event', data)

用途:策略决策,如权限检查。

派发模式对比

模式等待?派发顺序返回值?典型用途
emit注册顺序通知观察者
waterfall注册顺序中间件链
parallel并行扇出
serial注册顺序有序执行
bail直到 bail策略短路

3.4 Waterfall 语义详解

Waterfall 是 dsh 中最重要的派发模式。它实现了一个环绕中间件(around-middleware)模式。

3.4.1 基本流程

预览
源码
最终行为监听者 2监听者 1调用者最终行为监听者 2监听者 1调用者L1 修改 value,调用 next()L2 修改 value,调用 next()waterfall('event', value, next)next(modifiedValue)next(furtherModifiedValue)resultresultresult
sequenceDiagram
    participant Caller as 调用者
    participant L1 as 监听者 1
    participant L2 as 监听者 2
    participant Final as 最终行为

    Caller->>L1: waterfall('event', value, next)
    Note over L1: L1 修改 value,调用 next()
    L1->>L2: next(modifiedValue)
    Note over L2: L2 修改 value,调用 next()
    L2->>Final: next(furtherModifiedValue)
    Final-->>L2: result
    L2-->>L1: result
    L1-->>Caller: result

3.4.2 实际例子

// 监听 agent/pre-step 事件,修改模型输入
ctx.on('agent/pre-step', (decision, next) => {
  // 1. 添加上下文信息
  decision.messages.push({
    role: 'system',
    content: '当前时间: ' + new Date().toISOString()
  })
  
  // 2. 必须调用 next() 来委托
  return next(decision)
})

// 另一个监听者可以进一步修改
ctx.on('agent/pre-step', (decision, next) => {
  // 如果条件不满足,可以短路(不调用 next())
  if (!decision.messages.length) {
    return  // 短路,不调用 next()
  }
  return next(decision)
})

3.4.3 短路的后果

不调用 next() 意味着:

  • 下游监听者不会执行

  • 最终行为不会执行

  • 返回当前监听者的结果

必须调用 next() 的原因

  • 如果不调用,整个链被中断

  • 模型请求可能无法发出

  • 工具可能无法执行


3.5 生命周期管理

3.5.1 Fiber(光纤)

Cordis 中的每个插件运行在一个 Fiber 中。Fiber 管理插件的挂载、激活、卸载和清理。

预览
源码

Created

Pending

Loading

Active

Unloading

Disposed

Failed

graph TB
    A[Created] --> B[Pending]
    B --> C[Loading]
    C --> D[Active]
    D --> E[Unloading]
    E --> F[Disposed]
    D --> G[Failed]
    E --> F

3.5.2 Fiber 状态

状态说明
Created刚创建
Pending等待依赖
Loading正在加载
Active活跃运行
Unloading正在卸载
Disposed已清理
Failed加载失败

3.5.3 Disposer(清理器)

ctx.effect() 返回的清理函数在 Fiber 卸载时按反向注册顺序执行。

// 注册顺序:1, 2, 3
const d1 = ctx.effect(() => { /* 注册 A */ return () => { /* 清理 A */ } })
const d2 = ctx.effect(() => { /* 注册 B */ return () => { /* 清理 B */ } })
const d3 = ctx.effect(() => { /* 注册 C */ return () => { /* 清理 C */ } })

// 卸载时:清理 C, 清理 B, 清理 A(反向顺序)

3.5.4 作用域隔离

// ctx.isolate() 创建独立的作用域
const isolatedCtx = ctx.isolate('tools', Symbol('myScope'))

// 在隔离作用域下注册的服务不影响父作用域
isolatedCtx.provide('tools', myCustomToolRegistry)

// ctx.intercept() 添加服务特定的配置拦截
const interceptedCtx = ctx.intercept('llm', { provider: 'custom' })

3.6 实际例子:编写一个简单的插件

import { Context } from '@deepseek-ai/cordis'

/**
 * 一个简单的工具计数器插件
 * 记录每个工具被调用的次数
 */
export function toolCounterPlugin(ctx: Context) {
  // 依赖声明:需要 tools 服务
  // 在实际 dsh 中,这会是 ctx.tools
  const counts = new Map<string, number>()
  
  // 注册事件监听器
  const disposable = ctx.on('tool/result', (result) => {
    const name = result.toolName
    counts.set(name, (counts.get(name) ?? 0) + 1)
    ctx.logger('tool-counter').info(`Tool "${name}" called ${counts.get(name)} times`)
  })
  
  // 提供一个查询服务
  ctx.provide('toolCounts', {
    getCount(toolName: string): number {
      return counts.get(toolName) ?? 0
    },
    getAllCounts(): Map<string, number> {
      return new Map(counts)
    }
  })
  
  // 返回清理函数
  return () => {
    disposable.dispose()
    // toolCounts 会随 Fiber 卸载自动清理
  }
}

// 声明依赖
toolCounterPlugin.inject = ['tools']

注册到 Profile

# cordis.patch.yml
- id: my-tool-counter
  config: {}
// package.json
{
  "dsh": {
    "plugins": {
      "my-tool-counter": "./path/to/tool-counter.ts"
    }
  }
}

3.7 Loader 与配置

3.7.1 cordis.yml

cordis.yml 是 Cordis 的配置文件。Loader 解析它并挂载插件。

# 基本格式
- id: plugin-id
  name: @scope/plugin-name
  config:
    key: value

3.7.2 !!js 表达式

配置中支持 !!js 表达式,用于动态配置:

- id: my-plugin
  config:
    # !!js 表达式在加载时求值
    apiKey: !!js process.env.API_KEY
    # disabled 也可以是 !!js 表达式
  disabled: !!js process.env.NODE_ENV === 'production'

3.7.3 Patch 层

Patch 层按顺序应用,覆盖或添加配置行:

预览
源码

Bundle 层

Profile Patch

Home Patch

CLI Overlay

graph LR
    A["Bundle 层"] --> B["Profile Patch"]
    B --> C["Home Patch"]
    C --> D["CLI Overlay"]

每个 Patch 目标是一个 entry,替换其整个配置或插入新行。


3.8 小结

概念一句话解释
PluginCordis 的基本单元,函数或 Service 子类
Context服务的仓库,通过 Proxy 解析
Service暴露在 Context 上的 API
inject声明依赖,自动推导加载顺序
Registration可逆的效果,卸载时自动清理
Waterfall环绕中间件,必须调用 next() 委托
Fiber插件的生命周期容器
Disposer反向注册顺序执行的清理函数

下一步第四章:核心与 Agent 循环——深入理解 Agent、AgentLoop、Turn/Step 生命周期、Cancel 机制和 Scope 模型。

DeepSeek-Harness / 03-Cordis 框架基础 0 0 iliuqi
2026-09-04T04:08:17.835439036Z 2026-09-04T07:56:45.860916988Z