DeepSeek Harness Hub
← 返回列表

Meaple-SFKY/dsh-model-orchestrator

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
未验证

适用于 DeepSeek Harness 的通用模型编排器。 它会发现正在运行的 harness…

尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/15 · 已提供中文文档

Model routing for DeepSeek Harness, with a standing capability-to-model assignment table, per-route reasoning levels, and a user-triggered sync that researches public model prices. 为 DeepSeek Harness 提供模型路由:可持久化的「能力→模型」分工表、按路由的推理档位,以及手动触发、由模型联网核对公开价格的同步。

综合分
29.8
GitHub 分
29.8
用户评分
★ Stars
0
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add Meaple-SFKY/dsh-model-orchestrator
该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-agent@deepseek-ai/dsh-host-webserver@deepseek-ai/dsh-llm@deepseek-ai/dsh-subagent@deepseek-ai/dsh-system-prompt@deepseek-ai/dsh-tools@deepseek-ai/dsh-workflow
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

dsh-model-orchestrator

适用于 DeepSeek Harness 的通用模型编排器。 它会发现正在运行的 harness 实际拥有哪些模型,推断每个模型有哪些证据表明的强项,并把每个工作单元路由到当下最合适的那一个——你无需再决定哪个任务该交给哪个模型。

它在自己的选择逻辑里不指名任何模型、provider 或厂商,也不针对任何特定业务领域。

它做什么

| 关注点 | 处理方式 |
|---|---|
| 模型发现 | 在激活时、适配器拓扑变化时、距上次读取最多 5 分钟时,以及按需 —— 面板的刷新模型池、orchestrate_models { refresh: true }、或 GET /state?force=1 —— 读取实时 LLM 注册表。provider 的模型列表可能是一次网络往返,因此周期性重读是惰性的:只在下次真正需要模型池时发生,且失败时保留已有的模型池。模型池从不硬编码,也从不持久化。 |
| 能力画像 | 依据宿主的权威事实(输入模态、上下文窗口、暴露的推理档位)加上 provider 自己声明的描述,为每个模型建立画像。不臆造任何内容。 |
| 任务匹配 | 把任务转成一组需求,再用确定性方式让每个实时模型对其打分。无法被证据支持的硬性需求会拒绝该模型,而不是悄悄降级。 |
| 开放的能力体系 | 能力是通用描述符,不是"领域→模型"的映射表。无法识别的领域会从任务自身的词汇中生成新的描述符并持久化,让分类体系真正生长。 |
| 两种模式 | Auto 推断任务需要什么。Guided 用你为本次会话选定的能力领域为匹配提供种子。 |
| 编排 | 简单工作直接执行。聚焦的工作交给一个专家子代理。复杂的多领域工作会跨多个子代理编排,每个专家的结果都会返回给调用方代理。 |
| Captain | captain 是一个角色,不是与某个模型的绑定:它是任务的所有者,负责理解、拆解、派发、汇总、验证和收尾。它的路由由匹配器从实时模型池中选出。 |
| UI | 一个 Model Orchestrator 设置页,以及一个与 Chat、Trajectory 并列的「编排看板」,展示本会话的派发关系。默认自动化;一切皆可调整。 |
| 持久化 | 偏好设置、学到的能力描述符、按路由的校准值。绝不持久化模型池。 |
| 兼容性 | 面对不支持的宿主时拒绝激活,并给出精确原因。没有静默降级。 |

它是调度器,不是任务管理器

编排器是在正常任务执行内部被调用的。它不取代、不镜像,也不与 DSH 原生的任务处理竞争:

| 关注点 | 归属 |
|---|---|
| 任务列表、计划、步骤、步骤状态 | DSH —— 编排器不注册任何任务工具 |
| 进度与进度展示 | DSH —— 编排器不渲染任何进度界面,也不发出任何事件 |
| 会话记录 | DSH —— 编排器不追加任何自定义会话事件 |
| 子代理可见性 | DSH —— 被派发的子代理就是常规视图里的普通 DSH 子代理 |
| 已完成工作的记录 | DSH 会话日志 —— 编排器不保留运行历史 |
| 哪个模型执行某个工作单元 | 编排器 |
| 一个任务需要一个还是多个专家 | 编排器 |

因此:todo_write、计划模式、步骤跟踪和进度渲染全都照常工作,与没有这个插件时完全一样。专家的回答以 orchestrate_ 调用的工具结果返回,代理照常写出最终答案。

插件唯一保留的实时簿记,是一组用于在途派发的 abort controller,这样插件重载时会中止子代理,而不是把它们变成孤儿。它不可查询,也绝不作为任务状态暴露出来。

为什么通过子代理来路由

harness 只提供一个受支持的、用于选定模型的接缝:子代理上的 agentOptions(ctx.subagents.start)。代理自身的路由在创建时就固定了,运行中的会话也没有逐轮改换目标的钩子。

所以编排器顺着这条接缝工作,而不是与它对抗:

1. 你所在会话的代理仍是面向用户的界面,也是 captain。
2. 插件为每个工作单元选定一条路由,并在该路由上生成一个子代理。
3. 专家的结果作为工具结果返回给 captain。
4. 由 captain 撰写最终答案 —— 编排器从不与用户对话。

安装

dsh plugin --profile  add github:Meaple-SFKY/dsh-model-orchestrator

或者从本地检出安装——以 tarball 形式:

npm pack --pack-destination /tmp
dsh plugin --profile web add /tmp/dsh-model-orchestrator-0.3.0.tgz

直接安装目录本身(add /path/to/dsh-model-orchestrator)行不通,而且失败方式看起来像插件有
bug,而不是安装出了问题:pnpm 记录的是 link: 依赖,插件的真实路径因此留在 profile 之外,
Node 的逐级向上查找永远到不了 profile 的 node_modules——而它导入的宿主包
(@deepseek-ai/dsh-tools、@deepseek-ai/dsh-llm……)正放在那里。加载会以
Cannot find package '@deepseek-ai/dsh-tools' 失败。打包成 tarball 会被实体化到 profile 内部,
解析因此正常。这也意味着 profile 里存的是一份快照:改动检出目录后,需要重新打包并再次 add。

等本包发布到 npm 之后,上面这条可以简写为:

dsh plugin --profile  add dsh-model-orchestrator

说清楚原因,免得有人按错误前提做规划:本包目前不在 npm 上 —— npm 现在要求发布者具备 2FA 或一个带 bypass-2FA 的 granular token,而本账号未开启 2FA,且 TOTP 已无法再注册。从 GitHub 安装不需要这些,社区注册表里约一半的插件也正是这样安装的。详见 docs/marketplace-submission.md。

