docs(analysis): add plugin-system analysis notes
Structured walkthrough of the dsh plugin system across seven layers:
Cordis framework internals, boot composition (cordis.yml/profile/bundle/
patch), the capability-seam three-role pattern (traced via shell),
the tool registry and execution pipeline, a real plugin anatomy
(todo_write), cross-cutting invariants, and a source file index.
Analysis-only notes under analysis/, not part of the gated docs/
pipeline. Snapshot base: abe560f81e (0.1.0-rc.5).
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# 01 - Cordis 框架底座
|
||||
|
||||
Cordis 是 dsh 之下的 vendored 插件框架。本篇解析它的四个核心机制:Context 代理、Service、Fiber 生命周期、五种事件分派。源码位于 `vendor/cordis/src/`。
|
||||
|
||||
## 一、Context 是一个代理
|
||||
|
||||
Cordis 最关键的设计是 **`Context` 是一个 Proxy**。`ctx.tools`、`ctx.llm`、`ctx.sessions` 这些属性读取不走普通属性查找,而是走 `ReflectService.handler.get` 陷阱(`vendor/cordis/src/reflect.ts:135-171`):
|
||||
|
||||
```ts
|
||||
get: (target, prop, ctx) => {
|
||||
if (isSpecialProperty(prop)) return Reflect.get(target, prop, ctx)
|
||||
if (Reflect.has(target, prop)) return getTraceable(ctx, Reflect.get(target, prop, ctx))
|
||||
// 否则:通过 waterfall 在 fiber 链上解析服务实现
|
||||
return ctx.events.waterfall('internal/get', ctx, prop, error, () => {
|
||||
let fiber = ctx.fiber
|
||||
while (true) {
|
||||
const impl = fiber.store?.[prop]
|
||||
if (impl) return getTraceable(ctx, impl.value) // 找到服务实现
|
||||
if (prop in fiber.inject) throw error // 依赖未就绪
|
||||
fiber = fiber.parent.fiber // 沿父链向上查找
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
这意味着 `ctx.<key>` 是**按名查找服务**,而非导入具体实现。所有插件通过 `ctx.tools`、`ctx.llm` 这样的稳定 key 互相发现,不直接 import 对方的实现类--这是「可替换性」的根基。
|
||||
|
||||
### 三种作用域操作
|
||||
|
||||
Context 提供三种作用域操作(`vendor/cordis/src/context.ts:99-145`):
|
||||
|
||||
- `extend(meta)` - 创建原型继承的子 context,不修改父级
|
||||
- `isolate(name, label)` - 为某个服务名开独立作用域(不同 agent 可以有各自的 `ctx.tools`)
|
||||
- `intercept(name, config)` - 为某服务注入拦截配置(祖先优先级最低)
|
||||
|
||||
`isolate` 是实现「一个 agent 一套能力集」的关键:同一个服务名在不同隔离作用域里可以指向不同实现。
|
||||
|
||||
## 二、Service:插件如何挂载到 ctx
|
||||
|
||||
`Service` 是暴露命名 API 的基类(`vendor/cordis/src/service.ts`)。子类构造时调用 `super(ctx, name)`,立即注册:
|
||||
|
||||
```ts
|
||||
constructor(protected ctx: Context, name: string) {
|
||||
// ...
|
||||
self.ctx.reflect.provide(name, self, this[Service.check]) // 注册到 reflect store
|
||||
return self
|
||||
}
|
||||
```
|
||||
|
||||
但**绝大多数 dsh 插件不是 Service 子类**,而是更轻量的三种入口形态之一(`vendor/cordis/src/registry.ts:92-134`):
|
||||
|
||||
| 形态 | 签名 | 用途 |
|
||||
|------|------|------|
|
||||
| 函数插件 | `(ctx, config) => any` | 最常见,如 `todo_write` |
|
||||
| 类插件 | `new (ctx, config)` | 需要状态的服务/Provider,如 `LocalBashExecutor` |
|
||||
| 对象插件 | `{ apply(ctx, config) }` | 带 `inject`/`Config` 等元数据 |
|
||||
|
||||
每个插件可声明静态元数据(`registry.ts:99-111`):
|
||||
|
||||
- `name` - 显示名
|
||||
- `inject` - **依赖声明**(所需服务列表)
|
||||
- `Config` - standard-schema 配置校验器
|
||||
- `provide` - 提供的服务名
|
||||
|
||||
### 依赖注入是声明式而非手工排序
|
||||
|
||||
`inject` 表达加载顺序:声明了 `inject` 的插件会**等到所需服务存在后才激活**。`ctx.inject(deps, callback)` 是 `ctx.plugin({inject, apply: callback})` 的简写(`registry.ts:300-302`)。当依赖服务注册/注销时,Cordis 会重新评估该插件的 fiber(见下文 `notify`)。
|
||||
|
||||
## 三、Fiber:生命周期与「注册即可逆 effect」
|
||||
|
||||
**「Registrations are reversible effects」** 是整个系统最重要的运行时保证。`Fiber` 是一个插件的运行实例(`vendor/cordis/src/fiber.ts`),状态机为:
|
||||
|
||||
```
|
||||
PENDING -> LOADING -> ACTIVE -> (UNLOADING) -> DISPOSED
|
||||
↓ FAILED ↗
|
||||
```
|
||||
|
||||
`ctx.effect(execute, label)` 是核心(`fiber.ts:418-561`):`execute` 立即执行,它返回的 **disposer** 被收集起来,在 fiber 卸载时**按注册的逆序**执行。返回的 wrapper 本身也是 disposer,可提前调用:
|
||||
|
||||
```ts
|
||||
effect(execute, label) {
|
||||
// execute 立即运行,collect 收集其产出的 disposer
|
||||
// 卸载时:disposables.splice(0).reverse().forEach(runDisposable)
|
||||
}
|
||||
```
|
||||
|
||||
关键在于:**每一个注册操作都走 effect**。例如注册事件监听器(`events.ts:254-260`):
|
||||
|
||||
```ts
|
||||
register(label, hooks, callback, options) {
|
||||
return this.ctx.fiber.effect(() => {
|
||||
hooks.push({ ctx: this.ctx, callback, ...options })
|
||||
return () => this.unregister(hooks, callback) // disposer
|
||||
}, label)
|
||||
}
|
||||
```
|
||||
|
||||
注册服务(`reflect.ts:277-305`)同理:`provide()` 包在 `ctx.fiber.effect()` 里,返回的 disposer 删除 store 中的实现并通知依赖者。所以:
|
||||
|
||||
- 卸载一个插件 -> 它注册的工具、监听器、服务提供都自动撤销
|
||||
- 依赖它的插件被唤醒重新评估(`notify()`,`reflect.ts:314-336`)
|
||||
|
||||
这就是「热重载/卸载可预测回卷」的机制。disposer 可以是 generator(`yield` 多个),也可以是 async。
|
||||
|
||||
### 依赖驱动的重载(epoch 机制)
|
||||
|
||||
Fiber 用一个 `epoch` 字符串追踪依赖状态。`_refresh()`(`fiber.ts:611-623`)把当前所有 inject 服务的 fiber uid 拼成 epoch:
|
||||
|
||||
```ts
|
||||
_refresh() {
|
||||
let epoch = ''
|
||||
for (const name of Object.keys(this.inject)) {
|
||||
const impl = this._store[name]
|
||||
if (!impl) { epoch = INACTIVE; break } // 任一依赖缺失 -> INACTIVE
|
||||
epoch += ':' + impl.fiber.uid
|
||||
}
|
||||
this._setEpoch(epoch)
|
||||
}
|
||||
```
|
||||
|
||||
epoch 变化触发 `_setEpoch`(`fiber.ts:625-639`):从 INACTIVE 变可用就 `_reload()`(运行插件回调),反之 `_unload()`(运行所有 disposer)。`reload`/`unload` 通过 `inertia` 串行化,避免并发状态机竞态。`_reload` 内部有 epoch 失效检查(`fiber.ts:654`),保证过期的加载不会执行插件代码。
|
||||
|
||||
## 四、事件系统:五种分派模式
|
||||
|
||||
事件是插件间的通信手段,但**分派模式是事件契约的一部分**(`vendor/cordis/src/events.ts` + `docs/cordis-primer.md`):
|
||||
|
||||
| 模式 | await? | 顺序 | 返回值 | 方法 |
|
||||
|------|--------|------|--------|------|
|
||||
| `emit` | 否 | 注册序 | 无 | 同步广播 |
|
||||
| `waterfall` | 否 | 注册序 | 有(around 中间件) | `next()` 委托 |
|
||||
| `parallel` | 是 | 全部并行 | 无 | `Promise.allSettled` |
|
||||
| `serial` | 是 | 注册序,bail 止 | 第一个 bail 值 | 顺序 await |
|
||||
| `bail` | 否 | 注册序 | 第一个 bail 值 | 同步直到 bail |
|
||||
|
||||
### waterfall 是最关键的扩展点
|
||||
|
||||
`waterfall` 是 around 中间件(`events.ts:234-243`):最后一个参数是 `next`,监听器包裹整条链。调用 `next()` 委托给下一个,不调用则**短路否决**:
|
||||
|
||||
```ts
|
||||
waterfall(...args) {
|
||||
const cbs = this.dispatch('waterfall', args)
|
||||
const inner = args.pop() // 内层 next
|
||||
const next = () => { const cb = cbs.shift() ?? inner; return cb(...args) }
|
||||
args.push(next)
|
||||
return next()
|
||||
}
|
||||
```
|
||||
|
||||
dsh 的 `agent/pre-step`、`agent/request`、`llm/stream`、`tools/pre-execute`、`tools/execute`、`tools/post-execute` 都是 waterfall--这决定了策略/拦截/沙箱挂在哪里。协作型监听器通常修改共享请求/决策对象后委托;单决策事件中,拥有决策权的监听器可以不调 `next()` 直接短路。
|
||||
|
||||
### 类型化事件与声明合并
|
||||
|
||||
类型化事件通过 **declaration merging** 声明:
|
||||
|
||||
```ts
|
||||
declare module './context.ts' {
|
||||
export interface Events {
|
||||
'agent/pre-step'(this: Context, msg: Message, next: () => Message): Message
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
JSDoc 用 `@mode` 标注分派模式,生成目录会据此校验声明与分派点一致。作用域 key 若不在 payload 中需要标注 `@dshScopeScan unsupported`。
|
||||
|
||||
### dispatch 与上下文过滤
|
||||
|
||||
`dispatch()`(`events.ts:165-175`)解析某次分派的监听器并应用 context 过滤:
|
||||
|
||||
```ts
|
||||
dispatch(type, args) {
|
||||
const thisArg = typeof args[0] === 'object' ? args.shift() : null
|
||||
const name = args.shift()
|
||||
this.emit('internal/dispatch', type, name, args, thisArg) // 诊断事件
|
||||
const filter = thisArg?.[Context.filter]
|
||||
return (this._hooks[name] || [])
|
||||
.filter(hook => hook.global || !filter || filter.call(thisArg, hook.ctx))
|
||||
.map(hook => hook.callback.bind(thisArg))
|
||||
}
|
||||
```
|
||||
|
||||
监听器记录在 fiber 上(`on()` -> `register()` -> `fiber.effect()`),所以随 fiber 卸载自动移除。`global: true` 绕过 filter;`prepend: true` 插到队列前。
|
||||
|
||||
## 关键源码索引
|
||||
|
||||
| 主题 | 文件 |
|
||||
|------|------|
|
||||
| Context 类 + 三作用域 | `vendor/cordis/src/context.ts:42-146` |
|
||||
| Context 代理陷阱 | `vendor/cordis/src/reflect.ts:135-206` |
|
||||
| Service 基类 | `vendor/cordis/src/service.ts:1-115` |
|
||||
| 插件形态 + inject 元数据 | `vendor/cordis/src/registry.ts:92-187` |
|
||||
| `ctx.plugin()` / `ctx.inject()` | `vendor/cordis/src/registry.ts:300-336` |
|
||||
| Fiber 状态机 | `vendor/cordis/src/fiber.ts:147-154` |
|
||||
| `ctx.effect()` | `vendor/cordis/src/fiber.ts:418-561` |
|
||||
| epoch 重载机制 | `vendor/cordis/src/fiber.ts:611-696` |
|
||||
| 五种分派模式 | `vendor/cordis/src/events.ts:131-243` |
|
||||
| `ctx.on()` 注册 | `vendor/cordis/src/events.ts:288-302` |
|
||||
| `provide()` 可逆注册 | `vendor/cordis/src/reflect.ts:277-305` |
|
||||
| 依赖通知 `notify()` | `vendor/cordis/src/reflect.ts:314-336` |
|
||||
@@ -0,0 +1,238 @@
|
||||
# 02 - 启动组合层
|
||||
|
||||
这是 dsh 在 Cordis 之上加的**配置组合层**。运行中的 dsh 是一棵从有序层在启动时组合出的插件树。本篇解析 `cordis.yml` 行格式、`!!js` 延迟求值、profile/bundle/patch 四层叠加、以及 patch 算法。
|
||||
|
||||
源码:`vendor/loader/`、`vendor/include/`、`packages/boot/app-boot/`、`apps/cli/`。
|
||||
|
||||
## 一、cordis.yml 的行格式
|
||||
|
||||
每个插件是顶层 YAML 数组的一行,`EntryOptions`(`vendor/loader/src/config/entry.ts:8-22`):
|
||||
|
||||
```yaml
|
||||
- id: timer # 稳定 id,供 patch 定位
|
||||
name: '@deepseek-ai/cordis-plugin-timer' # 模块说明符
|
||||
config: # 传给插件的配置
|
||||
root: ['.']
|
||||
inject: [...] # 依赖声明
|
||||
disabled: false # 禁用整行 + 后代
|
||||
group: false # 标记嵌套入口组
|
||||
```
|
||||
|
||||
真实示例(`packages/bundle/base/cordis.patch.yml:16-22`):
|
||||
|
||||
```yaml
|
||||
- id: timer
|
||||
name: '@deepseek-ai/cordis-plugin-timer'
|
||||
|
||||
- id: hmr
|
||||
name: '@deepseek-ai/cordis-plugin-hmr'
|
||||
config:
|
||||
root: ['.']
|
||||
```
|
||||
|
||||
## 二、`!!js`:延迟求值的表达式节点
|
||||
|
||||
`!!js` 是自定义 YAML 标签,把值解析成 `{__jsExpr: "..."}`,**挂载时**才求值,而非解析时。注册在 `vendor/include/src/index.ts:9-23`:
|
||||
|
||||
```ts
|
||||
const JsExpr = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
resolve: (data) => typeof data === 'string',
|
||||
construct: (data) => ({ __jsExpr: data }),
|
||||
predicate: isJsExpr,
|
||||
represent: (data) => data['__jsExpr'],
|
||||
})
|
||||
export const entryListSchema = yaml.JSON_SCHEMA.extend(JsExpr)
|
||||
```
|
||||
|
||||
求值器是 `with(ctx){ return eval(expr) }` 动态函数(`vendor/loader/src/config/utils.ts:5-9`),表达式能看到 entry 的 fiber 上下文。两处使用:
|
||||
|
||||
1. **`disabled` 字段**(`entry.ts:104-108`)- 对 entry 自己的上下文求值:
|
||||
```ts
|
||||
private disabledOf(options: EntryOptions): boolean {
|
||||
return isJsExpr(options.disabled)
|
||||
? Boolean(this.evaluate(options.disabled.__jsExpr))
|
||||
: Boolean(options.disabled)
|
||||
}
|
||||
```
|
||||
用法:`disabled: !!js process.platform === 'win32'`
|
||||
|
||||
2. **配置值** - Loader 注册全局 `internal/config` hook(`loader/src/index.ts:92-101`),对每个非 tree-carrier entry 的 config 调 `interpolate(ctx, config)`,递归把每个 `__jsExpr` 节点替换成 `evaluate(ctx, expr)` 结果。
|
||||
|
||||
boot 把 `dshHomePath` 提供进该作用域(`app-boot/src/index.ts:770`:`ctx.provide('dshHomePath', dshHomePath)`),`ctx` 本身也在作用域内,所以 `task: !!js ctx.headlessStartup.task` 成立(见 `packages/bundle/headless/cordis.patch.yml:35`)。
|
||||
|
||||
**Tree carrier(`Group`、`Include`)豁免插值** - `loader/src/index.ts:98-100` 检查 `plugin?.[EntryGroup.key]`,对其 config 原样返回,因为它**包含**其他行自己的 `!!js` 表达式,那些属于各自行的 fiber。
|
||||
|
||||
### dump-config 时 `!!js` 原样打印
|
||||
|
||||
`dsh --profile web --dump-config` 走 `renderConfigDump()`(`app-boot/src/index.ts:379-442`),解析用同一个 `entryListSchema`,所以 `__jsExpr` 往返回 `!!js` **不执行**。这是为什么 dump 等于实际挂载的配置,却不运行任何代码。
|
||||
|
||||
## 三、挂载序列
|
||||
|
||||
`boot()`(`packages/boot/app-boot/src/index.ts:757-802`):
|
||||
|
||||
1. 创建根 `Context`,设置 `baseUrl` 为配置目录
|
||||
2. `ctx.provide('dshHomePath', dshHomePath)` - 把 home-path 解析器暴露给 `!!js`
|
||||
3. `await ctx.plugin(Loader)` - 安装 Loader 服务
|
||||
4. 运行可选 `prepare` hook(host 在任何 tree entry 挂载前的设置)
|
||||
5. `await mountRootInclude(ctx, absoluteConfigPath, patches, bareModuleBaseUrl)`
|
||||
|
||||
`mountRootInclude()`(`app-boot/src/index.ts:486-529`):
|
||||
|
||||
- 注册 `cordis:include`(可选 host 解析的子类,把裸名锚定到安装)和 `cordis:group` 为 Loader 内建
|
||||
- 创建一个根 entry,pin 住 id:
|
||||
```ts
|
||||
const rootInclude: EntryOptions = {
|
||||
id: 'include',
|
||||
name: 'cordis:include',
|
||||
config: { path: pathToFileURL(absoluteConfigPath).href, ...patches.length > 0 ? { patches: [...patches] } : {} },
|
||||
}
|
||||
```
|
||||
- `ctx.loader.create(rootInclude)` 挂载它;Include 插件读文件、应用 patches、调 `root.update(data)`
|
||||
|
||||
`EntryGroup.update()`(`vendor/loader/src/config/group.ts:59-106`)事务式协调新旧列表:用 `Promise.allSettled` 创建每个 `Entry`,移除孤儿 id,失败回滚。每个 `Entry.init()`(`entry.ts:259-289`)通过 `EntryTree.import()`(`tree.ts:145-162`)导入模块--处理 `cordis:` 内建、Node 内部模块 loader、相对说明符、裸包名--然后 `ctx.registry.plugin(plugin, config)` 启动。
|
||||
|
||||
## 四、profiles 和 bundles 组合
|
||||
|
||||
### `dsh.bundle` vs `dsh.profile`(package.json 的 dsh 字段)
|
||||
|
||||
`package.json` 的 `dsh` 节有两个半(`packages/boot/app-boot/src/profile.ts:42-62`):
|
||||
|
||||
- **`dsh.bundle`**(`DshBundleManifest`,bundle 包声明):一个字段 `{patch: string}` - 指向其 `cordis.patch.yml`。三盒内置 bundle 都一样声明:
|
||||
```json
|
||||
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
|
||||
```
|
||||
见 `packages/bundle/base/package.json:36-40`、`web-app/package.json:41-45`、`headless/package.json:41-45`。
|
||||
|
||||
- **`dsh.profile`**(`DshProfileManifest`,profile 目录声明):一个字段 `{bundles?: string[]}` - 有序 bundle 层列表。`initProfile()` 写入(`profile.ts:156-162`):
|
||||
```json
|
||||
{ "name": "dsh-profile-<name>", "private": true, "dependencies": {},
|
||||
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"] } } }
|
||||
```
|
||||
|
||||
### 四层叠加顺序(后者覆盖前者)
|
||||
|
||||
从 `composeProfile()`(`apps/cli/src/profile-boot.ts:142-171`)和 `loadProfile()`(`profile.ts:371-403`),patch 列表按此顺序构建(后者 = 胜出):
|
||||
|
||||
1. **bundle 层** - 按 `dsh.profile.bundles` 列出顺序。模板(`profile.ts:114-117`):
|
||||
- `web` -> `['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']`
|
||||
- `headless` -> `['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-headless']`
|
||||
2. **profile 自己的 `cordis.patch.yml`** - `$DSH_HOME/profiles/<name>/cordis.patch.yml`
|
||||
3. **home 级 `cordis.patch.yml`** - `$DSH_HOME/cordis.patch.yml`(机器本地偏好,高于 profile 层)
|
||||
4. **`--patch` 覆盖层** - argv 顺序
|
||||
5. **遥测开关** - 当 `DSH_TELEMETRY_DISABLED` 置位且行存在时,追加 `{id: 'session-telemetry-otel', disabled: true}`(`profile-boot.ts:80-83, 168-169`)
|
||||
|
||||
`allPatches()` flattener(`profile-boot.ts:122-129`)确认 live-boot 顺序:
|
||||
|
||||
```ts
|
||||
function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
return [...composed.bundlePatches, ...composed.profile.patches,
|
||||
...composed.homePatches, ...composed.overlays]
|
||||
}
|
||||
```
|
||||
|
||||
### 合并算法:先 flatten 再一次性应用
|
||||
|
||||
关键:层**不是**逐层增量合并,而是 flatten 成一个数组后单次 `applyEntryPatches` 调用。`composeEntries()`(`profile.ts:413-420`):
|
||||
|
||||
```ts
|
||||
export function composeEntries(layers: readonly PatchOptions[][], warn = () => {}): EntryOptions[] {
|
||||
return applyEntryPatches([], structuredClone(layers.flat()), ...)
|
||||
}
|
||||
```
|
||||
|
||||
空根 `[]` 是基础--每个行都由 bundle patch 插入。写入每个 profile 的根 `cordis.yml` 字面就是 `[]` 加注释(`apps/cli/src/profile-boot.ts:60-64`,`PROFILE_ROOT_CONFIG`)。
|
||||
|
||||
### bundle 解析(双锚)
|
||||
|
||||
`resolveBundleDir()`(`profile.ts:344-355`)按两锚顺序解析每个 bundle 名:先 dsh 安装的 `package.json`,再 profile 目录。这保证内置 bundle 永远来自运行的 dsh,而非 profile 本地副本。`loadProfile()`(`profile.ts:388-397`)读每个 bundle 的 `dsh.bundle.patch` 字段,声明的包缺 `dsh.bundle` 则 fail loud。
|
||||
|
||||
`healProfilesModuleFallback()`(`profile.ts:223-255`)维护一个扁平的 `$DSH_HOME/profiles/node_modules` 软链(对 app 依赖闭包 BFS),使裸插件名能从任何 profile 通过 Node 的父级遍历解析。
|
||||
|
||||
## 五、patch 算法:基于 id 的整字段替换/插入
|
||||
|
||||
核心算法 `applyEntryPatches()`(`vendor/include/src/index.ts:58-128`),**挂载和离线 config dump 共用**。
|
||||
|
||||
`PatchOptions` 形状(`include/src/index.ts:145-156`):
|
||||
|
||||
```ts
|
||||
export interface PatchOptions {
|
||||
id?: string
|
||||
insert?: EntryOptions[]
|
||||
name?: string
|
||||
config?: any
|
||||
group?: boolean | null
|
||||
disabled?: boolean | null
|
||||
inject?: any
|
||||
intercept?: any
|
||||
isolate?: any
|
||||
[key: string]: any
|
||||
}
|
||||
```
|
||||
|
||||
算法(`include/src/index.ts:63-127`):
|
||||
|
||||
1. `structuredClone(data)` - 输入永不被修改,结果总是 detached(使热重载可回退)
|
||||
2. 构建 `entryMap`(id -> entry),递归进 group
|
||||
3. 对每个 patch:
|
||||
- **`insert` 存在**:若 `id` 指向 group,把 insert 列表推入该 group 的 `config` 数组;无 `id` 则推到根。然后 `buildMap(insert)` 重新索引新行,使**同一列表里后续 patch** 能定位它们(96-101 行)
|
||||
- **`id` 定向覆盖**(无 insert):`id` 必填。找到目标;若给了 `name` 且不匹配,警告并跳过(防 patch 错行)。然后逐 key 直接赋值 `target[key] = value`(121-124 行)。这是**整字段替换,非深合并**--覆盖 `config` 会整体替换,必须重述要保留的字段
|
||||
4. 匹配不到的 patch 警告并跳过(110-113 行)
|
||||
|
||||
例如 `dsh-headless` 覆盖 base 层(`packages/bundle/headless/cordis.patch.yml:7-15`):
|
||||
|
||||
```yaml
|
||||
- id: system-prompt
|
||||
config:
|
||||
persona: >-
|
||||
You are a coding agent powered by the {{model}} model. ...
|
||||
|
||||
- id: hmr
|
||||
disabled: true
|
||||
```
|
||||
|
||||
## 六、`dsh --profile web --dump-config`
|
||||
|
||||
CLI flag 在 `apps/cli/src/args.ts:83-102`(`resolveBoot`)解析:`--dump-config`(带 user 层 + overlay)或 `--dump-default-config`(仅 bundle 层,互斥,不带 app 参数)。`apps/cli/src/bin.ts:45-49` 分派到 `runDumpConfig`。
|
||||
|
||||
`runDumpConfig()`(`apps/cli/src/dump-config.ts:30-52`):
|
||||
|
||||
1. `prepareProfile(profile, !defaultOnly)` - 加载 profile、heal module fallback、重写空根 `cordis.yml`
|
||||
2. 构建 `ConfigDumpLayer[]` - 每个 bundle 一个(`label: packageName`),然后(非 defaultOnly 时)profile patch 文件、home patch 文件、每个 `--patch` overlay
|
||||
3. 锚定空根:`renderConfigDump(NAME, join(loaded.dir, PROFILE_ROOT_FILENAME), layers)`
|
||||
|
||||
`renderConfigDump()`(`app-boot/src/index.ts:379-442`):
|
||||
|
||||
- 用 `entryListSchema` 解析基础配置(与 include 同方言)
|
||||
- 取增量快照:snapshot_k = `applyEntryPatches(base, layers[0..k].flat())` - 正是 `boot()` 在每个前缀会挂载的
|
||||
- 逐行追踪来源:哪个文件起源、哪些层 patch 了它(位置 diff)
|
||||
- patch 匹配不到行 -> 带层 label 警告
|
||||
- `groupedDump()`(`index.ts:445-473`)把同源连续行渲染到一个 `# == <origin>, patched by <layers>` 注释下,保持输出为单个可加载 YAML 文档
|
||||
|
||||
## 关键源码索引
|
||||
|
||||
| 主题 | 位置 |
|
||||
|------|------|
|
||||
| `EntryOptions` 行格式 | `vendor/loader/src/config/entry.ts:8-22` |
|
||||
| `!!js` YAML 标签 + `entryListSchema` | `vendor/include/src/index.ts:9-23` |
|
||||
| `evaluate` / `interpolate` / `isJsExpr` | `vendor/loader/src/config/utils.ts:5-27` |
|
||||
| `disabled` 的 `!!js` 求值 | `vendor/loader/src/config/entry.ts:104-108` |
|
||||
| Loader `internal/config` 插值 hook | `vendor/loader/src/index.ts:92-101` |
|
||||
| `EntryTree.import`(模块解析) | `vendor/loader/src/config/tree.ts:145-162` |
|
||||
| `EntryGroup.update`(事务式应用) | `vendor/loader/src/config/group.ts:59-106` |
|
||||
| `applyEntryPatches`(patch 算法) | `vendor/include/src/index.ts:58-128` |
|
||||
| `PatchOptions` 形状 | `vendor/include/src/index.ts:145-156` |
|
||||
| `boot()` | `packages/boot/app-boot/src/index.ts:757-802` |
|
||||
| `mountRootInclude()` | `packages/boot/app-boot/src/index.ts:486-529` |
|
||||
| `renderConfigDump()` + `groupedDump()` | `packages/boot/app-boot/src/index.ts:379-473` |
|
||||
| `watchUserPatches()` | `packages/boot/app-boot/src/index.ts:232-265` |
|
||||
| `PROFILE_TEMPLATES` / `DEFAULT_PROFILE_BUNDLES` | `packages/boot/app-boot/src/profile.ts:114-125` |
|
||||
| `resolveBundleDir`(双锚) | `packages/boot/app-boot/src/profile.ts:344-355` |
|
||||
| `loadProfile` | `packages/boot/app-boot/src/profile.ts:371-403` |
|
||||
| `composeEntries`(单次 flatten) | `packages/boot/app-boot/src/profile.ts:413-420` |
|
||||
| `composeProfile`(层顺序) | `apps/cli/src/profile-boot.ts:142-171` |
|
||||
| `allPatches` / `composeLive` | `apps/cli/src/profile-boot.ts:122-129, 240-245` |
|
||||
| `runDumpConfig` | `apps/cli/src/dump-config.ts:30-52` |
|
||||
| CLI flag 解析/分派 | `apps/cli/src/args.ts:83-102`、`apps/cli/src/bin.ts:45-49` |
|
||||
| `dshHomePath`(暴露给 `!!js`) | `packages/util/home-paths/src/index.ts:98-100`、`app-boot/src/index.ts:770` |
|
||||
| bundle 声明(base/web/headless) | `packages/bundle/{base,web-app,headless}/package.json` |
|
||||
@@ -0,0 +1,205 @@
|
||||
# 03 - 能力接缝三角色模式
|
||||
|
||||
Capability Seam 是 dsh 复用性最强的模式。一个**可替换能力**由三个角色组成,**缺一不可**,当角色独立演化时拆到不同包。本篇以 shell seam 完整追踪。
|
||||
|
||||
源码:`packages/shell/`。文档契约:`docs/capability-seams.md`、`docs/cookbook/adding-a-tool.md`。
|
||||
|
||||
## 三角色定义
|
||||
|
||||
| 角色 | 职责 | shell 例子 |
|
||||
|------|------|-----------|
|
||||
| **Service Definition** | 声明抽象接口 | `dsh-shell`(`ctx.shell`) |
|
||||
| **Service Provider** | 实现接口 | `dsh-bash-local` / `dsh-bash-sandbox` / `dsh-pwsh-local` |
|
||||
| **Consumer** | 使用能力(通常是模型工具) | `dsh-tool-bash` |
|
||||
|
||||
一个角色不构成 seam。文档把这套映射在 `docs/capability-seams.md:448` 标注:
|
||||
|
||||
> `ctx.shell` | `seam` | `shell` | `bash-local`, `bash-sandbox`, `pwsh-local` | `tool-bash`, `tool-pwsh`, `hooks-claude-code`, `hooks-codex` | … 模型面向的 shell 工具和 hook 桥消费此 seam;沙箱化、远程或 PowerShell 执行器替换 bash-local 而不触碰它们。
|
||||
|
||||
## 一、Service Definition - `packages/shell/shell/`
|
||||
|
||||
包:`@deepseek-ai/dsh-shell`,`packages/shell/shell/package.json:3` - *「Abstract bash executor seam (ctx.shell) for the DeepSeek Harness」*。
|
||||
|
||||
抽象服务在 `packages/shell/shell/src/index.ts`,augment Cordis `Context` 接口暴露 `ctx.shell`,声明继承 `Service` 的抽象类:
|
||||
|
||||
```ts
|
||||
// packages/shell/shell/src/index.ts:40-44
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
shell: ShellExecutor
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// packages/shell/shell/src/index.ts:65-101
|
||||
export abstract class ShellExecutor extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'shell') // <-- 注册为 ctx.shell
|
||||
}
|
||||
get sandboxMode(): SandboxMode | undefined { return undefined }
|
||||
abstract resolve(request: ShellExecRequest): ShellExecSpec
|
||||
abstract run(spec: ShellExecSpec): Promise<ShellRunResult>
|
||||
abstract start(spec: ShellExecSpec): ShellProcess
|
||||
}
|
||||
```
|
||||
|
||||
### request -> spec 分裂:seam 的核心设计模式
|
||||
|
||||
请求/结果类型在 `packages/shell/shell/src/types.ts`。seam 的中心设计--**request->spec 分裂**--体现在两者对比:
|
||||
|
||||
- `ShellExecRequest`(`types.ts:38-79`):调用方形状。`workdir`、`timeoutMs`、`stdoutMaxBytes`、`signal`、`stdin`、`env`、`dshEnv`、`sandboxPolicy` 全**可选** - *「由 `ShellExecutor.resolve` 从实现配置填充」*(`types.ts:34-36`)
|
||||
- `ShellExecSpec`(`types.ts:86-110`):已解析形状。`workdir`、`timeoutMs`、`stdoutMaxBytes`、`sandboxPolicy` 全**必填** - *「`ShellExecutor.resolve` 填充并限制必填字段」*(`types.ts:82-85`)
|
||||
|
||||
`resolve()` 是实现层填默认值和上限的**唯一**地方,`run()`/`start()` 只接收完全解析好的 spec,**不再 `?? default`**。这把「显式优于隐式」约束到了包边界(`CLAUDE.md`):*「默认值是 owning 实现里显式的 `resolve(request): Spec` 步骤,never a hidden `?? default` inside `run()`(`dsh-shell` 的 request/spec 分裂是模板)」*。
|
||||
|
||||
## 二、Service Provider - `packages/shell/bash-local/`
|
||||
|
||||
包:`@deepseek-ai/dsh-bash-local`,`packages/shell/bash-local/package.json:3` - *「Local-subprocess implementation of the … bash executor seam」*。其 `peerDependencies` 含 `@deepseek-ai/dsh-shell`(`package.json:34-40`)- provider 依赖 definition。
|
||||
|
||||
具体 provider 在 `packages/shell/bash-local/src/index.ts`。`LocalBashExecutor extends ShellExecutor`,以**类插件**加载(Cordis 用 `(ctx, config)` 实例化 `Service` 子类;其构造调 `super(ctx)` -> `super(ctx, 'shell')` 注册为 `ctx.shell`):
|
||||
|
||||
```ts
|
||||
// packages/shell/bash-local/src/index.ts:102-137
|
||||
export class LocalBashExecutor extends ShellExecutor {
|
||||
static inject = ['subprocess']
|
||||
static Config: z<Config> = z.object({
|
||||
cwd: z.string(),
|
||||
timeoutMs: z.number().default(120_000),
|
||||
maxTimeoutMs: z.number().default(600_000),
|
||||
maxOutputBytes: z.number().default(64_000),
|
||||
maxSpillBytes: z.number().default(DEFAULT_MAX_SPILL_BYTES),
|
||||
graceMs: z.number().default(DEFAULT_GRACE_MS),
|
||||
})
|
||||
// ...
|
||||
constructor(ctx: Context, config: Config) {
|
||||
super(ctx)
|
||||
const entry = config as ResolvedConfig
|
||||
assertServiceableBashConfig(entry)
|
||||
this.source = () => entry
|
||||
installSettingsSection(ctx, SHELL_SETTINGS_NAMESPACE, LocalBashExecutor.Config, entry, { ... })
|
||||
}
|
||||
```
|
||||
|
||||
无 `apply`/`name` export - `export default LocalBashExecutor`(`index.ts:333`)。组合层在 `cordis.yml` 接线:
|
||||
|
||||
```yaml
|
||||
# examples/headless-agent/cordis.yml:37-40
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
```
|
||||
|
||||
### `resolve(request): Spec` 模板
|
||||
|
||||
`index.ts:146-171`:
|
||||
|
||||
```ts
|
||||
resolve(request: ShellExecRequest): ShellExecSpec {
|
||||
const timeoutMs = clampTimeout(
|
||||
request.timeoutMs, this.config.timeoutMs, this.config.maxTimeoutMs, 'bash-local: request.timeoutMs')
|
||||
const stdoutMaxBytes = request.stdoutMaxBytes ?? this.config.maxOutputBytes
|
||||
assertPositiveFinite('request.stdoutMaxBytes', stdoutMaxBytes)
|
||||
return {
|
||||
command: request.command,
|
||||
workdir: request.workdir ?? this.config.cwd ?? process.cwd(),
|
||||
timeoutMs,
|
||||
stdoutMaxBytes,
|
||||
...request.signal ? { signal: request.signal } : {},
|
||||
...request.stdin !== undefined ? { stdin: request.stdin } : {},
|
||||
...request.env !== undefined ? { env: request.env } : {},
|
||||
...request.dshEnv !== undefined ? { dshEnv: request.dshEnv } : {},
|
||||
sandboxPolicy: request.sandboxPolicy, // 此 executor 惰性 - 从不限制
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`run`/`start` 委托 `ctx.subprocess.spawn(...)`(`index.ts:226`、`257`)。分裂的价值在兄弟 provider `bash-sandbox`,它**重写 `resolve()`** 给 spec 盖沙箱默认值,留 `run`/`start` 和工具不动:
|
||||
|
||||
```ts
|
||||
// packages/shell/bash-sandbox/src/index.ts:44-45, 84-86
|
||||
export class SandboxBashExecutor extends LocalBashExecutor {
|
||||
static override inject = ['subprocess', 'sandbox', 'sandboxPolicy']
|
||||
override resolve(request: ShellExecRequest): ShellExecSpec {
|
||||
return { ...super.resolve(request), sandboxPolicy: request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve() }
|
||||
}
|
||||
```
|
||||
|
||||
## 三、Consumer / Tool - `packages/shell/tool-bash/`
|
||||
|
||||
包:`@deepseek-ai/dsh-tool-bash`,`packages/shell/tool-bash/package.json:3` - *「Model-facing bash tool …」*。Consumer inject **Service Definition**,不是任何 provider:
|
||||
|
||||
```ts
|
||||
// packages/shell/tool-bash/src/index.ts:30-31
|
||||
export const name = 'tool-bash'
|
||||
export const inject = ['tools', 'shell', 'systemPrompt', 'shellEnv']
|
||||
```
|
||||
|
||||
模型面向工具用 `defineTool` 注册(`index.ts:242`)。其 `execute` body 构建 `ShellExecRequest`,调 `ctx.shell.resolve(request)` 得 spec,再交给 `ctx.shell.run`(前台)或 `ctx.shell.start`(后台):
|
||||
|
||||
```ts
|
||||
// packages/shell/tool-bash/src/index.ts:341-348 (构建 REQUEST)
|
||||
const dshEnv = ctx.shellEnv.collect(exec)
|
||||
const request = {
|
||||
command: args.command,
|
||||
...workdir !== undefined ? { workdir } : {},
|
||||
...args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {},
|
||||
dshEnv,
|
||||
...policy !== undefined ? { sandboxPolicy: policy } : {},
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// packages/shell/tool-bash/src/index.ts:380-383 (前台: resolve -> run)
|
||||
const result = await ctx.shell.run(ctx.shell.resolve({
|
||||
...request,
|
||||
signal: exec.signal,
|
||||
}))
|
||||
```
|
||||
|
||||
```ts
|
||||
// packages/shell/tool-bash/src/index.ts:370 (后台: resolve -> start)
|
||||
const proc = ctx.shell.start(ctx.shell.resolve(request))
|
||||
```
|
||||
|
||||
它还在加载时读 `ctx.shell.sandboxMode`(`index.ts:192`)决定是否广告 `sandbox_permissions`/`justification` 升级参数(`index.ts:259-269`)。`presentCall`/`presentResult` presenter(102-136 行)声明 `terminal` card - 即「工具的 UI render intent 是其设计的一部分」约定。
|
||||
|
||||
## 四、事件:shell seam 声明 NONE
|
||||
|
||||
shell seam 是**同步请求/响应服务,无能力事件**。grep `packages/shell/` 下所有 `interface Events` augmentation - 没有。Service Definition 只 augment `interface Context`(`shell/src/index.ts:40-44`),never `interface Events`,所以没有 `shell/*` 事件供监听。
|
||||
|
||||
包自己的 invariant companion 明示(`packages/shell/bash-local/src/invariant.ts:15-17`):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* No runtime invariant: this stateless Service Definition owns request/result types,
|
||||
* while executors and policy own observations.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
```
|
||||
|
||||
观测(退出码、signal、stdout/stderr、沙箱事实)作为 `ShellRunResult`/`ShellProcess` 返回值回来,不是事件。对比 `ctx.credentials` 这类 seam **会**声明 `credentials/updated` 事件(`packages/credentials/credentials/src/types.ts:15-30`)。所以本 seam 问题 4 的答案:无 `shell/*` 事件、因此无策略/适配器监听--seam 的契约纯是三个方法加 `sandboxMode` getter。
|
||||
|
||||
## 五、三角色包分裂(及它是真 seam 的证据)
|
||||
|
||||
| 角色 | 包 | 路径 | `peerDependencies`(相关) |
|
||||
|------|----|------|------|
|
||||
| Service Definition | `@deepseek-ai/dsh-shell` | `packages/shell/shell` | `dsh-invariants`、`dsh-subprocess`、`dsh-sandbox`、`cordis`、`dsh-settings` - **无 provider、无 consumer** |
|
||||
| Service Provider | `@deepseek-ai/dsh-bash-local` | `packages/shell/bash-local` | 含 **`@deepseek-ai/dsh-shell`**(provider -> definition) |
|
||||
| Consumer | `@deepseek-ai/dsh-tool-bash` | `packages/shell/tool-bash` | 含 **`@deepseek-ai/dsh-shell`**;`@deepseek-ai/dsh-bash-local` 只在 **`devDependencies`**(`package.json:65`) |
|
||||
|
||||
最后一行是承重证据:Consumer 的运行时依赖在 **Service Definition**,不在 Provider。`dsh-bash-local` 仅为 devDependency,使工具测试能挂具体执行器。这正是文档能宣称 *「沙箱化、远程或 PowerShell 执行器替换 bash-local 而不触碰它们」*(`docs/capability-seams.md:448`)的原因--在 `cordis.yml` 把 `bash-local` 换 `bash-sandbox` 或 `pwsh-local`,`tool-bash` 零行改动,因为它只跟 `ctx.shell` 对话。
|
||||
|
||||
`docs/capability-seams.md` 的 mermaid 图编码同样的箭头:`pkg_bash_local --> svc_shell`(provider 实现,206 行)和 `svc_shell --> pkg_tool_bash`(consumer 消费,362 行)。`docs/cookbook/adding-a-tool.md:5` 指向这个包为参考:*「`packages/shell/tool-bash` 是生产级三包范例」*。
|
||||
|
||||
## 关键源码索引
|
||||
|
||||
| 主题 | 位置 |
|
||||
|------|------|
|
||||
| Service Definition | `packages/shell/shell/src/index.ts`、`packages/shell/shell/src/types.ts` |
|
||||
| Service Provider | `packages/shell/bash-local/src/index.ts` |
|
||||
| 兄弟 Provider 重写 | `packages/shell/bash-sandbox/src/index.ts:84-86` |
|
||||
| Consumer / Tool | `packages/shell/tool-bash/src/index.ts` |
|
||||
| 组合接线 | `examples/headless-agent/cordis.yml:37-40` |
|
||||
| 文档契约 | `docs/capability-seams.md:448`(表)、mermaid 图 206-207/240/362-363 行;`docs/cookbook/adding-a-tool.md:5` |
|
||||
@@ -0,0 +1,103 @@
|
||||
# 04 - 工具注册表与执行流水线
|
||||
|
||||
`ctx.tools`(`ToolRuntime`,`packages/core/tools/`)是插件贡献的汇聚点。本篇解析工具注册、执行流水线、canonical 输出契约、以及 Code Mode。
|
||||
|
||||
源码:`packages/core/tools/`。文档:`docs/tool-execution-pipeline.md`、`docs/cookbook/adding-a-tool.md`、`packages/core/tools/README.md`。
|
||||
|
||||
## 一、注册即 effect
|
||||
|
||||
```ts
|
||||
ctx.tools.register(defineTool({
|
||||
name, description, parameters, output, execute, presentCall, ...
|
||||
}))
|
||||
```
|
||||
|
||||
返回 disposer,fiber 卸载时自动注销。schema 自动流入 system-prompt 组装(`ctx.systemPrompt.tools()`)。`defineTool` 提供**类型化参数 schema**(`ParameterSchemaSpec`),`execute` 里 `args` 已是 `InferArgs` 推断的类型,registry 在 `execute` 前已校验。
|
||||
|
||||
### 注册 API
|
||||
|
||||
- `ctx.tools.register(definition)` - 注册可信 typed 同进程定义,必带 canonical `output` 声明。调用上下文的 scope 即注册层:普通插件 context 全局注册;agent 的 `agent.ctx` 为该 agent 单独注册,shadow 同名全局工具。同层重复名抛错
|
||||
- `ctx.tools.presentAs(mode)` - 选该 agent 的模型面向呈现,shadow `mode` config
|
||||
- `ctx.tools.restrict(filter)` - agent-scoped allow/deny mask,多 mask 相交,scope-local 工具后合并
|
||||
- `ctx.tools.get(name, scope?)` - 单 scope 解析(shadowing 应用;restricted-away 的全局读为缺)
|
||||
- `ctx.tools.schemas(scope?)` - 该 scope 可见全部 schema(无 `execute`)
|
||||
- `ctx.tools.guard(guard)` - 注册单调同步执行 guard(`tools/pre-execute` 之后),返回 reason 拒绝、`undefined` 不变。后续 waterfall 监听器无法把 guard 拒绝翻回许可
|
||||
- `ctx.tools.execute(exec)` - lossless 快照+冻结参数、分配不透明 token、跑完整策略/分派/结果流水线、再独立快照权威结果
|
||||
- `ctx.tools.executionMode(exec)` - 仅当可见定义的 `isConcurrencySafe(args)` 返回**精确 `true`** 才返回 `parallel`
|
||||
|
||||
## 二、执行流水线
|
||||
|
||||
```
|
||||
tool/call (会话事件,执行前先记录)
|
||||
-> tools/pre-execute (waterfall: allow/deny/ask 策略门)
|
||||
-> 注册的 monotonic guards (最终否决,后置监听无法翻案)
|
||||
-> tools/execute (waterfall: 超时/重试/指标,around dispatch)
|
||||
-> execute() 函数体
|
||||
-> tools/post-execute (waterfall: 替换内容/值、阻断、附加 context)
|
||||
-> finalizeContent (定义拥有的最后内容不变量)
|
||||
-> tools/result (只读观察,不可变结果)
|
||||
-> tool/result (durable 会话事件)
|
||||
```
|
||||
|
||||
`tools/pre-execute` 是可重排的 allow/deny/ask 门;`ctx.tools.guard()` 在其后加单调 owner 策略。`tools/execute` 包规范 canonical 分派用于超时/重试/指标。`tools/post-execute` 可替换呈现内容、替换 canonical 值、带反馈阻断、附加有序 context。
|
||||
|
||||
### 关键不变量
|
||||
|
||||
- **执行身份受保护** - registry 把 `arguments` 做成 detached lossless JSON 递归快照并冻结,分配不透明 `exec.token`,`callId`/`name`/`arguments`/`agent`/`token`/必填的 caller-owned `signal`/可选 enclosing-transport `parent` token 全程不可变。`parent` 仅身份、不暴露活的外层执行。`args` 视为 readonly 输入。只有 around-dispatch wrapper 收到可变视图,且只能替换和恢复必填的 `exec.signal` 施加 deadline,不能删除它
|
||||
- **args 已校验** - `defineTool` 在 `execute` 前校验 model-generated `arguments`(类型、必填 key、literal 约束、exact-one union、嵌套值),`execute` 内 args 匹配 `InferArgs`。仍需手检 DSL 表达不了的约束(非空串、正数、跨字段规则)
|
||||
- **canonical 输出契约** - `output.schema` 用 `ValueSchemaSpec`,可为 object/array/scalar/null 根。`execute` 只返回推断值;registry 快照、校验、冻结后交给 `output.render(args, value)`。不要从 body 返回 content block
|
||||
- **抛错或返回非法值 = `isError`** - registry 捕获 throw 并在观察者前包含 schema/renderer/metadata-projector/lossless-JSON 失败。基础设施失败用 throw;成功域结果(即便非理想,如非零退出)用 canonical 值表示
|
||||
- **响应 `exec.signal`** - 触发时取消在途工作
|
||||
- **`exec.agent` 异步通知** - `agent.inject({ content, source: { kind: 'plugin', plugin: '<name>' } })` 追加 durable context 下次模型请求可见(非唤醒)
|
||||
|
||||
### 取消是协作且静止的
|
||||
|
||||
每次 typed 调用提供 caller-owned `AbortSignal`;tool body 收到必填 readonly `exec.signal`,只有 `tools/execute` wrapper 可临时替换。registry 保留 caller 取消穿过替换。分派前取消 = `ABORTED_BEFORE_DISPATCH`;调用后取消只能把成功换成 `ABORTED`。denial、wrapper 失败、tool 失败、post-policy 失败、timeout 的 `TOOL_TIMEOUT` 仍更具体。
|
||||
|
||||
## 三、工具拥有的 UI 呈现
|
||||
|
||||
工具可选拥有纯 `presentCall()`/`presentResult()` render intent,UI 不特殊处理工具名:
|
||||
|
||||
- **call 视图**:`{ card: 'generic', title, kind?, rawInput?, content?, locations? }`、`{ card: 'terminal', title, description?, cwd? }`、`{ card: 'diff', title, diffs, locations? }`
|
||||
- **result 视图**:`generic`、`terminal`、`diff`、`search`(发现搜索)、`read`(文件读)、`web`(web 检索)
|
||||
|
||||
返回 `undefined` 选 generic fallback。presenter 只依赖参数和 durable 结果(UI 在流式和日志回放时都调)。`output.presentationMeta(args, value)` 派生顶层调用的 JSON metadata;该 metadata 持久化在 `tool/result` 并回到 `presentResult`,而 canonical 值本身是 execution-local 永不回放。嵌套 Code dispatch 不计算 metadata。`defineTool` 软校验旧 logged 参数并 fallback 而非崩回放。
|
||||
|
||||
### 硬规则
|
||||
|
||||
- **纯净** - 流式和日志回放都跑,所以必须是 `args`(+结果)的纯函数:无 I/O、不读会话状态、不用时钟/随机。需要旧文件内容或工作目录时,放 durable result metadata 或 adapter,不放 presenter
|
||||
- **UI-only 格式不进模型结果** - fenced console 块、diff、相对化路径不属于 canonical 值或 Native 内容。`output.render` 管模型面向 prose;`presentationMeta` + card presenter 管可回放 UI 状态
|
||||
|
||||
## 四、Code Mode
|
||||
|
||||
`tools.mode: native | code | both`。`native` 贡献可见工具为函数定义。`code` 贡献保留 `run_code` transport、生成的 `tools:sdk` section、`tools:code-only` 规则(只有 `run_code` 可直接调)。`both` 贡献两者且无此规则。非 native 模式需 `ctx.codeRuntime.language` 有注册 SDK renderer(TS 经 `dsh-code-runtime-worker-thread`;Python 内建)。
|
||||
|
||||
### `run_code` 是唯一入口(`code` 模式)
|
||||
|
||||
模型直接调其他可见工具在执行创建时解析为 `UNKNOWN_TOOL`,在 `tools/pre-execute`、approval `ask`、guards 前,所以无物观察或批准只会失败的调用。denial 名回路由(`only \`run_code\` is callable directly - call \`<name>\` from inside a \`run_code\` program instead`)。
|
||||
|
||||
### 分派桥(`run_code` 的 execute)
|
||||
|
||||
每个 binding call 分派前 lossless JSON 快照(`undefined`/`BigInt`/cycles/sparse arrays/`-0`/exotic objects 拒绝该调用),经每 run pool 调度重用 native 并发契约 - 调用严格按提交序开始,连续 `isConcurrencySafe` 调用重叠至多 `maxParallelSubCalls`(默认 10;`1` 恢复串行),exclusive 调用清空 pool 独跑。成功返回策略后最终 canonical 值;失败到 worker 成一条消息变 `ToolCallError(toolName, message)`。每 started sub-call 记 `tool/code-dispatch-start` 事件,结算带一条 `tool/code-dispatch` 事件。
|
||||
|
||||
### 结算纪律
|
||||
|
||||
桥拥有 run-scoped abort 跟随外层 signal,run 结算时触发(预算到期 abort 在途 sub-tool 而非孤儿它);桥在返回前清空 queue,使每条 `tool/code-dispatch` 落在打开的 turn 内。失败 run 抛 `CodeRunFailedError`,流水线转成结构化 `isError` 供模型自纠。
|
||||
|
||||
### 结果大小
|
||||
|
||||
中间 binding 值整体跨 worker,无 per-binding 字节上限。`run_code` 返回 canonical `{ logs: string[], result?: JsonValue }`;串原文渲染,其他 JSON 根经 stack-safe pretty JSON 遍历(缩进上限 10 字符)。worker 的可配置 `maxOutputBytes`(默认 64 MiB)只用于组合序列化外层 log-array/completion-value/failure-message;只有此外层结果可 ordinary spill。
|
||||
|
||||
## 关键源码索引
|
||||
|
||||
| 主题 | 位置 |
|
||||
|------|------|
|
||||
| 工具注册表服务 | `packages/core/tools/README.md`、`packages/core/tools/src/` |
|
||||
| 工具流水线图 | `docs/tool-execution-pipeline.md` |
|
||||
| 工具编写契约 | `docs/cookbook/adding-a-tool.md` |
|
||||
| canonical 输出契约 note | `.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md` |
|
||||
| render-intent union note | `.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md` |
|
||||
| Code Mode foundation note | `.agents/notes/implemented/feature/2026-06-15-code-mode.md` |
|
||||
| code-mode typed-return note | `.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md` |
|
||||
| 并行工具调用 note | `.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md` |
|
||||
| 协作工具取消 note | `.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md` |
|
||||
@@ -0,0 +1,169 @@
|
||||
# 05 - 解剖真实插件:`todo_write`
|
||||
|
||||
`packages/todo/tool-todo/src/index.ts` 是一个最小的生产插件(226 行),集中体现了 dsh 插件系统的所有机制。本篇逐段解剖。
|
||||
|
||||
## 完整源码骨架
|
||||
|
||||
```ts
|
||||
export const name = 'tool-todo'
|
||||
export const inject = ['tools'] // 依赖声明:等 tools 服务就绪
|
||||
|
||||
export const Config = z.object({ allowParallelInProgress: z.boolean().required() })
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const allowParallel = config.allowParallelInProgress
|
||||
|
||||
// 条件依赖:仅当 sessionProjections seam 被组合时激活
|
||||
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
||||
projectionCtx.sessionProjections.register<'todos', TodoItem[] | null>({
|
||||
key: 'todos',
|
||||
schema: todosProjectionSchema,
|
||||
init: () => null,
|
||||
apply: (state, event) => { // 从会话日志投影
|
||||
if (event.type === 'todo/write') return event.data.todos
|
||||
if (event.type === 'turn/start') return null
|
||||
return state
|
||||
},
|
||||
view: state => state,
|
||||
stateVersion: 2,
|
||||
})
|
||||
})
|
||||
|
||||
// 注册工具(返回 disposer,fiber 卸载即注销)
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'todo_write',
|
||||
description: describe(allowParallel),
|
||||
parameters: { todos: { type: 'array', required: true, items: { ... } } },
|
||||
output: { schema: { type: 'object', ... }, render: (_args, value) => [{ type: 'text', text: ... }] },
|
||||
execute(args, exec) {
|
||||
const todos = toTodoList(args.todos, allowParallel) // 校验 DSL 表达不了的约束
|
||||
if (!exec.agent) throw new Error('todo_write requires an owning agent session')
|
||||
exec.agent.session.append('todo/write', { todos }) // 追加 durable 会话事件
|
||||
return Promise.resolve({ todos: ..., counts: { ... } })
|
||||
},
|
||||
presentCall: args => ({ card: 'generic', title: 'Update todo list', kind: 'other', rawInput: args.todos }),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## 体现的机制
|
||||
|
||||
### 1. 函数插件形态 + 元数据
|
||||
|
||||
```ts
|
||||
export const name = 'tool-todo'
|
||||
export const inject = ['tools']
|
||||
```
|
||||
|
||||
这是 Cordis 的**函数插件**形态(`registry.ts:121-123`):导出 `name`、`inject`、`Config`、`apply`。`inject = ['tools']` 声明依赖--插件只在 `ctx.tools` 服务存在时激活。Named export 保留 loader injection metadata。
|
||||
|
||||
### 2. 配置驱动行为(无硬编码 tunable)
|
||||
|
||||
```ts
|
||||
export interface Config {
|
||||
allowParallelInProgress: boolean
|
||||
}
|
||||
export const Config = z.object({
|
||||
allowParallelInProgress: z.boolean().required(),
|
||||
})
|
||||
```
|
||||
|
||||
`allowParallelInProgress` 是部署选择,从 `cordis.yml` config 传入。它改变两件事:
|
||||
|
||||
- **工具描述**(`describe(allowParallel)`,74-78 行):并行时让模型把每个活跃任务标 `in_progress`;单活时要求恰好一个
|
||||
- **校验逻辑**(`toTodoList`,91-111 行):`!allowParallel && active > 1` 时拒绝
|
||||
|
||||
这体现了 `CLAUDE.md` 的 *「No hardcoded tunables in plugins: 部署变化的选择是校验过的 `Config` 字段,可从 cordis.yml 改」*。
|
||||
|
||||
### 3. 条件依赖(ctx.inject)
|
||||
|
||||
```ts
|
||||
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
||||
projectionCtx.sessionProjections.register(...)
|
||||
})
|
||||
```
|
||||
|
||||
`ctx.inject(['sessionProjections'], cb)` 是 `ctx.plugin({inject, apply: cb})` 的简写(`registry.ts:300-302`)。这意味着:**仅当 `sessionProjections` seam 被组合时**这个子块才激活。headless 装配不含投影 seam 时,此块不执行,工具仍正常注册。注释也明示:*「headless assemblies without the seam stay unaffected」*。
|
||||
|
||||
### 4. 注册即 effect
|
||||
|
||||
```ts
|
||||
ctx.tools.register(defineTool({ ... }))
|
||||
```
|
||||
|
||||
`register()` 返回 disposer,随调用 fiber 卸载自动注销工具(见 [01-cordis-framework.md](01-cordis-framework.md) 第三节「注册即可逆 effect」)。同样 `sessionProjections.register()` 也是 effect。注释 *「Registration is effect-based: disposing the plugin fiber unregisters the tool」*。
|
||||
|
||||
### 5. model-visible ⟺ logged
|
||||
|
||||
```ts
|
||||
execute(args, exec) {
|
||||
// ...
|
||||
exec.agent.session.append('todo/write', { todos })
|
||||
return Promise.resolve({ todos: ..., counts: { ... } })
|
||||
}
|
||||
```
|
||||
|
||||
工具**不直接改状态**,而是 `exec.agent.session.append('todo/write', { todos })` 追加一条 durable 会话事件。UI 从事件流投影渲染(见下条)。这体现 *「model-visible means logged: 任何进入模型请求的内容都能从会话日志重建;新模型可见输入必须新 session event」*。canonical 返回值是 execution-local,UI 状态来自投影。
|
||||
|
||||
### 6. 从会话日志投影 UI 状态
|
||||
|
||||
```ts
|
||||
projectionCtx.sessionProjections.register<'todos', TodoItem[] | null>({
|
||||
key: 'todos',
|
||||
init: () => null,
|
||||
apply: (state, event) => {
|
||||
if (event.type === 'todo/write') return event.data.todos
|
||||
if (event.type === 'turn/start') return null
|
||||
return state
|
||||
},
|
||||
view: state => state,
|
||||
stateVersion: 2,
|
||||
})
|
||||
```
|
||||
|
||||
`apply(state, event)` 是 fold:最新 `todo/write` 整列表(last-write-wins),`turn/start` 清空(新 turn 重置),其他事件返回原状态。`init: () => null` 是 pre-first-write 或 turn 开始后的值。`view` 直接返回状态。`stateVersion: 2` 是投影 schema 版本。这是 *「render from the log」* 模式:UI 不调工具拿状态,而是从 durable 事件流投影。
|
||||
|
||||
### 7. canonical 输出 + 纯渲染
|
||||
|
||||
```ts
|
||||
output: {
|
||||
schema: { type: 'object', additionalProperties: false, properties: { todos: {...}, counts: {...} } },
|
||||
render: (_args, value) => [{
|
||||
type: 'text',
|
||||
text: `Updated todo list: ${value.counts.pending} pending, ...`,
|
||||
}],
|
||||
},
|
||||
```
|
||||
|
||||
`execute` 返回结构化 canonical 值(`todos` + `counts`),registry 快照+校验+冻结后交给 `render`。`render` 只做面向模型的文本。`presentCall` 返回 `card: 'generic'` UI render intent。`additionalProperties: false` 保证 logged 快照等于模型相信它写的(嵌套/扩展 item 形状在 schema 边界 fail loud 而非静默扁平化)。
|
||||
|
||||
### 8. 执行身份保护
|
||||
|
||||
```ts
|
||||
if (!exec.agent) {
|
||||
throw new Error('todo_write requires an owning agent session')
|
||||
}
|
||||
```
|
||||
|
||||
非 agent 调用者无 owning session 可写列表,**拒绝而非静默 no-op**。这是 *「misconfiguration fails loud」* 的体现。
|
||||
|
||||
### 9. 校验 DSL 表达不了的约束
|
||||
|
||||
`toTodoList`(91-111 行)在 registry schema 校验后额外手检:
|
||||
|
||||
- `content` trim 后非空(`content.length === 0` 抛错)
|
||||
- content 唯一(`seen.has(content)` 抛错)
|
||||
- 非并行模式下至多一个 `in_progress`(`active > 1` 抛错)
|
||||
|
||||
这对应 *「Args are validated for you... You still hand-check constraints the DSL does not express, such as non-empty strings」*。
|
||||
|
||||
## 关键源码索引
|
||||
|
||||
| 主题 | 位置 |
|
||||
|------|------|
|
||||
| 插件主源码 | `packages/todo/tool-todo/src/index.ts` |
|
||||
| 类型(TodoItem 等) | `packages/todo/tool-todo/src/types.ts` |
|
||||
| invariant companion | `packages/todo/tool-todo/src/invariant.ts` |
|
||||
| 客户端 | `packages/todo/tool-todo/src/client.ts` |
|
||||
| 测试 | `packages/todo/tool-todo/tests/*.spec.ts` |
|
||||
| README | `packages/todo/tool-todo/README.md` |
|
||||
@@ -0,0 +1,120 @@
|
||||
# 06 - 贯穿全局的不变量与工程约束
|
||||
|
||||
这些是把插件系统各机制粘合起来的强约束(来源 `AGENTS.md`)。每条都有对应的设计决策 Agent Note 或文档。
|
||||
|
||||
## 一、注册与生命周期
|
||||
|
||||
### 注册即 effect
|
||||
|
||||
每个贡献走 `ctx.effect()` / `ctx.on()`;registry 的 `register()` 返回 disposer。卸载插件 -> 它注册的工具、监听器、服务提供全部自动撤销(见 [01](01-cordis-framework.md) 第三节)。
|
||||
|
||||
### 运行时不变量断言自有关系
|
||||
|
||||
检查**权威事件流或可变数据**,而非服务或方法存在性、插件 metadata 或 effect、固定纯例子。无合理关系时,解释过的空 companion 是正确的(`packages/AGENTS.md` 的 package invariant rules)。
|
||||
|
||||
### 类型化事件用 declaration merging
|
||||
|
||||
事件 JSDoc 需要 `@mode` 和 payload `@param`;scope key 缺于 payload 需 `@dshScopeScan unsupported`。`SessionEventMap` 成员默认 read-on-required - 不知其类型的 build 拒绝 log,除非事件带 envelope 的 `ignorable: true`;只有结构格式变更 bump `SESSION_FORMAT_VERSION`。
|
||||
|
||||
## 二、模型可见性
|
||||
|
||||
### model-visible ⟺ logged
|
||||
|
||||
任何到达模型请求的内容必须能从会话日志重建;新模型可见输入需新 session event。运行时不变量断言此点(见 [05](05-plugin-anatomy.md) 第 5 点)。
|
||||
|
||||
## 三、扩展点哲学
|
||||
|
||||
### 插件,非 loop 改动
|
||||
|
||||
新行为挂到文档化的扩展点;改 `agent-loop` 需更新 `docs/architecture.md`。
|
||||
|
||||
### 能力 seam 三角色完整
|
||||
|
||||
一个 seam 包含 Service Definition / Service Provider / Consumer 三角色,**完整,非单一角色**;仅当角色独立演化时拆分(见 [03](03-capability-seam.md))。
|
||||
|
||||
### 显式优于隐式(包边界)
|
||||
|
||||
默认值是 owning 实现里显式的 `resolve(request): Spec` 步骤,**never a hidden `?? default` inside `run()`**(`dsh-shell` 的 request/spec 分裂是模板,见 [03](03-capability-seam.md) 第二节)。
|
||||
|
||||
### 无硬编码 tunable
|
||||
|
||||
部署变化的选择是校验过的 `Config` 字段,可从 `cordis.yml` 改;`DEFAULT_*` 常量或测试 hook 不是可配置性。协议常量、外部 spec、安全不变量保持固定(见 [05](05-plugin-anatomy.md) 第 2 点)。
|
||||
|
||||
### 误配置 fail loud
|
||||
|
||||
自包含时在加载时 fail,否则在最早可解析点;never silently skip missing referent。
|
||||
|
||||
## 四、类型安全
|
||||
|
||||
### 跨边界不透明 id 用 branded
|
||||
|
||||
`Branded<B>`(`dsh-brand`),never bare `string`。如 `ToolExecutionToken` 是 fresh branded `Symbol`,支持 equality correlation only,不跨 model/log/worker 边界。
|
||||
|
||||
### 信任 TS 在 typed 同进程边界
|
||||
|
||||
不为静态接口要求的值加运行时校验、fallback 行为、敌意输入测试;在 parser/config、queued、model/tool JSON、durable/file、worker、process、wire 边界校验。
|
||||
|
||||
### switch on discriminant tags
|
||||
|
||||
闭合 union 以 `assertNever` 结尾;merge-extensible union fall through 文档化的 default。
|
||||
|
||||
### waterfall 监听器必须调 `next()`
|
||||
|
||||
返回不调 `next()` 短路整条链(见 [01](01-cordis-framework.md) 第四节)。
|
||||
|
||||
## 五、工程纪律
|
||||
|
||||
### 偏好维护的依赖而非手搓
|
||||
|
||||
当依赖真的删除自有代码和测试时(policy:`.agents/notes/implemented/process/2026-07-26-dependencies-over-hand-rolling.md`)。
|
||||
|
||||
### Source plane vs artifact plane 不混
|
||||
|
||||
静态 gate 和 test 通过 tsconfig `paths` 解析 workspace import 到 `src`,clean tree 上过;消费 built `lib/` 的 gate 声明该依赖。
|
||||
|
||||
### 每个 package 一个 aggregate compiler face
|
||||
|
||||
`api/remotes` 例外;repo-wide 程序 seed 一个 face config,never root solution。
|
||||
|
||||
### 空 catch 命名吞了什么
|
||||
|
||||
并解释为何无他物可达;`try` 保持一句。
|
||||
|
||||
### 不注释代码显然的事实
|
||||
|
||||
### 偏好对称(平行值)
|
||||
|
||||
未解释的不对称通常信号漏了提取。
|
||||
|
||||
### 测试描述行为非正确性
|
||||
|
||||
用测试改过时行为;在 PR 解释为何。
|
||||
|
||||
### 非平凡改动必带 Agent Note
|
||||
|
||||
同一 PR;仅机械/局部编辑豁免(scope:`.agents/notes/README.md#when-to-write-one`)。归档 note 冻结:never edit 或当作当前权威。
|
||||
|
||||
### 测试策略
|
||||
|
||||
每个非平凡模型/产品用户可见行为变更通过真实可运行 example 在同 PR 加/更新 keyless snapshot;package test、e2e-only assertion、mock-only fixture 不替代装配应用 transcript。fixture 需 macOS/Linux 可重放;修 fixture 不修 normalizer。
|
||||
|
||||
### 工具 UI render intent 是设计的一部分
|
||||
|
||||
前置决定(`generic`/`terminal`/`diff`、`locations`);presentation 方法是 `args` 的纯函数。
|
||||
|
||||
### PR 历史
|
||||
|
||||
拆独立变更;propagation 前修 introducing PR。standalone PR 和 official stack 可 review 后 merge-forward 或 rebase。rewrite 用 `--force-with-lease`,remote 移动则 abort,never raw `--force`。
|
||||
|
||||
## 关键引用
|
||||
|
||||
| 主题 | 文档 |
|
||||
|------|------|
|
||||
| 防御模式 | `docs/defensive-patterns.md` |
|
||||
| 类型安全与文档 | `AGENTS.md` 的 Type safety and documentation 节 |
|
||||
| 测试策略 | `docs/testing.md` |
|
||||
| Cordis 语义 primer | `docs/cordis-primer.md` |
|
||||
| 术语表 | `docs/glossary.md` |
|
||||
| capability seam glossary | `docs/glossary.md#capability-seam` |
|
||||
| package invariant rules | `packages/AGENTS.md` |
|
||||
| Agent Note scope | `.agents/notes/README.md#when-to-write-one` |
|
||||
@@ -0,0 +1,124 @@
|
||||
# 07 - 关键源码文件索引
|
||||
|
||||
本索引汇总分析中引用的全部源码文件与行号,按主题分组。便于按文件查阅或追踪实现。
|
||||
|
||||
## 框架层(vendored Cordis)
|
||||
|
||||
| 主题 | 文件 | 行 |
|
||||
|------|------|-----|
|
||||
| Context 类 + 三作用域 | `vendor/cordis/src/context.ts` | 42-146 |
|
||||
| Context 代理陷阱 | `vendor/cordis/src/reflect.ts` | 135-206 |
|
||||
| Service 基类 | `vendor/cordis/src/service.ts` | 1-115 |
|
||||
| 插件形态 + inject 元数据 | `vendor/cordis/src/registry.ts` | 92-187 |
|
||||
| `ctx.plugin()` / `ctx.inject()` | `vendor/cordis/src/registry.ts` | 300-336 |
|
||||
| Fiber 状态机 | `vendor/cordis/src/fiber.ts` | 147-154 |
|
||||
| `ctx.effect()` | `vendor/cordis/src/fiber.ts` | 418-561 |
|
||||
| epoch 重载机制 | `vendor/cordis/src/fiber.ts` | 611-696 |
|
||||
| 五种分派模式 | `vendor/cordis/src/events.ts` | 131-243 |
|
||||
| `ctx.on()` 注册 | `vendor/cordis/src/events.ts` | 288-302 |
|
||||
| `provide()` 可逆注册 | `vendor/cordis/src/reflect.ts` | 277-305 |
|
||||
| 依赖通知 `notify()` | `vendor/cordis/src/reflect.ts` | 314-336 |
|
||||
| 框架 index | `vendor/cordis/src/index.ts` | - |
|
||||
|
||||
## 启动组合层
|
||||
|
||||
| 主题 | 文件 | 行 |
|
||||
|------|------|-----|
|
||||
| `EntryOptions` 行格式 | `vendor/loader/src/config/entry.ts` | 8-22 |
|
||||
| `!!js` YAML 标签 + `entryListSchema` | `vendor/include/src/index.ts` | 9-23 |
|
||||
| `evaluate` / `interpolate` / `isJsExpr` | `vendor/loader/src/config/utils.ts` | 5-27 |
|
||||
| `disabled` 的 `!!js` 求值 | `vendor/loader/src/config/entry.ts` | 104-108 |
|
||||
| Loader `internal/config` 插值 hook | `vendor/loader/src/index.ts` | 92-101 |
|
||||
| `EntryTree.import`(模块解析) | `vendor/loader/src/config/tree.ts` | 145-162 |
|
||||
| `EntryGroup.update`(事务式应用) | `vendor/loader/src/config/group.ts` | 59-106 |
|
||||
| `applyEntryPatches`(patch 算法) | `vendor/include/src/index.ts` | 58-128 |
|
||||
| `PatchOptions` 形状 | `vendor/include/src/index.ts` | 145-156 |
|
||||
| `boot()` | `packages/boot/app-boot/src/index.ts` | 757-802 |
|
||||
| `mountRootInclude()` | `packages/boot/app-boot/src/index.ts` | 486-529 |
|
||||
| `renderConfigDump()` + `groupedDump()` | `packages/boot/app-boot/src/index.ts` | 379-473 |
|
||||
| `watchUserPatches()` | `packages/boot/app-boot/src/index.ts` | 232-265 |
|
||||
| `loadOptionalPatches` / overlay 解析 | `packages/boot/app-boot/src/index.ts` | 278-338 |
|
||||
| `userPatchesSchema = entryListSchema` | `packages/boot/app-boot/src/index.ts` | 207 |
|
||||
| `PROFILE_TEMPLATES` / `DEFAULT_PROFILE_BUNDLES` | `packages/boot/app-boot/src/profile.ts` | 114-125 |
|
||||
| `initProfile` / `healProfilesModuleFallback` | `packages/boot/app-boot/src/profile.ts` | 152-168, 223-255 |
|
||||
| `resolveBundleDir`(双锚) | `packages/boot/app-boot/src/profile.ts` | 344-355 |
|
||||
| `loadProfile` | `packages/boot/app-boot/src/profile.ts` | 371-403 |
|
||||
| `composeEntries`(单次 flatten) | `packages/boot/app-boot/src/profile.ts` | 413-420 |
|
||||
| `composeProfile`(层顺序) | `apps/cli/src/profile-boot.ts` | 142-171 |
|
||||
| `allPatches` / `composeLive` | `apps/cli/src/profile-boot.ts` | 122-129, 240-245 |
|
||||
| `prepareProfile` / `homePatchPath` / `PROFILE_ROOT_CONFIG` | `apps/cli/src/profile-boot.ts` | 49-51, 60-64, 98-103 |
|
||||
| `runDumpConfig` | `apps/cli/src/dump-config.ts` | 30-52 |
|
||||
| CLI flag 解析 | `apps/cli/src/args.ts` | 83-102 |
|
||||
| CLI 分派 | `apps/cli/src/bin.ts` | 45-49 |
|
||||
| `dshHomePath`(暴露给 `!!js`) | `packages/util/home-paths/src/index.ts`、`packages/boot/app-boot/src/index.ts` | 98-100, 770 |
|
||||
| bundle 声明(base/web/headless) | `packages/bundle/{base,web-app,headless}/package.json` | 36-40 / 41-45 |
|
||||
| patch 实例 | `packages/bundle/{base,headless}/cordis.patch.yml` | - |
|
||||
|
||||
## 能力 seam(shell 例子)
|
||||
|
||||
| 主题 | 文件 |
|
||||
|------|------|
|
||||
| Service Definition | `packages/shell/shell/src/index.ts`、`packages/shell/shell/src/types.ts` |
|
||||
| Service Provider(local) | `packages/shell/bash-local/src/index.ts` |
|
||||
| Service Provider(sandbox 重写) | `packages/shell/bash-sandbox/src/index.ts:84-86` |
|
||||
| Consumer / Tool | `packages/shell/tool-bash/src/index.ts` |
|
||||
| 组合接线 | `examples/headless-agent/cordis.yml:37-40` |
|
||||
| 文档契约 | `docs/capability-seams.md:448`(表)、mermaid 图 206-207/240/362-363 |
|
||||
| cookbook 参考 | `docs/cookbook/adding-a-tool.md:5` |
|
||||
|
||||
## 工具注册表
|
||||
|
||||
| 主题 | 文件 |
|
||||
|------|------|
|
||||
| 工具服务 README | `packages/core/tools/README.md` |
|
||||
| 工具服务源码 | `packages/core/tools/src/` |
|
||||
| 工具流水线图 | `docs/tool-execution-pipeline.md` |
|
||||
| 工具编写契约 | `docs/cookbook/adding-a-tool.md` |
|
||||
| canonical 输出契约 note | `.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md` |
|
||||
| render-intent union note | `.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md` |
|
||||
| Code Mode foundation note | `.agents/notes/implemented/feature/2026-06-15-code-mode.md` |
|
||||
| code-mode typed-return note | `.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md` |
|
||||
| 并行工具调用 note | `.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md` |
|
||||
| 协作工具取消 note | `.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md` |
|
||||
|
||||
## 真实插件范例
|
||||
|
||||
| 主题 | 文件 |
|
||||
|------|------|
|
||||
| 插件主源码 | `packages/todo/tool-todo/src/index.ts` |
|
||||
| 类型(TodoItem 等) | `packages/todo/tool-todo/src/types.ts` |
|
||||
| invariant companion | `packages/todo/tool-todo/src/invariant.ts` |
|
||||
| 客户端 | `packages/todo/tool-todo/src/client.ts` |
|
||||
| 测试 | `packages/todo/tool-todo/tests/*.spec.ts` |
|
||||
| README | `packages/todo/tool-todo/README.md` |
|
||||
|
||||
## 核心架构文档
|
||||
|
||||
| 文档 | 位置 |
|
||||
|------|------|
|
||||
| 架构总览 | `docs/architecture.md` |
|
||||
| Cordis primer | `docs/cordis-primer.md` |
|
||||
| Cordis tutorial | `docs/cordis-tutorial/index.md` |
|
||||
| 防御模式 | `docs/defensive-patterns.md` |
|
||||
| 术语表 | `docs/glossary.md` |
|
||||
| 测试策略 | `docs/testing.md` |
|
||||
| 开发指南 | `docs/development.md` |
|
||||
| 扩展 cookbook | `docs/cookbook/extension-cookbook.md` |
|
||||
| 加包 cookbook | `docs/cookbook/adding-a-package.md` |
|
||||
| 加 LLM 适配器 cookbook | `docs/cookbook/adding-an-llm-adapter.md` |
|
||||
| 事件 producer/consumer | `docs/event-producer-consumer.md` |
|
||||
| 能力 seams | `docs/capability-seams.md` |
|
||||
| agent 生命周期 | `docs/agent-lifecycle.md` |
|
||||
| module graph(生成) | `docs/module-graph.md` |
|
||||
| tool catalog(生成) | `docs/tool-catalog.md` |
|
||||
| config catalog(生成) | `docs/config-catalog.md` |
|
||||
|
||||
## 规模与背景
|
||||
|
||||
- 版本:`0.1.0-rc.5`(developer preview)
|
||||
- 可发布 npm 包:226 个(`@deepseek-ai/dsh-*`)
|
||||
- 包分组:~50 个
|
||||
- `packages/*/src` TS:~198K 行
|
||||
- 测试:692 spec + 44 e2e
|
||||
- commit 总数:~12,293;贡献者:37
|
||||
- 无 `.codegraph/` 索引
|
||||
@@ -0,0 +1,41 @@
|
||||
# DeepSeek Harness 插件系统分析
|
||||
|
||||
本目录是对 dsh 插件系统实现的详细分析,基于源码阅读整理。覆盖从底层框架(vendored Cordis)到组合层、能力层、工具层,并解剖一个真实插件。
|
||||
|
||||
## 文档结构
|
||||
|
||||
| 文档 | 主题 | 关键源码 |
|
||||
|------|------|----------|
|
||||
| [01-cordis-framework.md](01-cordis-framework.md) | 框架底座:Context 代理、Service、Fiber 生命周期、可逆 effect、五种事件分派 | `vendor/cordis/src/` |
|
||||
| [02-boot-composition.md](02-boot-composition.md) | 启动组合:cordis.yml 行格式、`!!js` 延迟求值、profile/bundle/patch 四层叠加、patch 算法 | `vendor/loader/`、`vendor/include/`、`packages/boot/app-boot/` |
|
||||
| [03-capability-seam.md](03-capability-seam.md) | 能力接缝三角色模式(Service Definition / Provider / Consumer),以 shell seam 完整追踪 | `packages/shell/` |
|
||||
| [04-tool-registry.md](04-tool-registry.md) | 工具注册表与执行流水线、canonical 输出契约、Code Mode | `packages/core/tools/` |
|
||||
| [05-plugin-anatomy.md](05-plugin-anatomy.md) | 解剖真实插件 `todo_write`,集中体现所有机制 | `packages/todo/tool-todo/src/index.ts` |
|
||||
| [06-invariants.md](06-invariants.md) | 贯穿全局的不变量与工程约束 | `AGENTS.md` |
|
||||
| [07-file-index.md](07-file-index.md) | 关键源码文件索引 | — |
|
||||
|
||||
## 速览:一句话总结
|
||||
|
||||
dsh 的插件系统是「Cordis 代理 + 声明式依赖 + 可逆 effect 注册 + 五种事件分派」作底座,叠上「cordis.yml 行 + profile/bundle/patch 层叠加」的配置组合层,再用「Service Definition/Provider/Consumer 三角色 seam」封装可替换能力,所有插件通过 `ctx.<key>` 互相发现、通过 waterfall 事件挂策略——所以换一个 provider 就能连带迁移一整类能力,而工具和 loop 一行不改。
|
||||
|
||||
## 数据背景
|
||||
|
||||
- 版本:`0.1.0-rc.5`(developer preview,预告破坏性变更)
|
||||
- 规模:226 个可发布 npm 包、~50 个包分组、`packages/*/src` ~198K 行 TS
|
||||
- 测试:692 个 spec + 44 个 e2e
|
||||
- 无 `.codegraph/` 索引,分析基于源码直接阅读
|
||||
|
||||
## 使用说明
|
||||
|
||||
本目录是分析笔记,**不属于 dsh 官方 docs 体系**(未走 doc-sync 门禁、非双语)。查阅时建议按编号顺序阅读,每篇文档末尾附相关源码的文件路径与行号引用。
|
||||
|
||||
## 快照基准
|
||||
|
||||
本分析基于以下 commit 的检出整理:
|
||||
|
||||
- commit:`abe560f81edebe5f6a5b62706ff502daa0dccd40`(`abe560f81e`)
|
||||
- 版本:`0.1.0-rc.5`
|
||||
- 日期:2026-08-13
|
||||
- 提交:`release(dsh): 0.1.0-rc.5`
|
||||
|
||||
后续代码演进可能使引用的 `file:line` 偏移;结构层面的分析在架构大改前应当稳定。若需核对,可在该 commit 上 `git checkout` 后比对。
|
||||
Reference in New Issue
Block a user