← 返回列表
⚠ 装前注意
把任务分发给多个子代理并验证汇总成裁决报告
基本兼容但装前注意:未发布到 npm registry,仅可从源码安装 · 最近上游提交 2026/9/16 · 已提供中文文档
DeepSeek Harness 的 map-reduce 子代理委员会:一个任务分发给各个独立成员,它们的发现经过去重、由单独的小组验证,并归约为法定人数报告
综合分
30.7
GitHub 分
30.7
用户评分
—
★ Stars
1
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add starsinc1708/dsh-tool-council未发布到 npm registry,仅可从源码安装,改用 GitHub 源安装
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查⚠ 装前注意
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包@starsinc1708/dsh-tool-council(未发布到 npm,仅可源码安装)
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
未发布到 npm registry,仅可从源码安装
验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/19 10:27:20
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-agent-presets@deepseek-ai/dsh-api-session-controller@deepseek-ai/dsh-client-connection@deepseek-ai/dsh-client-locale@deepseek-ai/dsh-client-ui-chat@deepseek-ai/dsh-client-ui-conversation@deepseek-ai/dsh-client-ui-renderer@deepseek-ai/dsh-client-ui-session@deepseek-ai/dsh-client-ui-settings用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
@starsinc1708/dsh-tool-council
一个用于 DeepSeek Harness 的 map-reduce 子代理议事会:一个任务通过不同的视角分发给多个全新的子代理,验证器从源头重新检查每一项发现,法定人数将其投票转化为裁决表。
该插件是工作流和子代理接缝之上的一个 Consumer。其脚本归部署方所有且在构建期恒定:模型提供任务文本,并可选地提供一个预设名称——它无法更改拓扑结构、schema、法定人数或验证逻辑。议事会如何运行由 USER 在每次 Map-Reduce 会话开始时决定,在 composer-dock 设计器中(预设、各角色的宽度和路由、验证、法定人数)——绝不由模型参数决定。并发限制、取消、worker 终止以及 workflow-run 对话节点均来自 ctx.workflowEngine。
安装
一条命令,安装到 dsh web 启动时所用的 profile 中:
dsh plugin --profile web add github:starsinc1708/dsh-tool-council
然后启动 harness 并选择模式:
dsh web
Map-Reduce 模式会出现在 composer 的模式菜单中,与 Standard、PTC、
Minimal 和 Creator 并列。选择它会将议事会组合到标准代理平面上;其他所有模式均保持原样。
无需构建步骤,也无需 pnpm 的 allowBuilds 许可:本仓库提交了其 lib/ 输出,因此安装会解析到预构建的产物。如果你希望后续的推送无法改变你所运行的内容,请固定一个 commit:
dsh plugin --profile web add github:starsinc1708/dsh-tool-council#
要求
- DeepSeek Harness 0.1.5-rc.2(dsh --version),且 PATH 中有 pnpm。
- web profile,它组合了 @deepseek-ai/dsh-base 和
@deepseek-ai/dsh-web-app。议事会所需的一切——工作流
引擎、带有 spawn provider 的子代理注册表、设置
provider,以及 workflow-run 对话节点——都已包含在这两个
bundle 中。无需安装其他任何东西。
验证是否安装成功
dsh --profile web --dump-config | grep -A3 dsh-tool-council
一个带有 tool-council-host 行的 # == @starsinc1708/dsh-tool-council 层
意味着该 bundle 已组合成功。在首次 dsh web 启动后,已发布的预设
就在磁盘上:
ls "$DSH_HOME/.agent-presets/map-reduce" # agent.cordis.yml preset.yml
$DSH_HOME 默认为 ~/.dsh。发布发生在插件加载时,即启动后一两秒,
且预设发现未做记忆化——该模式无需重启即可出现。
其他安装来源
dsh plugin --profile web add ./dsh-tool-council # 本地检出
dsh plugin --profile web add ./starsinc1708-dsh-tool-council-0.1.5-rc.2.tgz # pnpm pack 输出
两者都跳过 git 拉取,也都不需要构建许可。开发时使用本地检出:
pnpm build 然后重启 dsh web。
更新与移除
dsh plugin --profile web update @starsinc1708/dsh-tool-councilsh
dsh plugin --profile web remove @starsinc1708/dsh-tool-council
remove 会移除该依赖和 bundle 层,因此工具和设置卡片会在下次启动时消失。它不会删除已发布的预设——$DSH_HOME/.agent-presets/map-reduce 一经写入便归你所有,而插件移除后,名册会将其列为损坏状态。请一并删除该目录:
sh
rm -rf "$DSH_HOME/.agent-presets/map-reduce"
安装此插件会对你的机器做什么
有两点值得明确说明,因为二者都处于 agent 沙箱之外:
1. 它会向 $DSH_HOME/.agent-presets 写入一个目录。 预设是一种组合,因此 harness 将编写预设视为与 shell 访问具有同等信任级别。每当源 standard 预设或此插件的行发生变化时,该目录都会被重新生成,对其的手动编辑会丢失——若要产生分歧,请将其复制到新的 id 下,并设置 installPreset: false 以完全阻止插件写入。
2. 一次运行会启动全新的子代理,它们可以在你的工作区中读取和运行命令。 这正是关键所在——验证者会重新读取它所投票的文件——但这也意味着一次 council 运行会消耗真实的 token 和真实的工具调用:一次 bug-hunt 就是八个子代理。
配置
yaml
profiles//cordis.patch.yml (or the bundle's own cordis.patch.yml)
- insert:
- id: tool-council-host
name: '@starsinc1708/dsh-tool-council'
config:
installPreset: true
presetId: map-reduce
presetName: 'Map-Reduce mode'
councilPolicy:
subagentProvider: spawn
toolName: council
maxAgentsPerLayer: 100
maxLayers: 6
maxFindings: 200
maxFindingsPerMember: 50
maxFindingChars: 2000
maxReportChars: 32768
maxRunMs: 0 # 0 表示禁用挂钟时间预算
retryFailedMembers: true
mergeSameLocation: true
maxMergeCandidates: 60
councilEveryRequest: true # false = 提供 council,但不强制要求
defaultPreset: bug-hunt
presets: [] # 整体替换四个内置拓扑
councilPolicy 是该工具自身的配置,由始终组合的 host 行所拥有,以便会话设计器能够渲染部署的真实拓扑,并且已发布的预设能够以相同的策略挂载该工具。省略它则使用四个内置拓扑(bug-hunt、research、feature-design、refactor)和默认上限。不存在按预设的合并——声明 presets 会整体替换它们,因为被部分覆盖的角色提示词是一种无人审查过的拓扑。
模式无法表达的结构性规则在加载时强制执行,并让部署失败,而不是让调用失败:每个预设都以一个 reduce 层结束,且该层恰好有一个角色实例;最多声明一个 verify 层,并且其后不能有 map 层;quorum 出现在 verify 层上,不出现在其他任何地方;threshold quorum 至多等于其所在层的宽度;并且预设、层和角色 id 在同一个预设内唯一。最后两条并非迂腐——verify 层之后的 map 层会重新聚类并重新编号选票所针对的发现,而在第二层上复用的角色 id 会把两个成员折叠成一个实例 id。否则,这两者都会在运行的结束时失败,那时每个子项都已付出了代价。
subagentProvider 必须已注册,必须声明 outputSchema,并且不得继承父上下文。一个以父级转录为种子的成员会继承父级对问题的框定,而这正是该层存在的目的所在——打破这种框定。
预算与失败上限
maxRunMs 是一次运行的挂钟时间预算;0(默认值)表示不启用。它被有意强制执行两次。脚本在每个层边界检查它,并跳过剩余的 examine/verify 层,同时仍然运行末尾的 reduce 层,因此超预算的运行会返回它确实收集到的发现,并标记为 deadline,而不是什么都不返回。宿主在 maxRunMs + 60s 处保留一个硬性的 run.cancel() 后备机制,以应对脚本自身检查无法帮助的情况——某一层永远无法结束。已经在执行中的子项绝不会在层中途被杀死。
retryFailedMembers(默认开启)会重新发起一次 agent() 调用,其子项已死亡。死亡的子项会将其调用解析为 null,而不是抛出异常,因此如果没有重试,一次传输失败就会静默地移除整个视角,而报告中没有任何内容说明这一点。maxTotalAgents 的大小要覆盖重试和单个 merge 子项,因为触发 AGENT_CAP 会杀死运行,而不是使其降级。
返回空内容的 reduce 子项会被报告为缺失报告,而不是空报告:工具结果在表格上方如此说明,持久记录中带有 reportMissing。
层
层是 map、verify 或 reduce。其宽度是其各角色 count 的总和,每个实例都在引擎的并发限制下作为一次 agent() 调用运行。
一个角色与其相邻角色的区别在于其 prompt 以及可选的 model/provider,除此之外没有其他区别:工作流 agent() 钩子既不接受 persona,也不接受工具过滤器。成员确实会到达工作区——spawn 子项会加入父级的预设——这正是验证者的投票值得计数的原因:它会重新读取所引用的位置,而不是根据发现文本进行推理。
发现与 quorum
子项通过结构化输出模式返回发现结果,因此无需解析任何散文。每个成员的列表在读取时就被限制为 maxFindingsPerMember,因此一个话多的成员无法填满切片并挤掉较安静的成员——也无法让累积列表超过 instances × maxFindingsPerMember。随后,发现结果按 normalizeLocation(location) + '|' + fingerprint(title) 进行聚类;最先出现的成员保留下来,后续成员则贡献一个报告者和一个标题变体。
聚类是词法层面的,因此两个成员用不相关的措辞描述同一位置的同一缺陷时,仍会作为两个发现结果出现。当 mergeSameLocation 开启时(默认如此),一个合并子项会恰好接收这些分组——共享同一位置但不共享指纹的聚类——并返回属于同一缺陷的 id 集合;最早的聚类吸收其他聚类的报告者和变体,id 会被重新编号。合并组会形成链:若告知 f1 ≡ f2 且 f2 ≡ f3,折叠会产生一个携带全部三个报告者的发现结果,无论这些组以何种顺序到达。一个死掉的合并子项会让每个聚类都保持原样。
maxMergeCandidates 是在所有有歧义的位置之间共享的,而不是按先到先得分配,因此一个热点文件无法耗尽预算并让其他所有位置都无法合并;仍然无法容纳的内容会在运行日志中列明,而不是被悄悄丢弃。整个步骤每次运行只执行一次,在最后一个 map 层——逐层聚类会从头重建列表,并连同上一层的合并决策一起丢弃,因为这些决策所表达的 id 在重新编号后已不复存在。两种 reduce 模式都会运行此流水线:vote 将裁决表作为答案呈现,synthesis 向 reduce 角色请求散文,并将同一张表作为证据交给它。
每个验证者都会收到完整的去重列表,对每个发现结果投出 confirmed、rejected、not-a-bug 或 uncertain,并且永远不会看到另一个验证者的投票。uncertain 永远不会确认——它只会否定一致同意。当一条规则不予确认时,众数否定票会在 not-a-bug(事实成立但行为正确)和 rejected(该主张有误)之间做出决定;这一区别会改变后续操作,因此它会在计票中保留下来。
弃权不计入。 法定人数的分母是对该发现结果投票的验证者人数,而不是该层收集到的选票总数:某个验证者若未对某一行返回裁决,就是对该行弃权,否则它的沉默会让一个确认加一个弃权被读作两人法定人数。表格中的 · 就是该弃权,渲染出的图例会说明这一点。
insufficient 是未解决分支,而不是否定分支——规则未被满足,并且没有人对该结论提出异议。有两种情况会到达该分支:对该行投票的验证者少于两个,或者投票的验证者未能达到规则设定的门槛(threshold 为三,但只有两个验证者达到;uncertain 否定了全体一致)。这两种情况都不是 rejected,因为没有人说该主张是错误的。完全没有验证层的预设则报告 unverified:从来没有人被要求验证。
./tally.ts 是该算术在宿主侧的权威副本。脚本运行自己的副本,因为验证层在运行期间需要去重后的发现,并且无法导入此包。这种重复在边界两侧都受到防护。在运行时,宿主根据原始投票重新计算法定人数,并拒绝脚本 tally 不一致的运行,同时指出第一个不同的行和字段;它还会拒绝破坏聚类所保证不变量的聚类——报告顺序中的连续 id、每个 location+fingerprint 键一个聚类、reporter 和 variant 列表无重复。在构建时,tests/parity.spec.ts 在数千个带种子的输入上运行全部五个重复函数的两个副本,并比较每个输出,因此漂移会导致提交失败,而不是运行失败。宿主在运行时不自行重新计算聚类:那意味着要把整个原始发现列表带回边界另一侧,并使有效载荷大致翻倍;一旦 parity 门禁让静默漂移成为构建失败,这样做就不值得了。
会话设计器
不再有全局 council 配置——council 在其运行的位置进行配置,即在 Map-Reduce 会话开始时配置,并且 Settings → Plugins 中刻意没有 Council。宿主行仍然拥有 council 设置命名空间,但只是为了镜像部署(只读的 topology、maxAgentsPerLayer、maxLayers、agentPresetId、defaultPreset),并在用户层中携带每个会话自己的设置(sessionCouncil,以会话 id 为键)。这些镜像让 composer-dock 设计器无需 Remote 命名空间即可绘制真实的预设和层,并针对真实上限约束每个宽度和层输入;任何遮蔽其中任一镜像的用户层都会被拒绝(assertMirrorsUnchanged),因此原始 API 调用无法在不改变工具实际运行内容的情况下改变设计器渲染的内容。
在每个 Map-Reduce 会话中,composer 卡片上方都有一个可展开的 Council 面板。折叠时,它会显示该会话运行的预设;展开时,它是一个小型 DAG 编辑器,按照 council 的读取方式来读取——一条层节点链(examining → verify → synthesize),每个节点为每个角色保留一行:
- 每个角色行都带有自己的成员数量(步进器)和一个模型选择器:一个 DSH 风格的触发器,打开一个可搜索的提供商分组菜单,该菜单由 harness 自身的每会话模型目录提供数据(与 composer 的模型选择读取的是同一来源),其中 inherit 会清除该角色的路由。数量是该角色的绝对宽度:tests: 3 会启动三个测试审查员,无论预设组合成了什么。当一行被调回其组合值时,一旦你编辑它,它就不再是覆盖值。
- 在缺少某个视角的地方添加角色。 每个 map 和 verify 节点都有添加角色:会出现一行,带有自己的名称和视角提示词(两者均可编辑,提示词通过 ✎ 编辑器编辑)、其数量和模型路由,以及一个 ✕ 可再次将其移除。提示词正是已编写成员运行时所依据的内容,因此它会与角色一起存储。
- 当一次遍历不够时添加层。 链下方的添加层会在预设自身的 map 层之后插入一个完整的已编写 map 层——在验证之前再进行一次审查遍历——最多到部署镜像的 maxLayers。
- verify 节点有一个开关。关闭时,会话仅运行 map → reduce:在合成器报告之前不会进行任何交叉检查,这更便宜,也是会话在探索而非认证时想要的方式。
- 法定人数行重述 verify 层的规则(majority、unanimous、带自身确认数量的 threshold)。设计器拒绝保存其自身宽度无法达到的阈值,也拒绝任何被推过 maxAgentsPerLayer 的层——否则主机在下一次运行时会遇到同样的两种拒绝。
- 在层之上,一个预设选择选择会话要编辑哪个部署拓扑、你保存的某个自定义预设(★ …),或自定义(从零开始)——一条你自己构建的空链:添加 map 层、verify 层和最终合成器(+ map layer / verify layer / synthesizer),每个都有自己的角色。自定义 council 会像任何其他 council 一样被验证(一个末尾合成器、至多一个 verify、verify 之后没有 map、每一层非空且处于镜像上限之内);给它起个名字并保存。保存会为该会话固定该拓扑:从那时起,会话中的每次 council 运行都会执行它,而模型的每请求预设选择将不再适用(只要存在 setup,工具就会忽略 preset 参数)。在部署自身默认预设之上的原始面板不算 setup——会话保持干净,模型继续按请求选择,运行行为与之前完全一样——直到你做出真正的选择。“让模型选择预设”会清除会话的条目并恢复该行为。
- 面板底部的两个库会跨会话保留:
- 我的角色——任何已编写角色上的 💾 按钮会将其存储(名称、视角提示词、数量、模型路由);随后每个 map/verify 节点的添加角色菜单都会提供我的角色条目,一键插入副本。✕ 移除库条目。
- 我的预设 — 在编辑自定义议会时,保存预设会将其存储起来(以你的名义保存其完整编写的拓扑结构);随后它会在每个会话的预设菜单中显示为 ★ …,选择它会将该模板复制到会话中以供编辑和运行。✕ 会移除已保存的模板。
整个文档在每次保存时都是一次字段写入(sessionCouncil[sessionId]),写入前由设计器进行结构验证,调用时由工具在写入后进行验证,因此两个会话永远不会共享同一拓扑结构,重新打开的会话也会保留自己的拓扑结构。工具行在每次调用时都会重新读取该部分,因此一次保存会在下一次运行时生效,无需重新组合。
议会选项卡
议会对话视图会将每次运行渲染为其各层与成员的图——实时状态、每个成员和每层的 token、每层持续时间,以及一行角色说明——随后是该次运行的裁决表和书面报告,并支持 Markdown 和 JSON 导出。每次运行的标题都带有其任务片段、开始时间,以及一个超预算或失败标签,因此即使运行列表被折叠也仍然可读。
一次进行中的运行会在标题中显示一个走动的时钟、其当前累计 token 总数和成本估算,并在该层的 token 旁显示每层成员计数(1 running · 3 done)。关于这些数字,有两点值得说明,因为二者都无法从实时运行中推导出来,而且二者都是如实报告而非猜测:
- 这些计数统计的是已经启动的内容,绝不是 2/3 这样的分数。 workflow-run 节点只有在成员启动后才会发布它,而承载每层真实宽度的产物只有在运行结束后才会落地。显示为 of N declared 的声明宽度来自 council 设置部分——即部署的 topology 镜像与本次会话的设计器设置组合而成,这与工具在每次调用时解析的是同一对内容——并通过运行名称中的预设 id(council:)与运行关联。这是一次实时读取,因此如果在运行中途编辑了设置,就会使其与该次运行实际启动的内容不一致;这就是为什么它位于计数旁边,而不是作为分母放在计数下方。
- 这个时钟说明了它是哪个时钟。 RunData 和聊天节点都不携带开始时间——ConversationViewNode 没有 time 字段,而 anchorSeq 是一个序列号。同一快照确实携带的是仍在运行的 tool/call 头部,其 time 是议会调用被记录的确切毫秒;视图通过该调用自身的轮次和步骤进行关联,并且当该步骤中有多个调用正在进行时拒绝关联,因为那里没有任何信息能判断哪个调用属于哪次运行。当关联被拒绝时,标题会退回到此选项卡首次看到该次运行的时间,并显示 watched here 而不是 elapsed——一个会在重新加载时重置的时钟必须承认这一点。
计时器只在运行状态为 running 时存在:实时头部是一个独立组件,因此结算时会将其卸载,这正是清除定时器并丢弃它所打开令牌订阅的原因。只有最新的运行会展开;较早的运行会折叠,这也阻止了已完成的运行在会话剩余时间内保持实时令牌订阅处于打开状态。长判定表只绘制前 50 行,其余行一键即可查看。两种导出都提供剪贴板复制和文件下载两种方式,因为剪贴板访问需要权限,并且在某些 webview 中会静默不可用。
每一行都以彩色徽章的形式携带上报成员的严重级别(blocker、high、medium、low),表格上方有三个筛选芯片:confirmed、unresolved 和 all。unresolved 是两种未解决分支的合并——INSUFFICIENT 和 NOT VERIFIED——因为它们的区别在于为什么没有人结算该行,而不在于它们留给读者去做什么;将它们分开会导致在每一个预设下都有一个芯片永远为零。每个芯片都携带它将显示的计数,因此空表格永远不会被读作一次空运行。芯片是在 50 行窗口之前应用的(windowRows 负责这一顺序,并且有测试固定它)——如果先开窗口,就会把第 60 行确认的 blocker 从声称显示它的 confirmed 芯片中丢掉——而“显示全部”统计的是筛选后的总数。行保留其在整次运行中的编号,因此 #7 在每个芯片下以及导出中都是同一条发现。
一条发现是你需要采取行动的东西,因此每个位置都是一个可自我复制的芯片,旁边的箭头则打开文件。打开操作走的是与 harness 自身聊天在 0.1.5 中处理文件提及时相同的接缝:ctx.sidebarRight.openResource(...),地址为 dsh-resource://file/session//,这会在右侧边栏中以代码预览方式打开该文件。位置是相对于工作区的,因此对于该地址,路径会基于会话的 cwd 进行解析;没有工作区根目录的会话会原样发送路径,而不是猜测一个根目录并打开错误的文件。当预览无法打开时(没有挂载侧边栏界面,或没有标签页类型认领该地址),该行会以 location.openFailed 说明这一点,而不是静默失败。芯片和导出共享同一条剪贴板路径,因此拒绝访问的权限化剪贴板会显示 copyFailed,而不是静默失败。
除了这两种导出之外还有第三种:将已确认的发现导出为 Markdown 清单(- [ ] {title} — {location},当成员提出了修复方案时,修复方案作为子项),提供复制和下载两种方式。仅限已确认项,因为未解决的行还不是工作。toChecklist 是一个纯导出函数,带有自己的测试,其中包括成员撰写的标题中的换行符会被折叠——否则包含 \n- [ ] already fixed 的标题会伪造出一条没有人上报的清单条目。
严重程度是一种自我报告:选择它的是提交该发现的成员,而一个无人确认的 blocker 仍然只是一个声明。持久记录将其存储为普通字符串,因此由配置不同的构建写入的级别会在中性徽章中渲染其自身的文本,而不是解析一个不存在的区域设置键。Markdown 导出也带有严重程度列;父模型的表格则没有——tool.ts 中的 renderTable 是一个 token 预算决策,而非 UI 决策,并且它被有意保持原样。
报告以文档形式渲染,而不是作为 。合成器被提示生成编号章节和列表,因此预格式化的等宽字体丢弃了其输出唯一具有的形状——但两个显而易见的修复方案都被堵死了。导入 harness 的 Markdown 渲染器是一种跨插件值导入,被 bundle 纯净性门禁所禁止(而且在这里也不是声明的依赖),而打包一个 Markdown 解析器会增加重量,并在模型编写的文本上增加一个 HTML 注入面,而这恰恰是你不希望靠近 innerHTML 的输入。
因此该标签页用 markdown.tsx 来渲染它,这是本插件集中每个插件共享的小型散文工具包:Markdown 将文档转换为 React 元素——标题、有序和无序列表、围栏代码、表格、引用、分隔线以及常见的行内标记——而 PromptText 保持必须与原文完全一致的文本(运行的任务)为等宽、换行且不重排。不构造任何 HTML,dangerouslySetInnerHTML 从不出现,因此报告中的 只是九个字符的文本,XSS 面根本不存在,而不是被防御。同一个工具包渲染发现标题及其建议修复中的标记,因此裁决单元格中的 path/name.ts 读起来像代码而不是标点——该单元格保持其宽度、换行和列顺序。tests/markdown.spec.ts 直接驱动该工具包:每个块分支、一个未闭合的围栏(它在文本末尾闭合,而不是吞掉报告)、一个不成对的行内反引号、所有三种行终止符,以及每个分支中的 HTML 标签。一次重排是文档的代价:段落的各行会被连接起来,因此合成器手动对齐的块只有在围栏内才保持对齐。toMarkdown 仍然原样导出原始报告——导出是记录,而不是渲染。
最后一部分是持久的,并且它通过插件实际拥有的唯一通道传递。运行的产物——拓扑、叙述、逐层计时、裁决行和报告——是工具的 presentationMeta,harness 将其持久化在它本来就会写入的 tool/result 事件上;标签页从那里读回它。因此,一次已完成的运行会从全新的客户端会话重新打开,其表格完好无损。
该插件不写入任何私有事件类型,也绝不能这样做。 会话读取器会根据 harness 的 KNOWN_SESSION_EVENT_TYPES 校验每一条记录,并拒绝解释包含未知类型且未标记为 ignorable 的日志——而 Session.append() 没有为仓库外的插件提供设置该标记的方式(dsh-session 也说明了这一点:“它们的注册接口要等到存在这样的消费者时才会提供”)。直到 0.1.1-rc.2 的各个版本都会追加一个 tool-council/ 系列,因此导致它们自己的会话日志在下次启动时无法读取:
SessionFormatUnsupportedError: session "…" contains event type "tool-council/run-start"
(seq …) unknown to this harness and not marked ignorable; refusing to interpret the log
如果你遇到了这种情况,请就地修复受影响的日志——它只会为那些记录添加 ignorable 标记,并将原始文件保留为 .bak:
sh
node scripts/repair-council-sessions.mjs # scan and report
node scripts/repair-council-sessions.mjs --write # repair
现在只会追加 tool-workflow/ 记录,并且 tests/recorder.spec.ts 会根据 harness 自身的目录来检查所追加的词汇,而不是手写的允许列表。
模型体验
委员会成员
没有直接影响。每个成员都会收到预设的框架、自己的角色提示词以及逐字不变的任务文本;没有任何成员会看到父对话或同级成员的输出。每个角色实例在每一层都有一个全新的子上下文,受 maxAgentsPerLayer 和引擎的并发限制约束——一次 bug-hunt 运行会产生八个子上下文。它们都不会加入父历史记录。
父工具结果
一行计数——N of M examining members answered; K reported … distinct findings; … verifiers voted, confirming …——然后是带有图例的 Markdown 裁决表,接着是综合器的报告,全部受 maxReportChars 限制。回答和报告是分开计数的,因为 map 提示词将空列表视为有效回答,如果把它们合并,一次没有任何发现的干净运行就会被读成四个死掉的子上下文。如果一次运行达到了预算上限或失去了综合器,会在表格上方说明,而不是把不完整的委员会呈现为完整的委员会;当没有裁决表也没有报告时,会直接列出发现本身,而不是静默丢弃。各个成员的记录和每个验证者的理由不会进入父上下文。措辞会将结论归于成员——“三名验证者中有两名确认”——因为验证者是重新阅读同一仓库的代理,而不是独立的预言机。
开发
sh
pnpm install
pnpm build # tsc -> lib/types, tsdown -> lib/.js (host halves + client bundle)
pnpm test # vitest: parity gate, tally arithmetic, policy rules, script body,
host rendering, recorder, session-setup composition,
designer controller, client fold
pnpm build 会同时产出两部分。宿主入口是普通的 ESM;浏览器入口则不是——lib/client.js 必须是加载器的 lazy-CJS 工厂产物(window.__ModuleLoader__.load({ id, factory })),这正是 tsdown.config.ts 所复现的形式。测试框架自带的 clientBundle 预设并未发布,因此树外包需要自行拥有该格式。该配置编码了两条规则:
- 共享模块表很小——react、react/jsx-runtime、react-dom、react-dom/client、@deepseek-ai/cordis、dsh-client-ui-slots、dsh-client-ui-primitives,外加预加载的 dsh-client-runtime/client。其他所有内容都必须内联,否则工厂会在 require 遇到模块表无法应答的模块时抛出异常。
- 禁止跨插件的值导入。通过 cordis 服务进行协作,其余保持仅类型导入。
- 同一条规则也适用于该包自身的子路径。deps: { neverBundle } 会将每个裸说明符外部化,因此从 src/client 中对 @starsinc1708/dsh-tool-council/types 的值导入会编译、打包,然后在加载时失败,报错 require(…) missed the module table。请以相对路径从 ../types.ts 导入值,以便它们被内联;该说明符对于 import type 是可以的,因为它在打包产物存在之前就被擦除了。tests/bundle.spec.ts 对两个方向都进行了强制检查。
@deepseek-ai/dsh-* 包被有意设为 devDependencies。在运行时,它们通过 profile 的 Node 父级查找解析到 harness 安装,因此将它们声明为运行时依赖会把一个更旧的副本安装到 profile 中,并遮蔽 harness 实际正在运行的那个。
要针对实时 harness 进行迭代,请安装该检出并在原地重新构建:
sh
dsh plugin --profile web add .
pnpm build && dsh web
已知限制与推迟的工作
- 角色无法限制自己的工具或人设——工作流的 agent() 钩子两者都不暴露,因此按角色隔离将意味着离开工作流引擎,转而使用 ctx.subagents.start(),并重新负责并发门控、取消和进度 UI。
- 设计器与工具位于不同的平面——设计器由裸名宿主行(宿主平面)提供服务,而工具运行在已发布的预设(代理平面)内。它们之间的链接是 council 设置命名空间:宿主拥有它,工具在调用时读取它。如果部署时手动挂载工具行而没有宿主行,则会得到工具,但没有设计器,也没有会话设置。
- 投票衡量的是相关代理之间的一致程度——不同的视角以及按角色的模型和提供方路由会降低成员之间的相关性,但不会消除它。法定人数是对自我报告的计数,而非认证。
- 合并步骤是一种判断,而非证明——确定性键是词法层面的,而解决同一位置歧义的合并子项是一个带有自身失效模式的 LLM。它的作用范围仅限于键无法做出的那一个决定,其候选数量有上限,而一次错误的合并正是发现可能悄无声息消失的途径。设置 mergeSameLocation: false 可保留词法行为。
- 运行预算在层与层之间检查——maxRunMs 无法停止一个已经在运行的层;它会停止下一个层,并在宽限期后回退到宿主 cancel()。它是运行持续消耗时长的上限,而不是它会精确命中的截止时间。
- 父级的表格上限为 100 行——将 maxFindings 提高到该值以上的部署会收到一条计入截断的通知,并在 Council 标签页中看到完整表格,而不是一张被报告上限从行中间截断的表格。
- Council 标签页是只读的——它展示并导出一次运行;它无法取消或重新运行一次运行。取消按钮将不得不调用 session.cancel,而它会停止父级的整个回合,而不是这一次运行,因此一份有文档说明的指令确实比一个做了别的事的按钮更好。精确取消和一键重新运行都需要一个 Host RPC 命名空间,这是 Client 组合所有者的决定,而不是本包的。改为取消父级步骤:该工具会随之取消它的运行。
- 模型菜单需要组合好 harness 的模型目录——选择器由 modelDirectories(ui-model-selection,标准 web 配置的一部分)提供数据。没有它的部署只会显示 inherit;其他一切都不会出问题,但没有可按提供方分组的目录可供搜索。
- 成本数字是你的算术,而不是账单——costPerMillionTokens 默认关闭(不再保留编辑器),并且从构造上就是混合的。计量器不报告价格,视图也无法知道每个成员实际运行在哪条路由上,因此这里不可能给出按路由划分的数字;所显示的是你的费率乘以真实 token 计数,并标注为估算值。
- 持久化的产物是被验证的,而不是被信任的——isArtifact 会检查标签页所解引用的每一个字段,包括行,而不仅仅是 kind/version/runId。仅靠 version 无法约束输入:一个发布了 bug 的构建也写入了版本 1,而产物会从其他构建写入的日志中重放。未通过检查的记录会被忽略,因此运行会降级为其实时成员图,而不是在渲染内部抛出异常并使整个标签页空白。
- 一个位置会打开其文件,但绝不会定位到其行——在 0.1.5 中,箭头通过 ctx.sidebarRight.openResource 在右侧边栏中以代码预览方式打开文件,其地址为 dsh-resource://file/session//,该地址由插件构建且不带行号,因此 rank.py:521 会打开 rank.py,而行号仅保留在 chip 所复制的内容中。0.1.5 的预览是否能够定位到某一行尚未经过检查;在能够做到之前,跳转到该行需要一个上游接缝。
- Council 标签页手动镜像了两套渲染器契约——workflow-run 的成员/阶段载荷结构及其状态联合类型在 council-view.tsx 中被重新声明,因为该包仅从其 exports 映射未发布的子路径导出它们。修复这一点需要上游导出;在那之前,对这些结构的更改会表现为渲染错误,而不是类型错误。
- 实时的“共 N 个已声明”读数镜像的是已保存的设置,而非草稿——运行中层级旁显示的已声明宽度由会话的已保存设置(其自身的 sessionCouncil 条目)组合而成;在设计器中暂存但尚未保存的编辑,以及运行中途被编辑的设置,都会使实时数字与运行实际启动的内容不一致。运行产物在稳定后会携带真实的层级,因此最终表格始终精确;这就是为什么实时数字作为已声明提示位于观测计数旁边,而不是作为分母位于其下方。
- 组合器设计器仅在运行 council 模式的会话中显示——它与 Council 标签页基于相同的 agent-preset 身份进行门控,因此将工具行挂载到通用模式的部署不会获得面板(该节条目仍可被写入,且工具会遵循它)。
- 会话可以编写角色和映射层,但无法移除预设角色或重新排序——会话可以调整每个预设角色的宽度和路由,将自己的角色(及其透镜提示)追加到映射层和验证层,添加完整的自定义映射层,删除验证层,并重新声明法定人数;它无法移除预设组合的角色,也无法将某一层移到验证之后。自定义提示在具有与任何成员相同访问权限的新子项上运行,因此它们由会话自行承担风险编写——这与模型自身指令已经承载的信任相同。
- 验证层总是用尽每一张选票——某一层的所有验证器都在一个 parallel() 中启动,因此一旦多数已决定,就没有任何机制可以提前停止。减少消耗将意味着不启动它们,这是宽度变更,而非运行时变更。
- 拓扑是一条无环链——不存在 verify → fix → re-verify 循环,最多一个验证层,且恰好一个末尾归约角色。迭代执行属于 @deepseek-ai/dsh-tool-ralph。
- 除了发现和投票,没有任何东西跨层传递——成员记录不会被向前携带;工作区就是共享内存。
- 长期存活的同伴团队是另一种形态——@deepseek-ai/dsh-experimental-agent-team 拥有名册、邮箱和共享任务 DAG;此插件是一次有界扇出,在一次调用中即告稳定。扫码进群