这个 bundle patch 会向该 profile 的 host composition 挂载一行,把 orchestrate_ 工具注册进共享工具注册表,向系统提示词贡献一个路由策略小节,并提供控制面板路由。安装后请重启该 profile,让宿主加载新的 bundle。

先核对宿主版本范围。 本版本声明 engines.dsh = "0.1.5-rc.1",面对其他版本会拒绝激活并给出原因与恢复方式。安装前(或在 CI 中)可用这条命令核对:

node scripts/check-compat.mjs

在一个没有 DSH 可核对的检出里 —— 刚克隆下来,或任何 CI runner —— 它会如实说明并以 0 退出,而不是报一个它从未得出的"不兼容"结论;同时仍会校验不需要宿主的那部分:声明的版本范围、dsh.engines.dsh 的镜像、peer 声明,以及 compatibility.json。若你希望"找不到宿主"就判失败(例如发布闸门),加 --strict。

它不发布任何服务,因此不需要 isolate realm,只消费宿主能力(llm、subagents、tools、systemPrompt)。

使用

无需配置。提个需求,代理就会路由它:

"重构这个解析器,然后跑基准测试,最后把改动写出来。"

你也可以显式引导它:

- /model-orchestrator  —— 无论代理本来会怎么决定,都把这一个任务交给编排器路由。见下文。
- Settings → Model Orchestrator —— 模式、能力领域、成本偏好、并行度、路由允许/拒绝列表、实时模型池、路由预览、能力分工。

/model-orchestrator 命令

自动路由是默认行为,但没有任何东西强制调用方模型去路由,而不是自己生成子代理——而且曾观察到一次真实会话通过原生 subagent 工具派发了四个异质的研究单元,让每个子代理都落在部署唯一的默认模型上。这个命令正是为这种情况准备的显式开关:

/model-orchestrator 分析这个季度的销售数据并写成一份给管理层看的总结报告,包含趋势图表和三条行动建议
/model-orchestrator status

handler 运行时命令行不会到达模型(这是宿主的命令契约),所以该命令会自己把任务作为一条普通用户消息投递出去,并附带指令:调用 orchestrate_run,并在各单元性质不同时按单元指定路由。之后 captain 照常持有结果。

诚实说明其边界:这让意图变得显式、投递可靠,但路由仍然由代理执行——该命令不会绕过 captain,也不会让编排变成自动行为。自动仍是默认;这个命令用于你希望它被保证执行的时候。recordInput: false 避免任务被记录两次,而且只有当部署挂载了 commands 服务时,该命令才会被注册。

它会用你的语言回答。 该命令的回复、它的一行摘要和它的输入提示,全都来自面板所用的同一张 locale 表。语言来自你设置过的持久 locale 选项;当你没有设置时,则来自面板告诉宿主它正在渲染哪种语言。后一个来源不是锦上添花:harness 解析语言的顺序是「显式设置 → 浏览器探测 → en」,而且从不把浏览器推导出的值写回宿主,所以没有它,宿主在默认情况下根本无从得知界面语言,这些文案在中文界面里会一直是英文。插件只读取该设置、从不写入,而且显式选择永远优先于浏览器的推断。

harness 对第三方命令的描述与提示是原样渲染的——它只翻译自己内置的命令——所以语言变化时插件会重新注册该命令;这次重新注册是它的命令面板条目能跟随你切换语言的唯一途径。设置页也会用面板自己的语言写明该命令、它的提示和它的用法,因此无需打开命令面板也能发现这段文案。

工具

| 工具 | 用途 |
|---|---|
| orchestrate_run | 分析、匹配、派发每个单元,并返回全部结果。主入口。 |
| orchestrate_dispatch | 把一个自包含单元派发给一个模型。更便宜,也更可预测。 |
| orchestrate_ask | 向同一次 run 中一个已完成的单元提出后续问题并取回它的答案,且使用当初完成该工作的那条路由。 |
| orchestrate_plan | 展示路由决策,但不执行它。 |
| orchestrate_models | 当前真实存在的模型,以及每个画像背后的证据。 |
| orchestrate_capabilities | 能力词汇表,包括已学到的内容。 |
| orchestrate_configure | 修改偏好设置:模式、成本、并行度、路由、推理档位、能力分工表、依赖交接,以及同辈提问与评审的边界。 |
| orchestrate_status | 当前模式、模型池、映射关系、分工表、研究到的公开事实、哪些 cue 组仍是内置的,以及健康报告。 |

每个会收到调用方分析的工具都会说明每个字段该放在哪里,并强制执行这一点,而不是选择相信:被放到顶层的值会以 misplacedArguments 回报,并给出应当改用的参数路径(路由属于 units[].route 或 analysis.modelPreference[].route);无法识别的参数会以 unusedArguments 返回;顶层的 modelPreference/unitModelPreference 会被折进 analysis,并在 foldedIntoAnalysis 中指名。一条被静默忽略的路由不可能发生——但这份报告必须被阅读,这正是描述里要点名它的原因。

模型池里有哪些模型

发现过程读取 LLM 注册表,它列出每个已注册适配器所广告的全部模型。这并不等于某个部署打算让你使用的那一组:挂了两个 provider 的 profile 常常把同一个底层模型广告两次,而一个 provider 广告的模型也可能多于用户启用的数量。

所以模型池由两层依次收窄:

1. 部署的子代理路由策略 —— subagentModelSelection.current(),也就是 Settings 页面在子代理模型选择处展示的那些确切路由。由于这个插件运行的每个模型都是一条子代理路由,这就是权威答案。
2. 你自己的路由偏好 —— orchestrate_configure 的 allowedRoutes / deniedRoutes,叠加在其上。

让它保持安全的规则:

- 只有当该服务存在、已启用、且至少指定了一条路由时,策略才会约束模型池。策略缺失、被禁用或为空,意味着部署没有表达偏好,发现结果照旧成立——过滤到空会静默地禁用路由。
- 路由按 provider/model 精确匹配。两个 provider 暴露同一个模型 id 时,它们是不同的模型,除非策略排除其中一个,否则两者都保留;id 从不做去重,因为那会丢弃一条合法路由。
- 当策略会导致没有任何可路由项时,这会作为问题上报,而不是显示成一个空模型池。

面板会说明是哪一层收窄了模型池,因此更短的列表读起来是一个决策,而不是故障:

Showing the 7 route(s) this deployment offers for subagents;
4 advertised route(s) are not selectable.

不暴露推理档位的 provider

并非每个 provider 都提供一个可选的档位。DSH 以三种状态报告推理能力,模型池会显示某条路由处于哪一种:

