DeepSeek Harness插件系统深度解析:从Cordis内核到12个能力Seam的源码级拆解

上一篇我们从架构层面介绍了DeepSeek Harness的Everything is a Plugin设计哲学。这一篇深入源码,从Cordis微内核的Context对象、插件生命周期Fiber状态机、类型化事件系统,到12个可替换能力Seam、8个生命周期钩子、三层工具执行管道、事件源会话日志和Profile/Bundle/Patch分层配置,逐层拆解dsh插件系统的内部工作机制。理解了这套机制,你就掌握了从工具开发到Agent Loop替换的全部扩展入口。

一、Cordis内核:dsh的运行地基

1.1 Cordis是什么

Cordis是dsh的底层依赖框架(vendored版本4.0.1),源自Koishi生态,其设计思想来自论文A Programming Paradigm for Spatiotemporal Composability。Cordis的运行模型可以概括为一句话:插件向共享上下文贡献服务、类型化事件和可逆的副作用。

在Cordis中,上下文(context,即ctx)是一个服务容器。插件通过ctx.provide()把服务写入ctx,服务以稳定的ctx.<key>形式暴露(如ctx.tools、ctx.llm、ctx.sessions);通过inject声明对其他服务的依赖;通过类型化事件与其余插件通信。所有注册都是可逆的副作用——插件通过ctx.effect()或ctx.on()安装注册项,插件被卸载时其disposer会被调用,实现干净的资源回收。

核心概念

定义

类比

dsh中的体现

Context(上下文)

服务容器,所有能力挂载于此

应用的全局注册表

ctx.tools / ctx.llm / ctx.sessions

Plugin(插件)

向ctx贡献服务和事件的模块

功能单元

每个能力包都是一个插件

Service(服务)

挂载在ctx上的可调用能力

API接口

ctx.tools.register()

Event(事件)

类型化的消息通道

消息总线

tools/pre-execute / agent/request

Inject(依赖注入)

声明插件需要的服务

import的解耦版

inject: ['tools', 'llm']

Effect(副作用)

可撤销的资源注册

useEffect的cleanup

ctx.effect(() => { return dispose })

Fiber(纤程)

插件生命周期状态机

组件生命周期

PENDING→ACTIVE→DISPOSED

1.2 插件生命周期Fiber状态机

每个插件在Cordis中由一个Fiber对象管理其生命周期。Fiber是一个有限状态机,追踪插件从加载到销毁的完整过程。

PENDING ──→ LOADING ──→ ACTIVE

  │            │            │

  │            │            ├─ 正常卸载

  │            │            │

  │            │            ▼

  │            │        UNLOADING

  │            │            │

  │            ▼            ▼

  └─────────► FAILED ──→ DISPOSED

状态

含义

触发条件

PENDING

已声明但未加载

插件被注册但依赖尚未就绪

LOADING

正在执行apply()

所有inject依赖已就绪

ACTIVE

正常运行中

apply()执行完成,服务已注册

UNLOADING

正在执行清理

依赖消失或主动卸载

FAILED

加载失败

apply()抛出异常

DISPOSED

已销毁,资源已释放

清理完成,无法恢复

inject声明是这个状态机的关键:插件A声明inject: ['tools'],意味着Fiber会等待ctx.tools就绪后才从PENDING进入LOADING。如果tools服务后来被卸载了,A的Fiber会自动进入UNLOADING并清理资源。这就是依赖驱动的自动激活/卸载——加载顺序由依赖决定,不是文件顺序。

1.3 四种事件分发模式

Cordis的事件系统不是简单的发布订阅,而是支持四种分发模式,适应不同的扩展场景。

模式

语义

调用方式

适用场景

emit

广播通知,各监听器独立执行

ctx.emit('event', payload)

日志、遥测、UI更新

waterfall

顺序传递,每个监听器可修改结果

ctx.waterfall('event', initial, next)

权限检查、结果改写、Hook链

parallel

并行执行,等待全部完成

ctx.parallel('event', payload)

初始化、批量通知

serial

串行执行,按顺序等待

ctx.serial('event', payload)

需要顺序保证的初始化

其中waterfall模式是dsh插件系统最核心的扩展机制。它和Koa/Express中间件一模一样:每个监听器收到(args, next),调用next()放行给下一个,不调用就短路返回。tools/pre-execute就是典型的waterfall——Hook规则、权限策略、沙箱策略依次检查,任何一个说deny就整个链条终止。

