上一篇我们从架构层面介绍了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落地实践
公众号:紫宸策

323

被折叠的 条评论
为什么被折叠?