| 状态 | 含义 | 模型池显示什么 |
|---|---|---|
| adjustable | provider 暴露档位(low、high、……) | 一个选择器,其选项就是它报告的档位 |
| automatic | 模型会推理,由 provider 决定深度 | automatic —— 没有选择器,因为没有什么可选 |
| none | 未报告任何推理信息 | 一个短横线 |

处于 automatic 状态的路由默认按原样使用:不发送任何档位,于是 provider 做的正是它本来会做的事。如果你知道自己的 provider 接受一个它没有列出的档位,你仍然可以设置——orchestrate_configure { reasoningEffort: { "": "high" } }——它会被接受、被发送,并在模型池中标记为 manual。没有任何东西能验证它,所以 provider 拒绝的档位会让那次派发以适配器自己的错误失败;这就是不去静默忽略你所提要求的代价。完全不报告推理的路由无法被指定档位,而列出了档位的路由则保持严格规则:一个已不在其列表上的 id 属于过期条目,会被忽略。

无论档位由谁提出,都不会被发送给一条无法表达它的路由。已存储的偏好、调用方自己提出的单元素档位、以及能力描述符声明的档位,都会针对目标路由逐一校验;路由没有声明的档位会被丢弃——该路由随后自行解析默认值——并在该单元的结果里报告为 effortUnavailable。过去它会被照发,理由是调用方的判断高于插件,结果适配器拒绝了这条路由,该单元一个字的回答都没有产出。

这一区分在路由中很重要,不只是面板里。需要推理的能力同时接受 adjustable 和 automatic;而一个指名档位的需求(例如"必须暴露 high")需要 adjustable,因为一个无法被选中的档位满足不了它。为一条不报告档位的路由设置档位,会在设置时被拒绝,若它过后过期则被忽略——发送不受支持的档位会让子代理直接失败。

按路由设置推理档位

模型池的 Reasoning 列是一个选择器,不是标签。它的选项是宿主为该路由实际报告的档位(reasoning.efforts),外加 default——意为"让模型自行解析档位",并在宿主报告时显示它是哪一个(reasoning.defaultEffort)。该选择按 provider/model 存储,并在编排器每次向该路由派发时作为 agentOptions.reasoningEffort 生效,因此 captain 会按你设置的档位派发。

让它保持诚实的规则:

- 路由未广告的档位在设置时被拒绝,若它过期则被忽略(适配器变更)。发送不受支持的档位会让子代理直接以 UNSUPPORTED_REASONING_EFFORT 失败,因此过期偏好会退化为模型默认值——而且模型池会说明这一点,而不是把它显示得像是生效了。
- 不广告任何档位的路由不会得到选择器。
- 调用方模型为某个单元声明的档位优先于已存储的偏好:读任务的模型才是那个单元更好的判断者。但若目标路由无法表达这个档位,它会被丢弃并在该单元结果里以 effortUnavailable 报出,而不是让整个单元失败。已存储的偏好、调用方逐单元的声明、能力描述符声明的档位,三者都会被按目标路由校验——路由没有广告的档位一律不发。
- 只有档位可配置。这个插件不发明档位,并且在选择逻辑中依然不指名任何模型。

同一个偏好也可以从工具界面设置,供被要求配置它的代理使用:

orchestrate_configure { reasoningEffort: { "commandcode/xai/grok-4.6": "high" } }

模型池的各列

| 列 | 来源 |
|---|---|
| Route | 部署自己的 provider/model 字符串 |
| Public model | 来自 Sync。同步前为空白;当研究无法把该路由对应到一个已发布的模型时,明确标记为未确认 |
| Cost ($/M tok) | 来自 Sync:已发布的每百万 token 标价,并带一根相对于本模型池中最贵路由的条形——单看价格回答不了"这贵不贵" |
| Context、Image | 由宿主测量 |
| Reasoning | provider 列出档位时是选择器,只推理而不列档位时是 automatic,未报告时为短横线 |

模型 tier 列已移除:deep / balanced / fast 是这个插件自己的词汇,而它旁边那些列——上下文、成本——才是它当时在概括的实测输入。

能力分工 —— 你固定下来的分工

如果你想要一种特定的分工,而不是按任务逐个判断——视觉交给一个模型,数学交给另一个,架构交给贵的那一个但实现交给便宜的——在 Settings → Model Orchestrator → Capability assignments 里设置一次,或者用工具设置:

orchestrate_configure { capabilityAssignments: {
"multimodal.vision":       { models: ["gemini-3.8-flash"] },
"reasoning.mathematics":   { models: ["Qwen 3.8 Max 0902"] },
"software.architecture":   { models: ["gpt-5.6-sol"] },
"software.implementation": { models: ["deepseek-v4.1-flash"] },
"web.information":         { models: ["grok-4.6"] },
"long.context":            { models: ["Kimi K3"] }
} }

条目的键是一个能力 id 或整个能力组,值是一个有序的模型身份列表。模型以身份存储,而不是路由,因此这张表比模型池活得久:

| 模型池的变化 | 由什么吸收 |
|---|---|
| 模型迁到另一个 provider,或它的路由被改写 | 身份匹配——上表中的行写的是 gemini-3.8-flash,而不是某条路由 |
| 发布了新版本(5.6 → 5.7) | 每条目上的 follow the family 开关,默认关闭:版本号变化通常是同一个模型,但不总是 |
| 模型离开了模型池 | 该条目自我报告为未解析;路由会落到下一个条目,再到实测排名 |
| 出现了没有被任何条目提到的模型 | 上报为未分配的实时路由——这是留给你做的决定,而不是表格悄悄替你做的决定 |

有两件事它刻意不是:

- 它不是一把锁。 表格提供偏好顺序;匹配器仍会重排合格候选,仍会强制执行硬性需求和部署的路由策略。表格条目永远无法复活被它们排除掉的路由。
- 它不是一把锁,但它确实是第一优先级。 你已配置的能力遵循这张表;要让位的是调用方模型自己的偏好,而单元结果会说明这一点(decidedBy: "assignment",外加指名被顶替了什么的 overriddenCallerPreference)。完整阶梯是:这张表 → 调用方模型的按单元选择 → 调用方模型的任务级偏好 → 实测排名。这是刻意反转了早先的顺序——你做过一次的决定,不该被碰巧在调用的那个模型悄悄推翻。

这两个偏好都写在 analysis 内部(任务级用 modelPreference,单个单元用 unitModelPreference)。若被放到调用的顶层,它会被折进 analysis 并回报为 foldedIntoAnalysis,因为这个错误曾让一个七单元的研究计划静默地全部落到同一个模型上;而工具不认识的任何其他参数会以 unusedArguments 报回,一个放错位置的已知参数则会得到一条 misplacedArguments 记录,指名它该放在哪里。