二、插件树:12个可替换的核心能力Seam

dsh的扩展性不是功能,是它的存在方式本身。12个核心能力全部以Seam(能力接缝)的形式存在,每个Seam包含Service Definition(接口定义)、Service Provider(具体实现)和Consumer(面向模型/用户的入口)三个角色。

┌──────────────────────────────────────────────────────────┐

│                    Cordis Context (ctx)                    │

│                                                            │

│  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐   │

│  │ llm │  │ fs  │  │shell│  │subp │  │sandb│  │ web │   │

│  └──┬──┘  └──┬──┘  └──┬──┘  └──┬──┘  └──┬──┘  └──┬──┘   │

│     │        │        │        │        │        │        │

│  ┌──┴────────┴────────┴────────┴────────┴────────┴──┐   │

│  │            Service Provider 注册/查找              │   │

│  └──┬────────┬────────┬────────┬────────┬────────┬──┘   │

│     │        │        │        │        │        │        │

│  ┌──┴──┐  ┌──┴──┐  ┌──┴──┐  ┌──┴──┐  ┌──┴──┐  ┌──┴──┐   │

│  │tools│  │sess │  │comp │  │subag│  │code │  │creds│   │

│  └─────┘  └─────┘  └─────┘  └─────┘  └─────┘  └─────┘   │

│                                                            │

│  共12个核心能力Seam,全部可替换                              │

└──────────────────────────────────────────────────────────┘

能力Seam

ctx

接口定义职责

典型Provider

替换影响范围

模型适配器

ctx.llm

流式生成、工具调用格式

DeepSeek/OpenAI/Anthropic

所有Agent的模型调用

文件系统

ctx.fs

读写删列、目录操作

本地FS/E2B远程FS/内存FS

全部文件读写

Shell执行

ctx.shell

命令执行、输入输出

本地Shell/沙箱Shell

所有命令执行

子进程

ctx.subprocess

进程生命周期管理

本地进程/远程进程

Bash/LSP/子Agent

沙箱

ctx.sandbox

安全策略、执行边界

landlock/Docker/E2B

进程安全隔离

Web访问

ctx.web

搜索、抓取、浏览

内置搜索/SearXNG

搜索和网页抓取

工具注册表

ctx.tools

注册、查找、执行工具

内置工具/MCP工具

模型可调用的全部工具

子Agent

ctx.subagents

创建、调度子Agent

Spawn/Fork/ACP/Claude/Codex

多Agent编排

会话持久化

ctx.sessionPersistence

存储/恢复会话日志

SQLite/JSONL/内存

所有会话的存储

上下文压缩

ctx.compaction

压缩策略、摘要生成

basic/自定义

长对话的上下文管理

代码执行

ctx.codeRuntime

Code Mode的VM环境

Worker Thread VM

Code Mode代码执行

凭证管理

ctx.credentials

加密存储、脱敏引用

本地加密文件

全部API密钥管理

2.1 Seam的传递性:一次替换,全家迁移

Capability Seam不只是策略模式,它有一个策略模式没有的关键特性——传递性。文件系统(ctx.fs)和子进程(ctx.subprocess)共享同一个执行世界。当你把两者都指向远程沙箱时:

替换前(本地执行):

  ctx.fs → fs-local

  ctx.subprocess → subprocess-local

     ├─ bash工具 → 本地执行

     ├─ PTY工具  → 本地执行

     └─ LSP工具  → 本地执行

替换后(远程沙箱执行):

  ctx.fs → fs-e2b (远程)

  ctx.subprocess → subprocess-e2b (远程)

     ├─ bash工具 → 自动迁移到远程 ✓

     ├─ PTY工具  → 自动迁移到远程 ✓

     └─ LSP工具  → 自动迁移到远程 ✓

Bash、PTY、LSP——所有依赖ctx.subprocess的组件——全部自动迁移到远程执行,不需要逐个修改。这就像换了一个房子的地基,所有建在上面的房间自动跟着迁移。每个Provider都声明自己属于哪个执行世界,Consumer不关心Provider是谁,只关心它提供的接口。

2.2 子Agent Seam:万能遥控器

ctx.subagents是最能体现Seam价值的例子。这个接口定义了一个子Agent应该能做什么(接收任务、返回结果),但背后的Provider可以完全不同。

Agent Provider

实现方式

通信方式

适用场景

Spawn-in-process