调用方自带的 units 也按同一套阶梯路由,那张表也包括在内。它们过去只查询调用方自己的偏好,因此自带一个单元图会静默绕过你所配置的分工。

同一个 cluster 内的能力在分工不同时会被拆成独立单元。这正是"架构给 GPT、实现给 DeepSeek"能成真的原因:两者都位于 software cluster,如果不拆,它们会合并成一个单元、落在同一个模型上,表格就会静默地什么都不做。

插件在选择逻辑中依然不指名任何模型——这张表是你的。身份匹配只回答"这是不是仍然是同一个模型";它从不判断哪个模型更擅长什么。

两个面板如何关联

它们并排放在一起,回答的是不同的问题,这也是它们不可能互相矛盾的原因:

| 面板 | 问题 | 控制什么 |
|---|---|---|
| 能力领域(Guided) | 本次会话涉及哪些类型的工作? | 需求集合:它以权重 0.7 为某个能力播种,因此会产出一个光靠任务文本不会出现的单元 |
| 能力分工 | 每种工作由谁来做? | 单元所匹配路由的偏好顺序,以及同一 cluster 内的两个能力是否拆成独立单元 |

所以组合关系是:领域决定存在什么,分工决定谁来做。 只有两者都指名同一个能力时才会重叠,而那种情况下也没有什么需要调和的——领域让需求存在,分工为它选定路由。

有两条规则维持这一点,它们都是新近才强制执行的,此前都被破坏了:

- 领域只在 Guided 模式下生效。 它们过去无论什么模式都会播种,于是在 Auto 打开时仍被选中的领域会静默地持续影响路由——而面板却说它们在 Guided 下生效。Auto 的意思是"读每个任务并自行判断"。你保持选中的领域是为 Guided 回来时留着的,不会被应用。
- 调用方自己的需求不再丢弃它们。 此前只要调用方声明了任何需求,intake 就整体返回调用方的分析,于是一个有问必答的调用方会静默覆盖用户自己的会话设置。现在它们会被合并:对两者都指名的能力,调用方胜出(它是关于这个任务更具体的陈述);调用方没有指名的领域会被加上,因为别的什么都不会把它带出来,而用户要求过它。结果里会报告 source: "model+guided"。

有一个后果值得知道:分工只对成为需求的能力生效。它是路由策略,不是触发器——指派 web.information 并不会让一个任务涉及 web。能力领域可以充当那个触发器,这是两个面板组合出彼此单独都做不到的效果的唯一方式。

匹配如何工作
score(model, task) = geometric_mean( satisfaction(requirementᵢ, model) ^ weightᵢ ) × confidence
− cost_shaping

- 合取,而非平均。 一个做不了某项必需事情的模型,不会因为擅长相邻的事而被救回,因此每个需求的满足度是相乘而不是取平均。
- 硬性需求会拒绝。 模态、上下文下限和推理可用性在打分之前就被检查。未知模态被当作未知,绝不当作有能力。
- 能力水平是测出来的,不是从文字推断的。 诸如 depth.difficult 这样的深度需求,会依据宿主测量到的事实(暴露的推理档位、上下文窗口)来解析。这正是路由能对一个从未有模型自称覆盖过的领域也生效的原因。
- 成本塑形永不覆盖需求。 它只用于打破平局。

证据层级

每个画像都会记录其证据来自何处:

| 来源 | 含义 |
|---|---|
| metadata | 宿主的权威事实:模态、上下文窗口、输出上限、推理档位。 |
| declared | provider 自己的模型名和描述。软证据。 |
| calibrated | 模型对编排器自身自探针的回答。 |

匹配器把实测事实的权重置于推断之上,并报告每个候选的证据,使决策可被审计。

兼容性与版本

宿主范围在生态读取的两处都有声明:

{
"engines": { "dsh": "0.1.5-rc.1" },
"dsh": { "engines": { "dsh": "0.1.5-rc.1" } }
}

engines.dsh 是权威;dsh.engines.dsh 为市场上的发现镜像它。peerDependencies 声明实际导入的 @deepseek-ai/ 包,并被独立检查。

每次激活时,在注册任何东西之前,插件都会证明三件事:

1. 声明的范围接受正在运行的 DSH 版本(从已安装的树中解析,并用宿主自己的 semver 求值)。
2. 每个必需的服务及方法都按名称存在。
3. 至少注册了一条带有可用 id 的 provider 路由——刻意不要求能响应模型列表,因为那可能是一次网络往返(见性能一节)。

任何一项检查失败,插件就不注册任何工具、提示词小节或路由,记录一个精确原因(指名该需求与发现到的东西),并抛出,让该行大声失败。它绝不静默降级。因为这道门在每次激活时都会运行,升级进入不支持的宿主会被拒绝,而不是被运行。

这道门之后的一切都是隔离的。 这道门本就该大声——不兼容的宿主是一个决定,不是一次意外。但过去每个子系统的注册同样没有保护,而一个 composition 条目失败并不是局部失败:harness 的启动审计视之为致命错误并销毁整个 context,于是一个坏掉的工具 schema 会连带拖垮无关的插件。现在每个子系统都在一个 guard 之内注册。失败只会禁用那个子系统、记录原因,并让其他一切继续工作。

这就是健康(health)这一行所报告的内容,在设置页和 orchestrate_status 上都是:每个子系统为 enabled、degraded 或 disabled 并附原因,此外还有这个部署没有提供的可选服务。一个没能挂载的控制面板,或一个因为没有搜索 provider 而永远不可能工作的 Sync,现在是一个可见状态,而不是一条日志。可选服务在每次读取时都会重新检查,因此一个挂载得晚的服务不会被报告为永久缺失。

随时可以运行独立检查:

node scripts/check-compat.mjs

环境要求

- DSH 0.1.5-rc.1(精确锁定;见 compatibility.json)
- Node.js ^22.19.0 || >=24
- 一个挂载了 llm、subagents、tools 和 systemPrompt 的宿主 composition —— 随附的 web 和 headless profile 四者齐全。
- 至少一个已注册、能列出模型的 LLM provider。

客户端面板还额外需要一个 web 服务器;没有它时工具照常工作,只有面板不可用。

运维

禁用与移除

harness 设置里没有通用的启用/禁用按钮:Plugins 设置页只配置插件,不停用它们。关掉一个插件要通过 loader row,这是官方机制,不需要插件做任何代码改动。

禁用:向该 profile 的用户 patch 层加一行:

dsh plugin --profile  add dsh-model-orchestrator   # if not installed yet

cat >> "$DSH_HOME/profiles//cordis.patch.yml"  remove dsh-model-orchestrator

移除会在下次启动时撤回全部工具、提示词小节和控制路由;profile 中其他内容不受影响。由于插件不注册服务、不持有任务状态,移除不会让任何会话搁浅。

社区市场插件只管理它自己安装的东西,所以用 dsh plugin add 添加的插件
不会出现在那里供你切换。请使用上面的 patch 行。

宿主不受支持时

一个 engines.dsh 不接受正在运行的 DSH 的构建会拒绝激活。composition 行会大声失败,给出需求、发现到的版本,以及一行恢复建议:

dsh-model-orchestrator 0.0.1: incompatible host — the plugin is disabled
1. DSH 0.1.5-rc.1 does not satisfy the declared range "0.9.9-rc.1" (>= 0.9.9-rc.1)
Required host range: dsh 0.9.9-rc.1  (running: 0.1.5-rc.1)
Recovery: remove the plugin from this profile with dsh plugin --profile  remove dsh-model-orchestrator, or install a build whose "engines.dsh" admits this host.

这是刻意的:它什么都不注册,也绝不静默降级。在安装前、或在 CI 中检查一个构建:

node scripts/check-compat.mjs

升级

这道门在每次激活时都会运行,而不只是安装时,因此升级进入不兼容的宿主会被拒绝,而不是被运行。偏好设置、学到的能力描述符和校准值存放在 $DSH_HOME/orchestrator/state.json,能跨升级保留;离开模型池的路由,其校准值会被自动清理。

界面

Settings → Model Orchestrator 是完整页面:模式、能力领域、带每个评估依据的实时模型池、偏好设置,以及路由预览。

能力领域属于 Guided 模式,页面也这么说明:在 Auto 下该面板是折叠的,并有一行说明它在哪里;选择 Guided 时,它以短促的滑动淡入展开,而不是瞬间替换布局。过渡约 340 ms,prefers-reduced-motion: reduce 会完全关掉它,文案称呼模式的方式与按钮一致(自动 / 引导,绝不混用译名与英文模式名)。

Orchestrator 是一个 Conversation 视图,与 Chat 和 Trajectory 同级。它就是你正在查看的那个会话的看板:一张自左向右的依赖图,描绘任务是如何被派发出去的——从任务本身,经过 captain,到编排器派发的每一个单元(包括某个单元又依次派发的子代理),一直到最终输出,而它永远是最右侧的节点。

- 布局 —— 每个依赖深度一列,因此阅读的方向就是工作的方向。边被绘制成曲线,其控制点始终留在列与列之间的空隙内,所以一根连线永远不会穿过节点框;共享同一来源或同一目标的边会分散在不同泳道上,彼此不重叠。
- 六种边,各自绘制方式不同,且全部列在图例中:task(任务指向它启动的东西)、dispatch(谁派发给了谁)、dependency(最强的一条线:这个单元必须等待那一个)、question(虚线——一个单元向已完成的同辈问了点什么)、review(有自己的颜色,并标注轮次与裁决),以及 output(一个已定局的单元汇入最终答案)。一根触及正在运行对象的连线会流动。
- 五种节点,全部列在图例中:任务、captain、被路由的单元、harness 自行启动的会话(一次原生派发——编排器没有为它选择任何模型),以及最终输出。
- 完整的生命周期 —— waiting(并指名它在等谁)、not started、running、completed、failed,以及 unknown。只有被路由的派发才显示模型:路由来自单元自己的记录,而不是对 label 的推断。
- 对不知道的事保持诚实。 harness 的列表报告的是一个会话记录是否驻留,这与是否真有代理在运行它并不是一回事。因此插件自己的运行日志优先级更高——编排器已完成的一个单元会报告 completed,即使持久记录仍说 running——而任何其他持续声称自己运行了超过半小时的,都会变成 unknown,原因写在其 tooltip 中。对于一个已不再运行该会话的宿主,它不会永远显示"running"。
- 交互 —— 拖动一个节点,它的连线会跟着走;悬停一个节点或一根连线,所有无关内容都会变暗;每根连线都有一个很宽的无形命中区域,因此悬停它会高亮它所连接的两个节点,并显示两端各自的类型。
- 每个节点的详情 —— 能力、路由以及由谁决定、评审轮次、耗时、它提出和回答了多少个问题、它写下的答案的路径、一段摘要,以及失败时的错误。过长的值会在框内截断,完整文本放在 tooltip 中,而 label 在两种语言下都会在框内换行。
- 一条顶栏,显示本次 run 的数字:已派发、运行中、已完成、失败、提问数与评审数,以及这次 run 已经花了多久。它下方是分组图例,覆盖每一种节点类型、每一种生命周期状态和每一种边的样式——由图渲染器所用的同一套词汇构建,因此不可能彼此漂移。

这张图合并了两个来源。harness 会话树(ctx.subagents.listDescendants)是真实的拓扑——每个持久会话一个节点,每条上报的父子关系一根边——而插件的运行日志补上了 harness 从来不知道的东西:一个单元覆盖了哪个能力、它跑在哪条路由上、它产出了什么以及写到了哪里、谁评审了它、谁向谁提了问,以及单元之间的依赖边。单靠任何一方都画不出这张图。

输入框上方刻意没有常驻条:看板才是检查派发工作的地方,再加一条既会与它重复,也会挤占输入框。

两者都遵循 harness 的语言设置:每个面向用户的字符串都位于插件的 modelOrchestrator locale 命名空间中(lib/locales.js,并在自包含的客户端 bundle 内镜像),覆盖两个随附 locale。模型读取的字符串——工具描述、参数 schema、路由提示词小节、persona——刻意保持英文,不跟随 UI 语言。

设置页还带有一行健康(health)信息:哪些子系统处于活动状态、哪些处于降级(degraded)以及原因、哪些被禁用以及原因,以及这个部署没有提供哪些可选服务。一个没能挂载的面板,或一个永远不可能工作的 Sync,会在那里可见,而不是变成一条没人看的日志。

能力名称沿用同样的划分:分类体系的 label 面向模型,保持英文,因为它们会进入子代理提示词和派发 label,而面板通过自己的 capability.label. 条目翻译它们——于是「能力领域」以中文呈现,而代理收到的仍是 Testing and verification。尚未有条目的已学能力会回退到它的分类体系 label。