进程内新建独立Agent

函数调用

边界清晰的并行任务

Fork-in-process

从当前会话分支,继承历史

函数调用

需要上下文延续的分支任务

ACP协议

通过Agent Client Protocol对接

网络协议

跨语言/跨进程Agent协作

Claude Code

把Claude Code当子Agent

进程通信

利用Claude Code的编码能力

Codex

把Codex当子Agent

进程通信

利用Codex的代码生成能力

上层编排逻辑(如workflow工具)只依赖ctx.subagents接口,完全不感知底层是谁在执行。你今天用Claude Code做子Agent,明天换成Codex,编排代码一行不改。

三、8个生命周期钩子:Agent的每一帧都可以插手

dsh把Agent的一呼一吸都暴露为事件,你可以在任何时候插一脚。8个核心钩子覆盖了从模型请求到工具执行的完整生命周期。

钩子事件

模式

触发时机

能做什么

类比

agent/pre-step

waterfall

模型即将看到输入前

改写、拒绝或放行本次输入

红绿灯——允许/禁止/改道

agent/request

waterfall

模型请求发出前

检查/修改请求内容

安检——检查/修改请求

agent/request-error

waterfall

模型请求失败后

重试或换路线

拖车服务——故障恢复

agent/turn-stopping

waterfall

一个Turn结束前

决定是否继续跑下一圈

终点检查站——是否继续

tools/pre-execute

waterfall

工具执行前

允许/拒绝/需审批

门禁——安全检查

tools/execute

waterfall

包裹工具执行

超时/重试/指标打点

计时器——执行监控

tools/post-execute

waterfall

工具执行后

结果改写/阻止/添加上下文

质检——结果审查

tools/result

emit

最终结果确定后

审计/日志(只观察不改写)

监控摄像头——事后记录

3.1 Waterfall钩子的工作原理

waterfall模式的核心是next()机制。每个监听器收到(args, next),调用next()就把控制权交给下一个监听器,不调用就短路返回自己的结果。

tools/pre-execute 链的执行流程:

  监听器1(Hook规则)

    │ 检查hook配置

    ├─ deny → 直接返回deny结果,链终止

    └─ allow → 调用next()

         ↓

  监听器2(权限策略)

    │ 检查用户权限

    ├─ deny → 直接返回deny结果,链终止

    └─ allow → 调用next()

         ↓

  监听器3(沙箱策略)

    │ 检查沙箱规则

    └─ allow → 调用next()

         ↓

  到达链尾 → 进入tools/execute执行

单调守卫(ctx.tools.guard)是另一个关键设计:多个策略叠加时,后加的插件不应把前面的拒绝重新改成允许。guard的deny是终态的,后面的监听器无法撤销。这防止了一个设置顺序就把安全策略变成偶然行为。

3.2 Code Mode也无法绕过权限

Code Mode中模型生成的TypeScript代码调用工具时,每个子调用都会记录tool/code-dispatch事件,然后完整走一遍三道关卡(pre-execute → execute → post-execute)。Denial会变成代码中的异常——模型写的代码无法绕过权限模型。

四、三层工具执行管道:安全是构建出来的

把工具调用想象成过海关,dsh设了三道关卡。每一关有明确的职责边界,选错关卡会导致安全漏洞。

模型输出 tool-call: bash rm -rf /

    │

    ├─ 第一关:tools/pre-execute (waterfall)

    │  安检员A (Hook规则): 检查hook配置...

    │  安检员B (权限策略): 没有root权限,拒绝!

    │  └─ 单调守卫 (ctx.tools.guard): 最终裁决,不可上诉

    │

    ├─ 第二关:tools/execute (waterfall)

    │  计时员: 给你30秒,超时就终止

    │  重试员: 失败了?再试一次

    │  └─ 实际 execute() 函数体

    │

    ├─ 第三关:tools/post-execute (waterfall)

    │  质检员: 结果太长,截断到10000字符

    │  上下文注入: 顺便告诉模型,这个文件已经改了

    │

    └─ 监控摄像头:tools/result (同步emit)

       记录到审计日志,不可篡改

你要做什么

应该挂在哪一关

为什么

权限检查、审批、沙箱拦截

tools/pre-execute

可以在执行前阻止,监听器可按优先级排序

不可被覆盖的最终否决

ctx.tools.guard()

单调守卫,后面的监听器无法撤销deny

超时控制、重试、指标打点