术语政策:通用和技术术语保持不译。host / requires 版本标签、provider、Route、诸如 workflowEngine 这样的部署 id、模型名与路由名、reasoning-effort id,以及像 0.1.5-rc.1 这样的版本字符串,都是数据或共享标识符,因此在每种语言中都原样出现。只有解释性文字才被翻译。

两者都由宿主通过同源路由提供:state、configure、plan、tree(看板的派发关系图)与 sync(研究扫描)。浏览器那一半无法自己枚举模型——LLM 列表接口只存在于宿主——因此面板从宿主读取真实模型池,从不猜测。

多单元计划如何执行

单元默认并行。各部分相互独立的计划(这是常见情形)按其 maxParallel 设置分波跑完,单元之间互不等待。

chain: true 则把计划变成流水线:每个单元收到前序单元的发现,这正是真正的先后序列("先研究、再评审、后总结")所需要的。它以前是自动的,而那是错在相反的方向:一个含八项需求的研究任务被串成七个顺序代理,每个都自己做检索、每个都要等前面全部完成 —— 结果撞上调用方 30 分钟的工具调用上限,返回一个超时错误、零结果,因为超时丢的是全部而不是已完成的部分。决定这一点的是代价的不对称:并行单元可能少一点跨单元上下文,而串行一旦超时就全都拿不到。调用方若自己提供 units,无论如何都完全掌控依赖图,其 dependsOn 永远不会被改写。其中每个单元都可以自带 route 来指定路由,也可以不写、由它声明的能力来路由;若指定的路由已不在实时模型池里,会降级为按能力路由并以 routeRequested 说明它原本要哪一个,而不是让这个单元因为一个过期的名字而丢掉。另外,自带单元的 prompt 请写短:本次 run 的 task 会被自动加到每个单元的提示里,重复写只会让这个参数大到写错的概率上升。

一次运行也会自我限时。 budgetMs(默认 25 分钟)会在调用方自己的工具调用上限之前中止本次运行,从而返回已完成的单元、把其余标为未完成,并置 budgetExhausted。没有它,那个上限就是唯一的限制,代价是整次运行。若你自己的工具调用上限比默认值更短,请传更小的 budgetMs。

一个单元从它依赖的单元那里收到什么

声明了依赖的单元会收到该依赖的完整答案,若有结构化结果也一并收到。这正是"依赖某个东西"的含义——预览恰恰会丢掉依赖单元所需要的推理。其他每个单元仍然只收到近期完成项的有界摘要,绝不是某个同辈的全文,因为它之所以独立,原因正是它不需要全文。

有三个设置会改变这一点:handoff.strategy(full,默认,或 summary)、handoff.maxChars(500–200 000)和 handoff.includeStructured。

每个答案都会被写下来

一次 run 的结果很容易大于 harness 愿意内联携带的大小,而一旦如此,harness 就会用一个有界预览加一个定位符来替换它——这过去意味着调用方模型只会被告知结果"被存在了某处",却读不到任何一个单元的答案。

因此每个单元的完整答案都会在结果被限界之前先写入文件:run 目录里每个单元一份 Markdown 文档,外加一个 index.md,列出每个单元、它的路由、它的结果和它的字节数。结果里会带上 results[].artifact.path 以及 run 级的 artifacts.dir / artifacts.index;被截断的内联答案会说明省略了多少、其余在哪里(textTruncated、elidedChars)。当已知调用会话的工作目录时,该目录是它里面的 .dsh-orchestrator/artifacts,否则就是插件自己的状态目录。写入是尽力而为的:只读工作区会降级为"本次 run 没有 artifacts",并上报在 artifacts.problems 中,而这次 run 不受影响。

向一个已经回答过的单元提问

orchestrate_ask 向同一次 run 中一个已完成的单元发送后续问题,并在当初完成该工作的那条路由上返回它的答案。每个单元都会在自己的提示词中得到本次 run 的 id 以及它可以提问的单元列表,因此这项能力是可发现的,而不是停留在理论上。

仍在运行中的单元会直接拒绝——它还没有答案,把问题排队要么会让提问者死锁,要么会重复工作——而拒绝时会指名哪些单元可以回答。提问按每次 run 和每个提问单元设上限(questions.maxPerRun,默认 4),并有超时(questions.timeoutMs,默认 4 分钟)。run 结果会报告每一次提问与回答。

评审循环

声明了 reviews: [ids] 的单元是一个评审者(reviewer)。它被排在它所评审的单元之后,收到它们的完整答案,并应当这样回答:

{ "verdict": "approve", "objections": [{ "unit": "impl", "issue": "no tests were mentioned" }] }

一次拒绝会把每个被提出异议的单元连同异议一起退回——这样它是修订,而不是从头再来——然后评审者再次裁决。这个循环由 review.maxRounds 限定(默认 2)。有两条规则让它安全,而不只是有限:无法被读取的裁决会被记录为 unknown,并且循环停止,因为一份读不懂的答案不是批准;而一个先给出了答案、随后失败的子代理绝不会换一个模型重跑,因为那是内容问题,再付一个模型的钱也修不好它。

谁来决定用哪个模型

选择是一种分工,因为任何一方都无法独自完成。

插件了解部署:它发现实时路由,应用子代理路由策略,强制执行硬性需求(声明的上下文下限、必需的模态),并报告实测事实——上下文窗口、模态、输出预算、推理档位。

调用方模型了解模型。某个不透明的路由 id 对应的是视觉强的模型还是数学强的模型,是插件没有、也绝不能臆造的公共知识;部署的别名也不见得匹配任何公开的模型名。

因此 orchestrate_run 和 orchestrate_plan 接受 analysis.modelPreference:调用方模型按最偏好优先的顺序,指名它认为最好的路由,并给出理由。对于各单元需要不同模型的计划——视觉单元和数学单元很少想要同一条路由——analysis.unitModelPreference 按精确能力 id 或按 cluster 逐个指名它们,对该单元最具体的目标胜出。没有匹配到任何条目的单元保持任务级偏好。规则如下:

| 规则 | 原因 |
|---|---|
| 偏好会重排合格候选 | 它是判断,不是约束 |
| 偏好无法复活被拒绝的路由 | 你的需求始终是权威 |
| 无法识别的名称会被回报 | 绝不静默丢弃 |
| 格式错误的条目会被丢弃 | 不能凭空造出半成品的路由 |
| 没有偏好时,实测排名照旧成立 | 插件单靠自己也能工作 |

同一小节还给出了完整阶梯,从高到低,因为一个看起来被忽略的偏好与一个 bug 别无二致:

| 层级 | 由谁设定 |
|---|---|
| 能力分工 | 用户固定下来的策略,在面板中设置一次——第一优先级 |
| analysis.unitModelPreference | 调用方模型,关于一个单元 |
| analysis.modelPreference | 调用方模型,关于任务 |
| 实测排名 | 插件,依据宿主事实 |

用户已配置的能力始终遵循那张表。这是对早先阶梯的刻意反转,那时调用方的按单元偏好可以压过它:表是用户做过一次、并期望它成立的决定,一个爱多说的调用方不该悄悄击败它。这次反转不是静默的——见下一段——而且那张表对调用方自带的单元也一样会被查询,此前并非如此:自带一个单元图过去会完全绕过你配置的分工。

由谁决定写在结果里。 每个单元都会带上 decidedBy —— assignment、caller-unit、caller-task、caller-pin 或 measured —— 而当那张表顶替了调用方的偏好时,该单元还会带上 overriddenCallerPreference,逐字指名被顶替的是什么。routeReason 用文字说明同一件事,因此"这个单元为什么在这个模型上"可以从 run 本身回答,而不必靠推断。

有序的表会顺次向下尝试。 一个能力可以按优先级顺序指名多个模型,而无法作答的路由会落到下一个候选。但只有当子代理什么都没产出时才会如此:一个先回答了、随后失败的子代理是内容问题,再为它付第二个模型的钱也修不好。

判断权属于模型

任何关于任务的判断都该由调用方模型来做。插件自己的线索列表是回退,用于没有模型提供分析的时候——绝不是可以推翻模型的权威,也绝不是唯一能被理解的措辞。

| 插件 | 模型 |
|---|---|
| 测量事实:上下文窗口、模态、输出预算、推理档位 | 读任务并判断它需要什么 |
| 强制执行路由策略与硬性需求 | 用自己的词汇指名能力 |
| 在没有任何输入时回退到线索列表 | 陈述复杂度和模型偏好 |

具体来说:

- 模型提供的分析按原样使用。它的 complexity 不再与本地解读做调和——那种调和曾把一个自称 complex 的模型降级为本地观察到的 specialist,并把一个 trivial 的声明提级。
- 回退词汇表位于 lib/decision-vocabulary.js,并可通过 orchestrate_configure 的 decisionCues 替换,因此运维者永远不会被作者的措辞困住。orchestrate_status 会报告哪些分组仍是内置的。
- 回退列表刻意保持精简,并被文档化为提示,因此它们不会被误当成"任务是什么"的定义。

插件不会做什么

它不会把两条共享模型名的路由合并。在此处测量的部署中,deepseek-official/deepseek-flash 与 commandcode/deepseek/deepseek-v4.1-flash 指同一个公开模型,但输出预算相差 4 倍(256k 对 64k),位于不同 provider 之后,计费方式也不同。把它们合并会丢弃真实的、已测量的差异,并破坏路由,而路由需要精确的路由。

它也不会自己从网络抓取公开的模型数据。部署中的路由 id 往往根本不是公开模型,而一个从名字猜测它们身份的插件将是在臆造能力,而不是测量能力。它提供的是 Sync:一个由你按下的动作,它会研究模型池并展示发现的结果、附上来源,并把无法确认的内容明确保持为未确认。

来自网络的模型事实(Sync)

宿主完全不报告定价,这让成本偏好成了一个什么都不做的开关:像这样模型池里的每条路由都测得同一档,于是它所塑造的打破平局处处相同。模型池里的 Sync 用公开事实补上了这一点:

- 一条路由是哪个公开模型。 路由 id 是部署自己的字符串,常常重复发布者(commandcode + id deepseek/deepseek-v4.1-flash,广告名 DeepSeek V4.1 Flash (CC))。Sync 会解析它,或者报告它无法解析。
- 已发布的标价,按每百万输入和输出 token 计。
- 公开来源说这个模型擅长什么,以及这些说法出自哪些 URL。

Sync 是插件自身在网络上采取的唯一动作——唯一一个由它选择成本与内容的动作。它刻意不是自动的:激活时或轮询时都不抓取,只有按下按钮才做一次清扫。(harness 自己的模型发现在刷新模型池时确实会与 provider 通话;这就是插件让发现过程不进入启动路径、也绝不在轮询时重跑它的原因。见性能。)
在没有 web 服务的部署上,Sync 会报告它无法研究,其他一切不受影响——该服务和命令注册表一样是可选的。它通过 harness 自己的 web 服务为每条路由搜索一次网络,然后用一次模型调用把来源归整为事实——模型判断来源,插件决定它被允许看到什么,且不存储验证器无法核查的内容。

一次扫描按 provider 分组,一次失败只让一个 provider 付出代价。 它过去是对整个模型池的单次调用,于是一个联系不上的 provider——或一个以散文形式返回的 reconciling 答案——会丢弃其他所有 provider 的事实,并只报一个错误、零结果。现在每个 provider 各自被研究,它的事实一落地就写入,因此扫描会以 done、partial 或 error 报告自身,并附带每个 provider 的结果;一个从未作答的 reconciling 路由会先在下一个候选路由上重试。

失败会说明它看到了什么。 the research answer contained no JSON object 曾是全部的诊断信息,从中什么都推不出来:不知道模型是答了散文、什么都没答,还是以解析器漏掉的方式包裹了它的 JSON。答案的长度、它的开头文字、是否存在围栏代码块、reconciling 路由及其分块数,现在都作为 sync.detail 随失败一起传递,而消息本身也会指名它们。

| | |
|---|---|
| 未确认就保持未确认 | 研究者无法对应到已发布模型的路由会以未确认存储,绝不给予一个看似合理的名字 |
| 不做估价 | 只有来源明确给出价格时才使用。缺失就是缺失;免费额度不是标价 |
| 来源是记录的一部分 | 每个条目都带有其来源、读取时间,以及是哪条路由做的读取 |
| 它绝不覆盖测量 | 价格只塑造打破平局,且在该 tier proxy 曾用的同一个 0.08 上限内,并相对于本模型池中最贵的路由做归一化。硬性需求仍会在查询这一切之前就拒绝 |
| 它始终显示为已研究 | 模型池在每一行上显示它,标记为已研究;画像自身的证据仍读作 metadata,因此来自网络的内容不会被悄悄混入实测事实 |

preferCheaper 在有研究价格的路由上使用该价格,在没有的上面使用 tier proxy,因此启用研究会改变成本是从什么测量出来的,绝不改变它被允许有多大影响。研究条目按模型身份作键,因此 provider 迁移不会让它们变成孤儿——而一个不再能解析的条目,正是模型池那一行所显示的漂移。

布局