tools/execute

包裹执行,能访问exec.signal取消令牌

改写结果、添加上下文

tools/post-execute

此时结果已产生但未最终确定

审计、日志、遥测

tools/result

此时结果是不可变的,只能观察不能修改

五、事件源会话日志:dsh最精妙的设计

5.1 把Agent的一生写成Git提交记录

dsh最核心的数据结构是一个仅追加的会话事件日志。把它想象成Git——Git把每一次代码变更存成commit,dsh把Agent的每一次呼吸存成event。核心原则:Model-visible iff Logged。模型能看到的一切,都必须能从日志中完整重建。这不是文档里的约定——代码里硬编码了断言,违反就崩溃。

Session 日志(就像 git log):

 [seq 0] turn/start         → 新一轮对话开始

 [seq 1] step/start         → 开始一次模型调用

 [seq 2] user/message       → 用户说了什么

 [seq 3] assistant/chunk    → 模型吐了一个token 我

 [seq 4] assistant/chunk    → 模型又吐了一个token 来

 [seq 5] assistant/message  → 模型完整回复 + token消耗

 [seq 6] tool/call          → 模型决定调用bash工具

 [seq 7] tool/result        → bash返回了结果

 [seq 8] step/end           → 这次模型调用结束

 [seq 9] turn/end           → 这轮对话结束

事件域

是否持久化

用途

重建角色