面板渲染为一个居中的、受限宽度的列,与随附的 Chat 和 Trajectory 视图的布局方式一致。这不是装饰:对话外壳对 transcript 的宽度手柄采用绝对定位,位置相对内容列偏移,因此一个满幅绘制的视图会把内容直接画到那些手柄下面。在一次真实会话中测得:面板列横跨 x 498–1272,而手柄位于 x 436 和 x 1304——避开了内容,这正是它们该在的位置。

面板采用外壳自己的内容宽度,因此拖动 transcript 宽度手柄也会改变它的大小——与 Chat 页面上输入框保持的行为一致。宽度是一条 CSS 链(外壳的变量,然后是它的一个被观察到的副本,再是外壳的 920px 上限)放在带下限的 clamp 里,因此一个未解析的变量不会让视图塌掉。

性能

插件不能让 harness 感觉变慢。有两条规则强制这一点,二者都是通过给真实 profile 启动计时而发现的回归:

1. 激活时不进行任何网络 I/O。 兼容性门最初用 listModels() 探测每个 provider,以证明模型池可用。provider 的模型列表可能是一次实时 HTTP 请求——内置的第三方 provider 每次调用都会重新抓取其目录,带 10s 超时,只在失败时回退到磁盘——于是这道门把每次启动变成了多秒等待。现在这道门只检查有一个 provider 路由已注册;provider 是否响应是运行时状况,作为模型池问题上报。实测效果:profile 启动从 6.7s 回到 3.9s,与无插件基线相差 3ms 以内。
2. 轮询绝不重读 provider。 面板轮询 /state,而发现过程原本每次读取都会重跑。现在 /state 提供它已有的模型池;只有显式 ?force=1(面板的 Refresh 按钮)或 harness 报告适配器变化时才会重新发现。/plan 也不再发现——预览一条路由绝不能打到 provider。

发现过程仍在激活时立即开始;只是不被 await,因此模型池相关的任何事都不会阻塞启动。第一次面板读取会等待那次在途的发现,而不是画出一个空模型池。

中断与重启

插件不持有任何持久任务状态,因此中断不会让它卡住。

如果你在编排中途取消会话,工具的信号会中止并到达子代理;子代理的结果被拒绝,这在运行中被记录为一个失败单元——永不挂起,永不出现未处理的 rejection,也永不留下静默的空缺。每个生成的子代理在每条路径上都恰好被销毁一次。

如果进程被杀(kill -9、崩溃、断电),内存里的东西都无关紧要了:

| 重启时 | 行为 |
|---|---|
| 偏好设置、模式、Guided 领域 | 从 $DSH_HOME/orchestrator/state.json 恢复 |
| 学到的能力描述符 | 恢复,并可再次使用 |
| 按路由的校准值 | 恢复;模型已离开模型池的条目会被清理 |
| 模型池 | 从实时注册表重新发现——从不恢复,因此不可能陈旧 |
| 路由注册 | 在激活时重新挂载 |
| 被截断或损坏的状态文件 | 回到默认值并报告原因,下一次写入会修复它 |
| 来自更新插件版本的状态文件 | 拒绝且不覆盖,因此降级不会损坏它 |

写入是原子的(临时文件,然后重命名),因此在写入过程中被杀只会留下旧文件或新文件——没有部分状态需要恢复。test/lifecycle.test.js 固定了以上全部行为。

已知的原生注意点

一条来自测试的观察,为完整起见而报告,而非作为插件缺陷:在一次运行中,对 dsh --profile headless 进程执行 kill -9 后留下了一个存活的 dsh 子进程(被 init 收养,仍持有一个网络套接字)。我没能在其后两次受控尝试中复现它——一次没有任何子代理,一次有编排器生成的子代理——而进程内子代理不可能比其父进程活得更久,所以那个子进程不是被派发的代理。如果你在杀掉会话后看到残留的 dsh,用下面的命令检查:

pgrep -fa 'dsh --profile'

开发

node --test "test/.test.js"   # 408 checks, no host required
node scripts/check-compat.mjs  # host compatibility report

测试套件无需真实 harness 即可运行。在真实宿主契约重要的地方——defineTool schema 编译器与无损 JSON 输出验证器、客户端模块加载器、版本解析——测试会解析已安装的宿主包并演练真实的东西,把插件实体化在一个指向宿主 node_modules 的符号链接旁边。

其中两项检查之所以存在,是因为一次真实启动发现了形状测试无法发现的缺陷:每个工具都缺少 output.render,以及一个被宿主的无损 JSON 验证器拒绝的 undefined 属性(JSON.stringify 会悄悄隐藏它)。两者现在都被固定住了,另有一条守卫确保插件永不长出任务或进度界面。

布局

lib/
index.js          plugin entry: gate, wiring, pool, tools, prompt, routes
compatibility.js  activation gate and host version resolution
taxonomy.js       open, domain-agnostic capability descriptors
discovery.js      live pool discovery and capability profiling
matching.js       task analysis, scoring, and route selection
engine.js         orchestration tiers, delegation, aggregation
persistence.js    atomic state: preferences, descriptors, calibrations, research
preferences.js    the ONE preference-patch implementation both configure surfaces call
tools.js          the orchestrate_* model-facing tools
schemas.js        tool names and parameter specs (host-free, so they are testable)
locales.js        zh/en dictionaries for the UI (mirrored into the client bundle)
routes.js         host control routes for the browser panel
commands.js       the /model-orchestrator human command (host-free, so it is testable)
reasoning-effort.js  per-route reasoning level: validation and merge, shared by both configure surfaces
model-research.js    public facts about models: prompt, validation, price lookup
web-research.js      the two-stage sync: web searches, then one reconciling model call
sync.js              one research sweep at a time, and its status
model-identity.js    model identity and family keys: which live route a stored intent means
assignments.js       the standing division of labour: normalise, resolve, report drift
agent-tree.js     subagent relationship tree and lifecycle truth for the board (pure, testable)
runs.js           the run journal: sibling questions, review rounds, and the board's edges
artifacts.js      each unit's answer on disk, plus the run index, with degraded fallback
arguments.js      how a tool call's arguments are read, and where a misplaced one belongs
health.js         which subsystem is enabled, degraded or disabled, and why
host-locale.js    the host half's view of the UI language (read-only, from settings)
route-policy.js   narrows discovery to the routes the deployment offers
prompt.js         the routing-policy system prompt section
client.js         client bundle: settings page + the Orchestrator board
home.js, util.js  harness-home and value helpers

参见 DESIGN.md 了解它所依据的、经过验证的宿主契约;docs/ 中有从已安装的 DSH 树中收集的完整 API 参考。

许可

MIT —— 见 LICENSE。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群