turn/*、step/*

重建任务与步骤边界

控制流骨架

user/message、assistant/*

恢复模型可见对话与流式输出

对话内容

tool/call、tool/result

重放工具轨迹、校验调用结果配对

工具交互

agent/*

否(实时)

inbox、状态、中途引导、重试和续跑

运行时状态

compaction/*

追踪压缩操作,原始事实不变

上下文管理

fs/*、tools/*等能力事件

按事件定义

把策略挂在能力边界上

扩展点

5.2 为什么这个设计是神来之笔

  • 调试体验质变:Agent任务失败了,不需要猜模型当时看到了什么——日志就是完整的行车记录仪,每一帧都有。Trajectory视图可以按来源查看每一次上下文注入
  • 时光机能力:因为模型历史是从日志派生出来的(deriveMessages()),而不是另外存储的,你可以回放(从任意时间点重放,做A/B测试)、分叉(从任意事件点分叉出新会话,相当于git checkout -b)、恢复(崩溃后从日志重建,精确到最后一个token)
  • 插件之间零耦合的共享数据层:UI渲染、Hook桥接、遥测导出、审计——都是ctx.on('session/event', ...)的监听器,各自消费同一份日志,互不干扰。这就像Linux的/dev目录——一切皆文件让所有工具能协同工作
  • 日志自带版本控制:插件可以通过TypeScript声明合并向SessionEventMap添加新事件类型。SESSION_FORMAT_VERSION机制确保:旧版本遇到不认识的事件类型会拒绝加载(除非事件标记了ignorable: true),防止静默数据损坏

5.3 Surface机制:压缩不改原始历史

日志中的消息事件(user/message、assistant/message、tool/result)携带SurfaceOp元数据,声明它如何进入模型的可见消息列表:

  • append:正常追加到末尾——99%的情况
  • { op: 'replace', start, end }:替换一段消息——用于Compaction把旧消息替换成摘要

当对话长了需要压缩,Compaction插件不是修改原始日志,而是追加一个带replace标记的新事件。模型看到的是压缩后的历史,但原始数据完整保留。这就像Photoshop的图层——你不破坏原始图片,只是在上面叠加了新的图层。

六、Profile/Bundle/Patch:三层配置层叠模型

6.1 配置即组装

dsh的插件组合不是写死在代码里的,而是通过YAML配置文件声明。这非常像Docker Compose——你声明需要哪些服务,框架负责启动和管理。

cordis.yml —— 就像 docker-compose.yml

plugins:

  dsh-llm-deepseek:              # 模型服务

    config:

      apiKey: !!js process.env.DEEPSEEK_API_KEY

  dsh-tool-bash:                 # Shell工具

  dsh-tool-fs:                   # 文件工具

  dsh-tool-web:                  # Web搜索工具

  dsh-session-persistence-sqlite: # 持久化

6.2 分层覆盖:像CSS一样的层叠

配置不是扁平的一层,而是像CSS一样有层叠优先级。每层都是对前一层的增量补丁,而非整体替换。

第1层:bundle默认配置(官方预置的插件组合)

    ↑ 叠加

第2层:Profile的cordis.patch.yml(你的自定义覆盖)

    ↑ 叠加

第3层:Home级别的patch(全局用户偏好)

    ↑ 叠加

第4层:--patch命令行参数(临时覆盖,优先级最高)

配置层

位置

作用

热重载

Bundle默认配置

node_modules内的cordis.patch.yml

官方预置的能力组合

Profile补丁

~/.dsh/profiles/<name>/cordis.patch.yml

当前profile的自定义覆盖

支持

Home全局补丁

~/.dsh/cordis.patch.yml

所有profile共享的全局设置

支持

命令行--patch

启动参数指定的文件

本次启动的临时覆盖

不适用

比如官方默认用SQLite做持久化,你一个patch就能换成JSONL:

# cordis.patch.yml

plugins:

  dsh-session-persistence-sqlite:

    disabled: true

  dsh-session-persistence-jsonl:

    config:

      path: ~/my-sessions/

6.3 Profile vs Agent Preset:两个组合层级

dsh中有两个容易混淆的组合概念,分属不同层级。简单理解:Profile选底座装哪些能力包,Preset选这个Agent用哪套面向模型的工具组合。二者正交,一个Profile下可以有多个Preset供不同agent选用。

维度

Profile

Agent Preset

层级

Launcher级(进程启动时选定)

Agent会话级(每个agent可独立选择)

位置

~/.dsh/profiles/<name>/

node_modules内或~/.dsh/.agent-presets/<name>/

声明

package.json的dsh.profile.bundles

preset目录内含agent.cordis.yml + preset.yml

作用

决定整个进程加载哪些bundle层

决定单个agent面向模型的插件集合

切换方式

dsh --profile <name>(重启生效)

会话创建时指定,运行中可recompose()热切换

内置选项

web / headless

standard / code / minimal / cordis

七、Python实战:dsh插件系统模拟器与依赖分析器

为了更直观地理解Cordis的插件系统,以下Python代码实现了一个简化版的插件系统模拟器,包含服务注册、依赖注入、Fiber生命周期和事件分发。可以用来做插件依赖分析和加载顺序验证。

7.1 简化版Cordis插件系统模拟器

from collections import defaultdict

from enum import Enum

from dataclasses import dataclass, field

from typing import Callable, Optional, List, Dict, Any

import networkx as nx

class FiberState(Enum):

    PENDING = "pending"

    LOADING = "loading"

    ACTIVE = "active"

    FAILED = "failed"

    UNLOADING = "unloading"

    DISPOSED = "disposed"

@dataclass

class Fiber:

    """插件生命周期状态机"""

    plugin_name: str

    state: FiberState = FiberState.PENDING

    inject: List[str] = field(default_factory=list)

    apply_fn: Optional[Callable] = None

    disposers: List[Callable] = field(default_factory=list)

    error: Optional[str] = None

class Context:

    """简化版Cordis上下文——服务容器 + 事件总线"""

    def __init__(self):

        self._services = {}

        self._event_listeners = defaultdict(list)

        self._fibers = {}

        self._effect_stack = []

    def provide(self, key: str, service: Any) -> Callable:

        """注册服务,返回disposer"""

        self._services[key] = service

        def dispose():

            if key in self._services:

                del self._services[key]

        return dispose

    def has(self, key: str) -> bool:

        return key in self._services

    def __getattr__(self, key):

        if key.startswith("_"):

            return super().__getattr__(key)

        if key in self._services:

            return self._services[key]

        raise AttributeError(f"Service '{key}' not found in context")

    def on(self, event: str, listener: Callable, priority: int = 0):

        """注册事件监听器,返回disposer"""

        self._event_listeners[event].append({"listener": listener, "priority": priority})

        self._event_listeners[event].sort(key=lambda x: -x["priority"])

        def dispose():

            self._event_listeners[event] = [

                l for l in self._event_listeners[event] if l["listener"] is not listener

            ]

        return dispose

    async def waterfall(self, event: str, initial: Any = None) -> Any:

        """Waterfall模式事件分发——顺序传递,可短路"""

        value = initial

        for entry in self._event_listeners.get(event, []):

            result = await entry["listener"](value, lambda v=None: v)

            if result is not None:

                value = result

        return value

    def emit(self, event: str, payload: Any = None):

        """Emit模式事件分发——广播通知"""

        for entry in self._event_listeners.get(event, []):

            entry["listener"](payload)

    def effect(self, setup_fn: Callable) -> None:

        """声明副作用,setup_fn返回cleanup函数"""

        cleanup = setup_fn()

        if cleanup and callable(cleanup):

            self._effect_stack.append(cleanup)

    def register_plugin(self, name: str, inject: List[str], apply_fn: Callable) -> Fiber:

        """注册一个插件"""

        fiber = Fiber(

            plugin_name=name,

            inject=inject,

            apply_fn=apply_fn,

        )

        self._fibers[name] = fiber

        return fiber

    def try_activate(self, name: str) -> bool:

        """尝试激活一个插件——所有依赖就绪才加载"""

        fiber = self._fibers[name]

        if fiber.state != FiberState.PENDING:

            return False

        # 检查所有依赖是否就绪

        for dep in fiber.inject:

            if not self.has(dep):

                return False

        # 依赖就绪,进入LOADING

        fiber.state = FiberState.LOADING

        try:

            # 压入effect栈,用于收集cleanup

            prev_effects = len(self._effect_stack)

            fiber.apply_fn(self)

            # 保存本次effect产生的disposers

            fiber.disposers = self._effect_stack[prev_effects:]

            fiber.state = FiberState.ACTIVE

            print(f"  [activate] {name} → ACTIVE")

            return True

        except Exception as e:

            fiber.state = FiberState.FAILED

            fiber.error = str(e)

            print(f"  [error] {name} → FAILED: {e}")

            return False

    def activate_all(self):

        """拓扑排序激活所有插件"""

        changed = True

        round_num = 0

        while changed:

            changed = False

            round_num += 1

            print(f"\nActivation round {round_num}:")

            for name in list(self._fibers.keys()):

                if self._fibers[name].state == FiberState.PENDING:

                    if self.try_activate(name):

                        changed = True

        # 检查是否有未激活的插件

        pending = [n for n, f in self._fibers.items() if f.state == FiberState.PENDING]

        if pending:

            print(f"\n[warn] {len(pending)} plugins stuck in PENDING: {pending}")

    def build_dependency_graph(self) -> nx.DiGraph:

        """构建插件依赖图"""

        G = nx.DiGraph()

        for name, fiber in self._fibers.items():

            G.add_node(name, state=fiber.state.value)

            for dep in fiber.inject:

                # 找到提供这个服务的插件

                for provider_name, provider_fiber in self._fibers.items():

                    if provider_fiber.state == FiberState.ACTIVE:

                        pass  # 已激活的就是provider

        return G

    def status_report(self) -> dict:

        """生成插件状态报告"""

        report = {"active": [], "pending": [], "failed": [], "services": list(self._services.keys())}

        for name, fiber in self._fibers.items():

            if fiber.state == FiberState.ACTIVE:

                report["active"].append(name)

            elif fiber.state == FiberState.PENDING:

                report["pending"].append({"name": name, "waiting_for": fiber.inject})

            elif fiber.state == FiberState.FAILED:

                report["failed"].append({"name": name, "error": fiber.error})

        return report

这段代码实现了Cordis插件系统的核心机制:服务注册与发现、依赖驱动的自动激活(拓扑排序)、Fiber生命周期状态机、两种事件分发模式(waterfall和emit)、可逆副作用(effect + cleanup)。你可以用它来模拟和验证dsh的插件加载顺序,理解为什么某个插件会卡在PENDING状态。

7.2 工具执行管道模拟

import asyncio

from dataclasses import dataclass

from typing import Any, List, Callable, Optional

@dataclass

class ToolExecution:

    tool_name: str

    args: dict

    status: str = "pending"  # pending/denied/executing/done/error

    result: Any = None

    error: Optional[str] = None

    deny_reason: Optional[str] = None

    duration_ms: float = 0.0

class ToolExecutionPipeline:

    """模拟dsh的三层工具执行管道"""

    def __init__(self):

        self.pre_execute_hooks = []   # 第一关:准入检查

        self.execute_wrappers = []    # 第二关:执行包裹

        self.post_execute_hooks = []  # 第三关:结果处理

        self.result_listeners = []    # 监控:只观察

        self._guard_denied = False

    def add_pre_execute(self, hook: Callable, priority: int = 0):

        """添加pre-execute钩子(waterfall,可deny)"""

        self.pre_execute_hooks.append({"hook": hook, "priority": priority})

        self.pre_execute_hooks.sort(key=lambda x: -x["priority"])

    def guard(self, check_fn: Callable):

        """单调守卫——只能deny不能allow,后面的hook无法撤销"""

        def guard_hook(exec, _next):

            result = check_fn(exec)

            if result == "deny":

                self._guard_denied = True

                exec.status = "denied"

                exec.deny_reason = "Guard denied"

                return exec

            return _next()

        self.add_pre_execute(guard_hook, priority=9999)

    def add_execute_wrapper(self, wrapper: Callable):

        """添加execute包裹器(超时、重试、指标)"""

        self.execute_wrappers.append(wrapper)

    def add_post_execute(self, hook: Callable):

        """添加post-execute钩子(结果改写)"""

        self.post_execute_hooks.append(hook)

    def add_result_listener(self, listener: Callable):

        """添加结果监听器(只观察)"""

        self.result_listeners.append(listener)

    async def execute(self, tool_name: str, args: dict, exec_fn: Callable) -> ToolExecution:

        """执行完整的三层管道"""

        import time

        exec_obj = ToolExecution(tool_name=tool_name, args=args)

        # 第一关:pre-execute waterfall

        for entry in self.pre_execute_hooks:

            if exec_obj.status == "denied":

                break

            result = await entry["hook"](exec_obj, lambda: None)

            if isinstance(result, ToolExecution):

                exec_obj = result

                if exec_obj.status == "denied":

                    break

        if exec_obj.status == "denied":

            for listener in self.result_listeners:

                listener(exec_obj)

            return exec_obj

        # 第二关:execute包裹器 + 实际执行

        exec_obj.status = "executing"

        t0 = time.time()

        try:

            # 从内到外包裹执行函数

            fn = exec_fn

            for wrapper in reversed(self.execute_wrappers):

                fn = lambda f=fn, w=wrapper: w(f)

            exec_obj.result = await fn(args)

            exec_obj.status = "done"

        except Exception as e:

            exec_obj.error = str(e)

            exec_obj.status = "error"

        finally:

            exec_obj.duration_ms = (time.time() - t0) * 1000

        # 第三关:post-execute waterfall

        for hook in self.post_execute_hooks:

            exec_obj = await hook(exec_obj)

        # 通知监听器(结果不可变)

        for listener in self.result_listeners:

            listener(exec_obj)

        return exec_obj

工具执行管道模拟器完整复现了dsh的三层设计:pre-execute准入检查(含单调守卫)、execute包裹执行(超时/重试)、post-execute结果处理(改写/添加上下文),以及只观察不修改的result事件。你可以用它来验证企业安全策略的组合效果,确保多层Hook叠加后不会出现安全漏洞。

八、Creator模式:Agent能给自己装插件

四种运行模式中,Creator模式是最有想象力的一个。它做的事情很简单:让Agent能检查、试验和修改自己的插件树。

模式

Agent能做什么

本质

适用场景

Standard

调用开发者预置的工具

固定功能集

日常编码任务

Code Mode

生成TS代码编排多轮工具调用

程序式编排

复杂多步骤任务

Minimal

仅Shell+Editor两工具

最小基准环境

模型能力评测

Creator

检查/试验/修改自己的插件树

自扩展Agent

自定义Agent预设、插件开发

传统工具:你能用的功能 = 开发者写死的。Creator模式:Agent可以检查运行时配置、在内存中试验新插件、把试验成功的组合保存为新的Agent Preset。这意味着Agent可以在运行中扩展自己的能力——不是通过调用更多工具,而是通过安装新插件来改变自己的运行时结构。

九、插件开发实践:三种扩展方式

扩展方式

复杂度

适用场景

核心API

自动清理

注册工具

低(5分钟)

给Agent添加新能力

ctx.tools.register(defineTool(...))

Hook插件

低(10行代码)

权限门禁、策略拦截

ctx.on('tools/pre-execute', ...)

模型适配器

接入新的模型服务

ctx.llm.registerAdapter(...)

能力Seam

替换整个执行环境

ctx.provide('fs', ...) + inject

Profile/Bundle

分发能力组合

cordis.patch.yml + package.json

N/A

9.1 开发工具的三个你不需要操心的事

  • 参数校验自动完成:defineTool声明的parameters schema会自动校验,类型不匹配直接报错
  • Code Mode自动可用:注册的工具自动在Code Mode的SDK中可用,不需要额外适配
  • 卸载时自动清理:通过ctx.effect()注册的内容,插件卸载时自动执行dispose

9.2 模型适配器的核心职责

模型适配器本质上是一个翻译器——把OpenAI格式、Anthropic格式、自研格式的流式响应,统一翻译成dsh内部的StreamChunk序列。有两个关键规矩:usage在finish之前,finish之后不产生任何输出;Tool-call的arguments必须保持原始JSON字符串,不能解析——这是为了保持日志的lossless JSON特性。

十、不存在特权内核的意义与挑战

10.1 架构优势总结

优势

具体体现

业务价值

零侵入扩展

不需要修改源码,通过patch即可替换任意组件

企业定制成本大幅降低

依赖驱动激活

inject声明自动管理加载顺序

减少配置错误,提升可靠性

可逆副作用

所有注册都有cleanup,卸载不留痕迹

热重载、动态切换能力

传递性替换

换一个Provider,所有Consumer自动迁移

执行环境切换成本极低

事件源可审计

模型可见即已记录,完整可回放

企业级合规和可观测性

生态可扩展

第三方插件通过dsh-plugin话题发现和安装

社区共建的生态飞轮

10.2 挑战与风险

挑战

具体风险

缓解策略

学习曲线陡峭

Cordis概念多,插件开发门槛高于普通框架

从工具注册开始,逐步深入

供应链风险

第三方插件可能位于关键路径上

版本锁定、物料清单、权限审查

配置复杂度

多层patch叠加后,排错需要理解加载顺序

--dump-config命令查看最终配置

兼容性风险

开发者预览阶段API可能变化

锁定版本,建立升级回归测试

安全组合风险

插件组合可能导致安全策略被绕过

测试单调守卫行为,审计pre-execute链

性能开销

事件和服务间接调用增加调用栈深度

关键路径上的插件需性能评测

10.3 给企业的插件治理建议

  • 建立插件物料清单(CBOM):记录每个插件的版本、来源、权限声明和依赖关系
  • 分层审批制度:工具级插件低门槛,能力Seam级插件需安全审查,Agent Loop级需架构委员会审批
  • 沙箱强制:生产环境声明所有执行为受限模式,禁止SANDBOX_UNAVAILABLE静默降级
  • 遥测接入:将session事件流接入企业可观测性平台,建立异常检测规则
  • 版本管理:Profile配置纳入Git管理,patch变更走代码审查流程
  • 定期演练:测试插件卸载和回滚能力,验证可逆副作用的完整性

十一、总结

维度

核心要点

Cordis内核

Context服务容器 + Fiber生命周期状态机 + 4种事件分发模式

插件树结构

12个核心能力Seam,每个Seam含Service Definition/Provider/Consumer三角色

传递性替换

换一个Provider,所有依赖它的Consumer自动迁移

生命周期钩子

8个waterfall/emit钩子,覆盖Agent从请求到工具执行的全流程

工具执行管道

三层关卡(pre/execute/post)+ 单调守卫 + result不可变

事件源会话

仅追加SessionEvent日志,Model-visible iff Logged,支持回放/分叉/恢复

分层配置

Bundle → Profile → Home → --patch,四层增量覆盖,像CSS一样层叠

Creator模式

Agent能检查、试验和修改自己的插件树,实现自扩展

扩展方式

从工具注册(5分钟)到能力Seam(高复杂度),五种扩展路径

治理建议

CBOM物料清单 + 分层审批 + 沙箱强制 + 遥测接入 + 版本管理

DeepSeek Harness的插件系统不只是一个可扩展框架——它是对Agent工程本质的重新思考。传统框架中,核心模块是神圣不可修改的,扩展只能在预设的扩展点完成。dsh把这个前提彻底推翻:Agent Loop本身就是普通插件,会话日志是可替换的服务,工具执行管道是可叠加的waterfall链。没有特权内核,一切皆插件,只通过服务和事件通信。

这种设计的代价是学习曲线和复杂度——你需要理解Seam、inject、waterfall、SurfaceOp、Profile/Bundle/Patch等一整套概念。但收益是根本性的:当企业需要定制Agent时,不需要fork整个项目再维护一个渐行渐远的分支,只需要写一个插件、加一个patch。这就是Everything is a Plugin的真正含义——不是功能多,而是结构活。

紫宸策 | GEO咨询与企业AI落地实践

公众号:紫宸策

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值