一个 DSH 自建 agent preset:一个调度智能体 + 九个专家智能体名册。 任务由调度智能体判断范围后分派给对应专家;专家之间不能直接互相转交,越界时由调度智能体再派发下一步。
九个专家各一行职责:
agent_file|文件管家:文件与文档的检索定位、阅读理解与问答、批量整理归类、格式转换与文档生成agent_computer|系统运维专员:系统与硬件信息查询、系统设置修改、优化清理、故障排查、进程与服务控制agent_app|应用操作专家:桌面软件启停/安装卸载与命令行接口调用、Android 模拟器上的手机 App、微信小程序agent_browser|网页交互专员:需要登录、多步表单、点击选择、多页跳转抓取的网页操作agent_search|全网搜索专员:多轮联网检索与多源资料综述,结论带来源链接;只联网,不碰本地文件与系统agent_researcher|代码与仓库事实检索员:在本仓库/本机文件里定位实现、配置与出处,只读、必须带行号agent_coder|实现工程师:按已确定的方案改动工作区代码,并运行验证证明改动有效agent_reviewer|审查验证员:对已有改动做对抗性审查,只报告不修改agent_general|全功能智能体(交接专用):把一整件工作交接给一个独立上下文里的通用智能体,由它独自做完(读写文件、跑命令、联网检索、整理产出都在范围内);只在用户显式要求「交给子代理 / 另开一个上下文 / 换个智能体接手」时才派,它是叶子、不会再往下委派
第 9 个专家与前 8 个有本质区别:它不是"某个专项的专家",而是用户点名要"交接"时才派的全功能
角色,用途是上下文隔离(上层把活交给下一个智能体、另开一个上下文)。它拿的是本 preset 里最全的
叶子工具集(文件 / 命令 / 后台任务 / 联网 / 技能 / 待办 / 交付物 + send_message),但刻意不含
任何 agent_* 名册行与通用 subagent / subagent_fork / workflow / ralph —— 即它不会再往下
委派。三个理由:① 调度者的 list_agents 只列直接子级、send_message 只到直接父/子,孙代理对它
不可见、不可 steer,一跳可达(红线 1)才有可追踪的链路;② 编排层规则(同实体合并 / 必要性闸门 /
digest 中转)只作用于调度者自己那一次委派,一旦它再委派,这些纪律整段失效,而同一份材料会被再读
一遍(实测 91% 的提示 token 是 cache-read);③ 用户要的是"一个独立上下文把活做完",不是"再长出一
棵树"。技术上它当然能委派 —— 子代理会 composeFrom 继承父代理的整套组合,把名册行写进它的
allow 就生效,深度上限由该行的 maxDepth 决定(dsh-tool-subagent 默认 3)——这是一次刻意的
能力裁剪,所以 persona 里写明了"越界时回报需要 agent_X"。补偿是"结束本次会话并回报上级"这条协议
由运行时自动提供:dsh-subagent 只在子代理看得见 send_message 时,给它的任务末尾追加
「Your parent agent id is …,结束前用 send_message 把结果回报给它」(源码依据
withContinuableReturnGuidance),而它每一轮的 final message 还会作为 settlement notice 的 closing
message 回到调度者 —— 不用递归也能交接回来。
配套技能 adg-add-agent:让你在任何模式(包括创造模式)下说一句「给 Adg 加一个智能体」就能新增专家。
配套插件 dsh-adg-token-budget(第二层):给委派出去的子代理加步数收敛检查点(默认阶梯
4 / 8 / 12 / 18 / 24 / 32 / 42 / 55 / 72 / 95 / 125 / 165 / 215 / 280 步,早期的密集、之后逐渐
拉开)。子代理进入命中阶梯的第 N 步时,插件先放行这一步(next()),再追加恰好一条它自己
署名的用户消息(【收敛检查点 n/N】…),每个 tier 在每个驻留期最多一次;消息本身是一条
可选提醒,让子代理自己选"收尾汇报"还是"继续做完必需的工作",不是停止指令。
本次改动把另一半整体移除了:插件原先还有一条累计 token 预算(软档收尾提醒 + 硬档
agent.cancel,默认 300 万/子代理),现在一行都没有了 —— 设计理由与旧数据见
第二层。
上线顺序是三步:先挂 enabled: false(装上了、但不动作)→ enabled: true + dryRun: true
(按自己的流量校准,只记录、不注入)→ enabled: true + dryRun: false(提醒真的注入)。
见
第二层:子代理的步数收敛检查点。
省 token 的口径(重要):preset 侧不压低任何体积旋钮 —— 上下文压缩阈值、单条工具结果的
截断长度、web_fetch / 检索的上限一律用插件出厂默认值,persona 里也不写读取/汇报预算。
成本控制分三层:编排层是调度 persona 的五条规则(同一实体 + 同一性质的任务只派一次;同一实体
的后续任务接给已经读过它的那个专家;大范围改动先定位再动手;跨专家传递大材料走 digest;派发前过
必要性闸门,不做的旁路在交付里挂号);输出层是同样写在调度 persona 上的去冗余纪律(不回贴
工具输出原文 / 同一结论只说一次 / 不转述中间过程 / "未验证 / 未纳入"必填块不许为求简短省略 ——
没有字数上限);运行期是上面那个插件的
步数收敛检查点:它不牺牲单条结果的完整度,也不截断任何产出。
见 多智能体的 token 消耗。
理由与实测见 token 成本纪律。
装到四个位置(${DSH_HOME:-~/.dsh} 是你的 dsh 用户根;2026-09-28 起 preset 的形状变了,
见 给 AI 的安装指令 开头那段):
| 仓库里的路径 | 安装到 |
|---|---|
preset/(两个文件) |
不再是"拷两个文件":先由 tools/gen-preset-bundle.mjs 生成 bundle(bundle/adg-preset/,构建产物、在 .gitignore 里),装到 ${DSH_HOME:-~/.dsh}/bundles/dsh-adg-preset/(挂 bili 的 profile 用注入版 .../bundles/dsh-adg-preset-bili/,见红线 11),再把 dsh-adg-preset 写进目标 profile 的 dsh.profile.bundles |
skills/adg-add-agent/SKILL.md |
${DSH_HOME:-~/.dsh}/skills/adg-add-agent/SKILL.md |
plugin/dsh-adg-token-budget/ 的 package.json / cordis.patch.yml / src/ / examples/ / README.md / LICENSE(六项,与 package.json 的 files 一致;cordis.patch.yml 就是挂载行本身,缺了它这个包只是普通依赖;test/ 与 INSTALL.md 不进部署) |
${DSH_HOME:-~/.dsh}/bundles/dsh-adg-token-budget/(2026-09-28 起与 dsh-adg-preset 同一根;旧落点 ${DSH_HOME:-~/.dsh}/plugins/dsh-adg-token-budget/ 已删除),再由目标 profile pnpm add link: 过去,还要把包名写进该 profile 的 dsh.profile.bundles——光有依赖不算选中(不要再放 profiles/node_modules/,那个共享根在本版 dsh 的解析里被排除了) |
browser/(浏览器工具链,零依赖) |
${DSH_HOME:-~/.dsh}/browser/(重新跑一次安装脚本即生效,不用重启 dsh) |
安装脚本不再往 ${DSH_HOME:-~/.dsh}/profiles/<profile>/cordis.patch.yml 补挂载行 —— 那一行现在由
包自己的 cordis.patch.yml 在 bundle 层提供;它改的是 profile 清单:把 dsh-adg-token-budget 写进
dsh.profile.bundles(改前先备份成 package.json.bak-adg-token-budget;目标 profile 默认是所有
"能装 preset 的" profile,判据是它的 dsh.profile.bundles 含 @deepseek-ai/dsh-web-app)。
profile patch 里若还留着旧机制那条手贴行,脚本只报告、不代删,而它必须被删掉(理由见
挂载行住在哪一层)。插件那一层是什么、怎么开、怎么确认已武装见
第二层:子代理的步数收敛检查点。
把这个仓库的地址发给 dsh 里的 AI,说一句「按仓库 README 装到本机」即可 —— 下面的 给 AI 的安装指令 一节就是写给它看的。
git clone <repo-url> ~/adg-multi-agent
sh ~/adg-multi-agent/install.sh # macOS / Linuxgit clone <repo-url> $HOME\adg-multi-agent
powershell -ExecutionPolicy Bypass -File $HOME\adg-multi-agent\install.ps1 # Windows预设改动按"重启"来验收,别赌热重载。 旧版本文档里写过"已实测:preset 挂载之后把 composition
的行改掉,compositionInventory() 仍然返回旧行",所以安装完必须重启 dsh,重启后在新建对话里选择
「Adg 多智能体模式」。(在重启之前,Adg 模式用旧组合运行,不要拿它做验证。)
不过 2026-09-25 这次实测看到的是另一套机制:当前这版 dsh-agent-presets 的
ensureStanding() 会比较 composition 文件的 stamp,文件变了就起一个新的 generation
(源码注释原话:"a changed file starts the next generation here, for this and later sessions")。
已过时:下面这段讲的是 0.1.7 之前那套 standing mount + .agent-presets/ 机制,本版 dsh 已整体移除(见给 AI 的安装指令)。当时这次交付把改好的文件部署到 .agent-presets/adg/ 之后重跑了挂载校验(第一次跑早了一步、
校验的是还没替换的旧文件,所以又跑了一次):resolve('adg') 的 broken 为空、
standingKeyFor('adg') 返回 mounted OK、compositionInventory() 报 34 行、
8 行启用的专家行、tool-subagent-fork 0 行(tool-subagent 模块名出现 10 次,
因为 tool-subagent-codex / tool-subagent-claude-code 两行是 enabled: false 的)——
也就是新文件确实能组合。2026-09-28 加第 9 个专家(agent-general)之后按同一套口径又跑了一次,
当时的实测值是:standingKeyFor('adg') mounted OK、compositionInventory() 报 35 行、
9 行启用的专家行(多出来的就是 agent-general,fiberState 与其余 8 行相同)、
tool-subagent 模块名出现 11 次(9 行 + 两行 disabled)。但"新会话会不会自动加入新 generation"
没有实测(要有真实的 Adg 会话来观测新 persona 文本),所以结论仍然写成:preset 改动后重启 dsh,
并通过上面的挂载校验确认它可组合;
只有在重启代价很高时,才值得去测"不重启会不会也能生效"。
插件那一行不受这条约束,但要分清改的是哪一层:挂载行本体现在住在 bundle 层
(${DSH_HOME}/bundles/dsh-adg-token-budget/cordis.patch.yml)—— 没有任何东西 watch bundles/,
单独编辑它就是不会自己触发重读 ⇒ 以重启 dsh 为准(禁止宣称"不重启也会生效";实测细节与
冷启动后的 bundle 层仍是未观测见 挂载行住在哪一层);
写在 profiles/web/cordis.patch.yml 里的**config: 覆盖行才是那个热重载层,改它立即生效、不用重启
(但覆盖行按 id 整块替换 config、不是深合并,要留的键必须全部重写);
改 src/ 下的代码则必须重启 —— 已实测:热重载会重新 apply 这一行,但不会重新 import
已经加载过的模块(Node 的 ESM registry 按文件 URL 缓存,而 URL 没变),激活行仍然是旧形状。
所以先部署代码 + 重启 + 确认激活行出现新字段,再改 config:** ——
在旧代码还活着的时候写 dryRun: false,会把旧代码里那条已移除的硬档真武装(旧代码不认
新字段,缺省的 budgetTokens 就是 300 万)。
enabled: false 时它不注册任何监听器、不写决策日志,但会写一行加载期的激活行
(activation: inactive (enabled: false) …),所以"装上了"这件事看得见 ——
见 怎么确认它已经武装。
本次改动之前的那次交付就是按这个顺序做的,两个时间点都有日志为证:01:37:18 重启后宿主重新
加载了包,激活行出现 stepNudge=true stepTiers=[12, 24, 40] stepText=builtin dryRun=false
(新代码在跑;阶梯当时是 [12, 24, 40],后来换成现在的 14 档);01:38:47 只改了
dryRun: false,没有重启,新的激活行就生效了(config 热重载)。反过来那次"同时改代码和 config"
在 17:10:28 把旧代码里那条已移除的硬档短暂真武装了两分钟,日志显示窗口内 hard stage: cancel
计数没有增加 —— 这也是"代码必须先到、config 后到"这条顺序的由来。
名册分两组:前五个(agent_file / agent_computer / agent_app / agent_browser /
agent_search)覆盖文档、系统、应用、网页、检索五类外围能力,后三个(agent_researcher /
agent_coder / agent_reviewer)是代码向专家。缺口一栏写的是本环境的真实实现口径,
不是宣传语:
| 专家(toolName) | 覆盖的能力 | 本环境的实现口径 / 缺口 |
|---|---|---|
agent_file |
文件与文档的检索定位、深入阅读与问答、复制/移动/重命名/批量归类、格式转换与文档生成 | 图片内容理解走 read_image(把图片交给模型看,需要模型路由支持图像输入,调用报错就如实说明);文本类文档(PDF/Word/Excel/PPT)用 pwsh 调本机已有工具提文本。OCR(图片里的文字)、人像/场景检索、跨设备传输取决于本机工具链(Python 库、Office、同步盘目录等):persona 要求先用 pwsh 探测可用工具,缺什么就直说「本机缺少 X,无法完成」并给替代方案,不允许假装完成 |
agent_computer |
系统与硬件信息查询、系统设置修改、优化清理、故障排查、窗口与桌面管理、进程/服务/计划任务控制 | 不依赖模拟点击的 Windows API 路线可用(PowerShell / CIM / P-Invoke)。会改变系统状态的操作要先说明影响与回退;不可逆或高风险操作必须先停下、写明「需要用户确认后才能执行」 |
agent_app |
桌面软件启停/安装卸载与内部功能调用、Android 模拟器上的 App、微信小程序 | GUI 视觉识别 + 模拟点击在 DSH 没有对应工具:只能走 CLI / adb / winget / 软件自带接口。凡是「看界面点按钮」类需求必须明说不具备,并给出替代(应用 CLI、adb 命令、官方 API、或请用户手动完成) |
agent_browser |
登录态下的站点操作、多步表单、点击与下拉选择、多页跳转抓取 | 本会话必须是「完全权限」(danger-full-access)—— 硬约束,理由与源码依据见下一节「浏览器专家需要完全权限」:在 workspace-write / read-only 下本机 Chrome / Edge 根本起不来(受限令牌禁止创建 Chromium 内部 IPC 必需的有名管道),所以调度者会先停下来问用户。能跑起来时走仓库里的 browser/ 工具链(cli.mjs 一个入口、零依赖、有头、profile 固定在 <DSH_HOME>/browser-profile,见「浏览器工具链与登录态资产」);工具链不可用、或目标本来就静态可取时降级成 web_fetch 单次抓取(只能取静态内容、不能交互),并在回答里说明是降级执行。遇到登录墙 / 验证码 / 二次验证按「登录墙与验证码:人工介入协议」办:专家开好有头窗口后停手并如实报,由调度者转达用户 |
agent_search |
多轮联网检索与多源资料综述、关键信息引用溯源 | 只联网:allow 里只有 web_search / web_fetch,本地文件与系统级请求被硬性排除(这不是偏好)。天气、汇率、股价这类简单事实查询、以及一两次抓取就能答完的已知 URL 定点核对由调度智能体直接回答,不派给它 |
agent_researcher |
在本仓库/本机文件里定位实现、配置与出处,只读、带行号 | 硬只读 —— allow 里没有 write / edit / pwsh,真的改不动东西;公网发现式调研归 agent_search,它自己的 web_search / web_fetch 只用于已知 URL 的定点核对 |
agent_coder |
按已确定的方案改工作区代码,并运行编译/测试自证 | 只在当前工作区内改动文件;不做需求解读、方案设计与系统级运维 |
agent_reviewer |
对已有改动做对抗性审查,尽量用只读命令或测试验证 | 只报告不修改;每条结论给路径与行号或命令依据 |
本节写三件事:硬约束(为什么必须切权限)、根因(实测到哪一层)、处置(preset 侧唯一能做的两道闸门)。
结论先说:agent_browser 要做真正的浏览器自动化,必须让本会话处于 danger-full-access
(界面 Permissions 选择器里 id 为 danger-full-access 的那一项,或 /permission danger-full-access)。
在 workspace-write / read-only 下,本机的 Chrome 与 Edge 根本起不来 —— 这不是配置问题,
也不是 persona 能绕过去的偏好,是 Windows 沙箱后端的机制。这条约束无法从 preset 侧修掉
(下一节逐条给源码依据),所以本 preset 的处置是把它做成调度侧的前置闸门:派发 agent_browser 之前,
调度智能体先读自己上下文里那行 Current DSH file policy:,不是 danger-full-access 就先
ask_user_question 问一次,再按回答决定。
沙箱在 Windows 上用 WRITE_RESTRICTED 受限令牌运行子进程(@deepseek-ai/dsh-sandbox-windows-acl)。
这个后端自己的 README 把该边界写在「已知限制」里:受限孙进程的管道 stdio 捕获不可用 ——
libuv 的管道 stdio 用有名管道,其 client 端打开所请求的写访问没有任何 restricting SID 被授予,
所以受限进程内 spawn(..., { stdio: 'pipe' }) 以 EPERM 失败。Chromium 的 Mojo IPC 同样走有名管道,
于是浏览器在进程初始化阶段就死掉。2026-09-26 在本机做了一次 A/B:同一台机器、同一个 node、
同一批浏览器二进制,只改会话文件策略(复现脚本与原始输出见 docs/evidence.md §11):
| 探测 | workspace-write |
danger-full-access |
|---|---|---|
spawn('cmd.exe', …, { stdio: 'pipe' }) |
spawn THREW EPERM —— 后端的文档边界,实测复现 |
退出码 0 |
同一条命令改用 stdio: 'ignore' / 'inherit' |
退出码 0 —— 换 stdio 能让别的程序跑起来 | 退出码 0 |
chrome.exe --version |
退出码 0 —— 二进制本身没问题 | 退出码 0 |
chrome.exe --headless=new --no-sandbox --remote-debugging-port=… |
退出码 21,CDP 端口从未起来 | 退出码 0,CDP 起来(Chrome/152.0.7977.76),导航 + 取回页面文本成功 |
msedge.exe 同一组参数 |
FATAL:mojo\public\cpp\platform\platform_channel.cc:183] Check failed: . : 拒绝访问。(0x5) |
退出码 0 |
也就是说:换 stdio 救不了浏览器(它要的是进程内部 IPC,不是它自己的 stdout),
--no-sandbox / --single-process / --no-zygote、profile 放工作区或临时目录都试过,全部无效;
同一批命令在 danger-full-access 下全部转绿。本机没装 Firefox(只装了 Chrome 与 Edge),
其它浏览器未测试;全访问那一列只有 Chrome 做了完整的「启动 → 连 CDP → 导航 → 取回文本」,
Edge 只做到 --dump-dom 退出码 0。
| 问题 | 结论 | 源码依据(源码级事实) |
|---|---|---|
| 父智能体能否给子智能体指定权限范围? | 不能 | dsh-tool-subagent 的实例配置只有 provider / toolName / modelSelectionSettings / enableRunInBackground / backgroundMode / agentOptions / persona / toolFilter / maxDepth;它的 lib/index.js 里 sandbox 零命中,模型可见 schema 也只多 provider / model / reasoning_effort / run_in_background |
| 能否用 preset 文件改默认权限范围? | 不能 | sandbox-policy(部署默认 mode)、permission(预设表)、approval 三行都在 host-plane 的 @deepseek-ai/dsh-base/cordis.patch.yml 里;模式解析是 request.mode ?? 会话的 sandbox/mode 事件 ?? 部署默认(dsh-sandbox-policy/lib/index.js 的 resolve() / overrideOf()),没有 preset 侧入口能改一个会话的模式。dsh-permission-presets 自己的「已知限制」第一条就写着:预设只组合沙箱模式与审批策略这两个机制级旋钮,agent / profile 选择尚未纳入 |
子代理能否自己升权(sandbox_permissions + 用户批准)? |
不能 | 委派时子会话的审批策略被钉成 never(dsh-subagent/lib/index.js 的 captureDelegatedPolicyOverrides(),注释原话 "the approval policy is pinned to 'never' regardless of the parent's own policy");dsh-user-approval 对 never 直接 return "rejected"、不弹窗。所以专家侧的升权重试是失败关闭,不是弹出审批 |
唯一能把子代理送进完全权限的路径是:用户在会话里把权限切到 danger-full-access。
子会话只继承父会话的显式覆盖值 —— captureDelegatedPolicyOverrides() 取的是
parent.ctx.get('sandboxPolicy')?.overrideOf(parent.session),也就是那条 sandbox/mode 事件,
而切换权限正是写入这条事件的动作(部署默认值不会被继承)。
- 派发前(调度 persona 规则 11):不是
danger-full-access就先ask_user_question, 选项是「已切到完全权限,继续派发」/「改用降级方案:只做web_fetch静态抓取(不能交互)」/ 「暂不做这项网页操作」。用户答已切换后,先确认上下文那行真的变了再派发;没变就如实说没切成功。 - 失败时(
agent_browserpersona):命中上面那张表的任一签名就立刻停手,如实报 「本会话不是完全权限,浏览器自动化不可用」+ 报错原文,不许反复换参数重试、不许假装完成。
这是流程闸门,不是安全边界:它靠 persona 被遵守,机制上拦不住一个不听话的模型 ——
与「allow 是真实边界、persona 只是补充说明」那套口径一致(见「设计要点」)。
刻意没做的事(备选方案与取舍): 可以用一个 preset 侧的 tools/pre-execute 监听器把这条闸门做成
确定性拒绝 —— 那个瀑布是真实存在的(dsh-tools 的 waterfall(carrier, 'tools/pre-execute', exec, …),
非 allow 的判定会带着 reason 变成一次 Error: 工具结果),而 ctx.get('sandboxPolicy').resolve({ session })
能算出含部署默认的有效模式。没有这么做,是因为它会把一条「流程提醒」升级成硬拦(连用户想降级执行也会被一并挡掉),
而要做对就得再起一个包、一条部署路径与一套测试;收益(拦住不听话的模型)与体积不成比例。
真要做时,它应当拒绝 agent_browser 调用、并回一条指向 ask_user_question 的说明,
而不是自己去改沙箱模式 —— 绕过用户批准改沙箱模式,正是这套系统刻意不提供的口子。
能让你手动去登录/过验证码,但不能由浏览器专家直接问你;而且这是默认路径,不是失败。 需要登录态才拿得到目标时,调度者就该照常派发、请你手动登录一次 —— 它不许在派发前就禁止专家登录(把「不登录」写进「本次不做」是明确的反例),也不许为了回避登录先降级成静态抓取。只有你明确说过「不想登录/不想验证」时才走收手那条路。 注意区分两件事:「登录由人在有头窗口里完成」约束的是代理不许自己代填密码、不许绕过登录墙,不是「不许请你登录」—— 这两件事曾被混为一谈,症状就是调度者给浏览器专家下「不登录」的要求(见 docs/evidence.md §12 的复核)。
分工是:专家把窗口开好并停下来说明 → 调度者用 ask_user_question 转达你的选择 → 按你的回答决定「重派/换方式/收手」。
为什么专家问不了(源码级事实):ask_user_question 由 @deepseek-ai/dsh-tool-ask-user 按 preset 注册(不在全局工具层,Adg 组合里那一行是给调度者的,见 preset/agent.cordis.yml 的 tool-ask-user);而 @deepseek-ai/dsh-user-questions 的 ask() 在带上调用者 agent 时只认 live runtime root(agents.roots()),被委派的子代理会拿到 DELEGATED_CALLER —— 那句错误文本自己就规定了做法:
「human interaction is unavailable while the calling agent is owned by another live agent; include the unresolved question or decision in the child agent's final result」。
所以不要把 ask_user_question 加进专家的 allow 名单(加了也不解决任何问题)。
| 你的回答 | 调度者做什么 | 专家做什么 |
|---|---|---|
| 「我去手动登录/过验证,已完成」 | 重新派发同一个 agent_browser,带上专家上一轮报的「CDP 端口 / profile 目录」与「用户已完成」 |
CDP 重连那个已有实例继续,不新开浏览器(登录态在旧实例的 profile 里) |
| 「不想登录或验证」 | 停手,如实汇总「因为未登录,X 拿不到」 | 停手;不绕过、不换路径再试、不拿别的来源冒充 |
| 「试过了还是被挡」 | 停手,把结论交回你换方案(想再试、或换个办法就照常重派 —— 默认不设次数上限) | 停手,报「人工验证未通过」并说明还能换的方式;要不要再试由你决定(不自己反复催、也不换路径偷试) |
| 「换种方式」 | 走降级路径(web_fetch 静态抓取/换来源),或按你的替代方案改派其它专家 |
说明这次拿不到哪些内容 |
人工介入没有次数上限(2026-09-27 修订,按你的要求)。 默认情况下,需要你本人做的事 —— 登录/验证码/二次验证/切换会话权限/要你拍板的选择/要你在本机某处操作 —— 想做几轮就几轮,而且这条口径对所有专家、所有任务都适用(不只浏览器)。原先的「同一条路径的人工介入每任务至多一轮」已删除:它会让智能体把"还能请你帮忙"错判成"已经没救了",于是过早放弃、甚至事前就禁掉某条路径(上一版调度者「要求不登录」就是它和红线误读叠加的结果)。唯一例外是你自己提出的:说过「不要打扰我 / 别问我」→ 需要介入时直接如实报「因为没有打扰你,X 拿不到」,不许换路径偷试;说过「只介入一轮」→ 该任务最多请你介入一次,之后停手如实报。这两种情形都会在交付里写明这是你的要求,而不是它替你做的决定。真正该收手的判据只有两个:你说不想做,或你自己试过仍被挡。
同一份信息默认只在一个站点取(2026-09-27 追加,按你的要求)。 浏览器操作很贵:本机实测一次「开空白标签 → 导航 → 等页面可读 → 读回 → 收走临时页」的下限就是 0.8–2.0 秒(工具侧,还不含它为此付的每一个模型步),而在两个以上站点取同一份信息,基本等于把同一份材料买了 N 次。所以调度者默认不会要求同一个信息在两个以上站点各取一遍 —— 除非 ①你明确要「多源 / 对比 / 交叉验证」,②那个站点拿不到、或各站数据互相矛盾,③交付物本身就是跨站点比较的结果(比价、同款选型)。真需要多源时,它会让一个 agent_browser 在一条委派里串行跑完并把结果合并,而不是开成两条。这条是规则 6(同一实体 + 同一性质的任务只派一次)的浏览器那半,见 preset/design.md I13 ① 与 docs/evidence.md §13。
专家侧的窗口是怎么开的(真机实测,2026-09-26):有头浏览器(不加 --headless)+固定 --user-data-dir+固定 --remote-debugging-port,分离启动(不等在工具调用里)。实测三件事:①启动那个工具调用退出后浏览器还活着(GET /json/version 仍返回 200);②另一个进程能重连并继续驱动同一页面(Page.navigate + Runtime.evaluate 成功);③它是真的窗口(MainWindowHandle 非 0、标题可读),所以你能在里面操作。定位这个实例靠端口号 / profile 目录,不要靠 pid —— 实测启动进程可能已经退出、浏览器却还活着。这套动作现在已经固化在 browser/ 工具链里(launch 幂等、profile 与端口都固定、分离启动),见下一小节。
未观测:真实站点的登录/验证码流程没有端到端跑过(现在验到的仍只是机制:有头窗口 + 幂等复用 + cookie 跨浏览器重启存活)。「关掉浏览器之后再靠 profile 复用登录态」这条已被 2026-09-27 的实测推翻一半:旧观测是"往 profile 写 cookie 后 30 秒内磁盘上没有 cookie 库"(Chrome 惰性刷盘),而现在的实测是 —— 优雅关闭(cli.mjs close)之后 cookie 库确实落盘,且同一个 cookie 活过了浏览器重启(RESULT="adg_probe=1")。所以"让用户登录一次、以后靠同一个 profile 免登录"在机制上已经成立(见 docs/evidence.md §13);仍未观测的是真实站点上真的走完这一步。
agent_browser 用的不是"每个任务现写一个脚本",而是仓库里的 browser/ 工具链:一个入口 cli.mjs、零依赖(只用 Node 内建 + 全局 fetch / WebSocket,要求 Node ≥ 22)、有头窗口、实例活着就复用。装完之后在 ${DSH_HOME:-~/.dsh}/browser/ —— persona 里写的就是这条路径。
node "$env:DSH_HOME\browser\cli.mjs" help # 契约以它为准(选项、输出行、退出码)
node "$env:DSH_HOME\browser\cli.mjs" profile # 排错第一站:profile / 端口 / Chrome
node "$env:DSH_HOME\browser\cli.mjs" launch --url "https://example.com/login"
node "$env:DSH_HOME\browser\cli.mjs" text --url "https://example.com/a" --out page.txt
node "$env:DSH_HOME\browser\cli.mjs" eval --file probe.js --match example.com
node "$env:DSH_HOME\browser\cli.mjs" tabs # 看现在开着哪些页(清理前先看这个)
node "$env:DSH_HOME\browser\cli.mjs" close-tab --match hotels.ctrip.com # 收掉自己开的那些页
node "$env:DSH_HOME\browser\cli.mjs" close # 唯一让登录态落盘的动作登录态是资产,不是每任务重来的消耗品。 profile 固定在 ${DSH_HOME:-~/.dsh}/browser-profile、与会话工作区无关 —— 旧口径是"放工作区里一个固定目录,例如 .browser-profile",工作区一换 profile 就换,这正是"浏览器代理经常被登录拦住"的直接成因。于是流程变成:第一次撞登录墙 → 用户在那个有头窗口里登录一次 → 每次任务收尾 close(cookie 落盘)→ 之后同一个 profile 免登录。要沿用别处已有的 profile 就传 --profile <绝对路径>,不要复制目录。
四条不变的行为(不变量见 browser/design.md 的 I1 / I3 / I5 / I8 / I9 / I10):
| 行为 | 为什么 |
|---|---|
launch 幂等:端口活着就 STATE=REUSED,不重启 |
重启会丢内存里的会话态,而"用户刚登录完"正是最不该被打断的时刻 |
任务进行中不 close;只有本轮交互全部完成、用户不再需要在窗口里操作时才 close |
close 会关掉那个有头窗口;用户可能正登录到一半 |
选页必须命中:--match / --tab 不命中就报错,不随便挑一页 |
静默挑错页会让"读到的内容"与"以为在读的内容"不一致(实测报错原文见 docs/evidence.md §13) |
标签页不堆积:text/eval/shot --url <新地址> 自己开的临时页读完自己收(--keep 才留);存量用 close-tab 点名清;不点名不关、也不许关到只剩 0 个页面 |
一次真实的酒店比价任务在窗口里留下 19 个标签页(12 个携程详情页 + 只差 query 的列表页),"读得越多、页越乱";而关到 0 个页面等于绕过 close,用户开着登录表单的页更绝不能被自动关掉(见 docs/evidence.md §13) |
边界(不做的事):不代填账号密码、不读取 profile 的 cookie 库、不做验证码识别与指纹伪装、不加 --no-sandbox 之类降权旗标、不引入 playwright / puppeteer(旧形态三条伪装旗标齐全,见 browser/design.md「非功能红线」);不判断哪一页"已经不需要了"—— 只关自己刚开的页与调用方点名的页。登录永远由人在有头窗口里完成 —— 短信与图形验证码都靠"把窗口开好 → 停手 → 调度者转达"这条人工介入链路(见上一节),自动化只负责把页面开到那一步。
未观测:真实站点的登录墙端到端(用户真的登录 → 专家真的抓到登录后内容)没有跑过;专家是否真的照 persona 用这套工具(含收尾点名清理),也没有真实 Adg 会话为证。实测到的是机制 —— 逐条见 docs/evidence.md §13。
- 真机实测(2026-09-26,同一台机器上的 A/B,只改会话文件策略):上面那张两列探测表;
原始输出与复现脚本见
docs/evidence.md§11。「全访问下浏览器确实可用」是实测(Chrome 走完了 启动 → 连 CDP → 导航 → 取回页面文本),不再是用户报告。 - 源码级事实:三个问题的结论,两处「子代理被钉死」的位置(路径见上表),以及人工介入为什么
只能由调度者转达(
ask()只认agents.roots(),子代理拿DELEGATED_CALLER)。 - 未观测(人工介入):真实站点的登录/验证码流程没有端到端跑过;「调度者是否真的每次都转达」 同样没有真实会话为证;「关掉浏览器后再靠 profile 复用登录态」也没有证据(cookie 30 秒内没落盘)。 已实测的只是机制:有头窗口存活 + 跨工具调用 CDP 重连 + 能继续驱动。
- 未观测:「调度者是否真的每次都先问」没有实测 —— 闸门刚落地,还没有一次真实 Adg 会话走过它;
「用户在沙箱外自己拉起带
--remote-debugging-port的浏览器、专家只连那个 CDP 端口」这条路 设计上可能可行(受限策略下网络不受限,受限进程自建监听与本地fetch都通)但 本仓库未实测,不许写成可行 —— 而且既然全访问下浏览器本来就能用,这条路只在「用户不愿切权限」时才有意义。
- 新对话选择 Adg 多智能体模式,直接说需求。
- 调度智能体自己负责意图理解、任务拆解、调度与汇总,先判断范围再按 9 个专家的范围派发:
| 需求范围 | 派给 |
|---|---|
| 文件与文档(检索、整理、转换、生成) | agent_file |
| 系统 / 硬件 / 设置 / 清理 / 故障排查 | agent_computer |
| 软件与 App 操作(CLI、adb、winget、小程序) | agent_app |
| 网页登录 / 填表 / 点击 / 多页抓取 | agent_browser(需本会话为完全权限;不是的话调度者会先停下来问你 —— 见「浏览器专家需要完全权限」;走 browser/ 工具链:有头窗口 + 登录态跨会话复用) |
| 全网检索与综述(只搜不点) | agent_search |
| 在本地代码库与文件里定位事实与出处 | agent_researcher |
| 改工作区代码并自证 | agent_coder |
| 审查已有改动 | agent_reviewer |
| 把一整件工作交接出去、另开一个上下文做 | agent_general(只在用户显式要求时;不按能力范围"自动"选中 —— 见下) |
agent_general 的触发条件是你说的话,不是任务的性质:明确说了「把这件事交给子代理 / 让另一个
智能体接手 / 另开一个上下文去做 / 你自己别做,交接出去」才会派;调度者不会因为"任务看起来很大"
"想省自己的上下文""想并行"就自行改派。交接出去之后:它在自己的上下文里从头负责到底,收尾时按运行时
自动注入的指引用 send_message 把结论回报给调度者(它的 final message 也会作为结算通知的 closing
message 一起回来);需要继续时,调度者用 list_agents + send_message 接给同一个它(沿用已
持久化的会话与上下文),要换角色或你想再开一个新上下文时才另派。
- 需要多个专家时(实体不同或性质不同才拆),它在同一条回复里并行启动多个委派,不串行等待;
而且一律以后台方式派出(不设
run_in_background: false)—— 只有后台的 continuable 子代理才能被你在界面上发消息、随时停掉,也才能被调度者按下面那条复用;阻塞调用会把它降级成 一次性运行(不进list_agents、send_message报NOT_RESUMABLE),复用与"省重读"当场失效。 后台不等于"结果丢了"、也不需要它停在原地等:即使下一步马上要用这份结果,也照样后台派出, 然后结束本轮 —— 子代理结算时会带着它的收尾正文唤醒调度者,后续步骤照常接上; 没有任何"可以阻塞"的例外。同一个代码库 / 文档库 / 站点的多个"方面"不会各派一个子代理,而是合并成一条委派、让一个 专家一次通读并按方面分节产出;同一实体的后续任务优先接给已经读过它的那个专家 (list_agents+send_message),不重新开一个 —— 但接之前要先看它的status:它还在running时send_message是 steer(会插进它当前任务那一轮),只有"修正/补充同一件事" 才该现在发,"另一件事"要等它结算后再接给同一个它;大范围改动先定位再动手(先让agent_researcher出path:line清单,再让agent_coder按位改);多个不同性质的专家要看 同一份大材料时,用前一个专家的返回结果当 digest 中转(长了才落成临时目录里的文件、 交付前删掉)。每条委派都必须带验收标准与"本次不做",而且派发前要过必要性闸门: 答案不会改变交付物、又不在验收标准里的旁路不派子代理,只在最终答复里挂号 「未纳入本次:X(可能影响 Y,未调研)」—— 省钱但不隐瞒。见 多智能体的 token 消耗。 - 专家越界时会回一句「超出能力范围,需要 agent_X」;调度智能体据此再派发下一步,链路可追踪。
专家之间不能直接互相转交 —— 它们看不到
agent_*名册行(见「设计要点」)。 - 专家的返回值只是给调度智能体汇总用的中间材料,不是给你的最终答复;最终交付由调度智能体整理后给出。
在任何模式里说,例如:
给 Adg 加一个智能体:文档员,负责把内部笔记改写成对外口径,只能读不能改文件,需要改动时报告需要 agent_coder。
AI 会加载技能 adg-add-agent,一次问清岗位 / 能力范围 / 越界时报告需要谁,然后改两处
(新增专家行 + 顶部调度名册)。改完在仓库里跑一次自检:
node tools/check-preset.mjs通过(exit 0)之后重启 dsh 即可生效。
自检还会核对承载三组体积旋钮的那三行(compaction-basic / tool-result-pruner /
tool-web)是否完好 —— 行在、包名对、没被 disabled 关掉、config: 里没有插件不认识的键 ——
并且会报告生效值:本 preset 刻意不覆盖任何旋钮,所以打印出来的是插件出厂默认值:
体积旋钮(生效值):compaction 0.8(默认)/0.16(默认) | pruner 8192(默认)/4096(默认)/1024(默认) | tool-web 200000(默认)/8(默认)/4(默认)
裁剪后实际吐出(按生效配置算):head 4096 + 标记 39 + tail 1024 = 5159,threshold 8192
它不再钉死这些取值:万一有人把某个键写回去,自检只检查该值在不在插件会接受的范围内 (写错会在挂载时抛错),并在摘要里把它标成「已覆盖」。依据与取舍见 token 成本纪律。
- 刻意删掉了通用的
subagent/subagent_fork行。 子代理会继承父代理的整套 composition;一旦存在通用委派行,专家就能绕过自己的范围再开一个"什么都能干"的子代理 (已在创造模式实测复现)。删掉之后,「每个智能体只负责有限范围」才真正成立。 注意这现在是双重保险:allow名单本身也会把未列出的工具裁掉,通用委派行与专家 名册行都进不了专家的工具目录。 toolFilter.allow是真实的能力边界,不是提示级约束。 已实测:给agent_coder委派任务,它报告的可见工具目录恰好等于它的 allow 名单,agent_*名册行与通用subagent都不在其中 —— preset 自己注册的工具也一起被裁。persona 只是补充说明。- 因此专家之间不能直接互相转交。 专家越界的正确做法是回一句「超出能力范围,需要
agent_X」,由调度智能体据此再派发下一步(链路可追踪)。如果确实想让某个专家能直接
转交,把对应的
agent_*名字加进它的allow即可。 - 能力边界不是安全沙箱。
allow给出的是工具面上的真实限制(专家的工具目录确实被裁), 但pwsh这类通用执行工具本身能做很多事 —— 例如agent_reviewer的allow里有pwsh, 机制上它完全能改文件,靠的是 persona 里"只报告不修改"这条提示级约束。要的是分工清晰、 越界会被报出来,不是权限隔离。按工具面看,硬边界(toolFilter强制、越界直接调不动) 是agent_researcher(无 write/edit/pwsh)、agent_search(只有联网工具)、agent_reviewer(无 write/edit);agent_file与agent_coder(只差一个read_image)、agent_computer与agent_app(完全相同)的工具面几乎一样,它们的边界靠 persona 与调度规则维持。 allow里只能写已注册的工具名。dsh-tools的restrict()遇到未知名会直接抛names unknown global tool ...。改 composition 的 tool 行时要同步tools/check-preset.mjs里的KNOWN_TOOLS,并用它提前拦下拼错的名字。条件性注册的名字 (bash在 Windows 上被disabled关掉、read_image依赖attachments服务)与策略越界 的名字(workflow/ralph只留给调度智能体)都只给「提示」—— 所以看到1 个警告仍是 通过(exit 0)。自检里 ERROR 的含义只有一个:这次委派必然抛错(名字没注册)。- 有
pwsh的专家一律同时给job_list/job_output/job_kill。 工具的指导段落 会讲后台任务,只给pwsh不给收集工具会让"后台跑了但取不回来"。
这一节的每个数字都是实测的,不是估算。测量基线:32 个 Adg 会话、983 次模型请求。 (当前口径:preset 侧不设任何体积上限 —— 下面的数字是"为什么会有第二层插件"的依据, 也是撤销那些闸门时的对比基线。)
| 观测量 | 实测值 |
|---|---|
| 总 token | 94.1M = 未缓存输入 8.0M + 输出 0.9M + cache-read 85.1M(下界:见下) |
| cache-read 占提示 token | 91% |
| 输出占总花费 | 1% |
| 调度智能体 / 专家 | 55.9M(59%)/ 38.2M(41%),22 个子代理 |
| 每个子代理 | ≈1.73M token |
| 子代理内部工具结果 | web_fetch 3.5M 字符 / 369 次(平均 9,477,被截在 50,000 附近);read 1.68M 字符 / 321 次(平均 5,222);grep 705k 字符 / 171 次 |
这三个数字的口径要一起看,否则会读错:
- 94.1M 是下界,不是全量。
cacheWriteTokens在全部 1183 个已记录的 usage 对象里 都不存在,所以 cache-write 只能记成 0 —— 它的含义是「provider 没上报」,不是「没有 cache 写入」。真实账单只会比 94.1M 更高。 - 语料是活的。 审计脚本跑的同时会话日志还在增长,报告里的计数是某一刻的快照, 两次跑出来的数字不会完全一致;对比时看比例与量级,别抠绝对值。
- 报告内部有约 20,000 token(0.02%)的口径差。
=== sessions by preset ===里adg那一行的分组总计,与main + sub(origin为user/subagent)拆分之和对不齐: 有少数行既不是user也不是subagent来源,拆分口径没有覆盖它们。量级可忽略, 但引用数字时要说明用的是哪个口径。
成本驱动因素是「上下文体积 × 步数」:每一步都要把整段上下文重发一遍,所以 91% 的提示 token 是 cache-read —— 便宜的单价换不来小体积,体积本身就是账单。
策略只剩一条:压步数。 体积那一侧本 preset 不再手动压低 —— 早先版本压过三组旋钮, 并在 persona 里写了「读取预算 / 只读几个文件 / 结论 2000 字符内」,后来整体撤销(理由见 为什么撤销 preset 侧的体积闸门)。 现在压步数的是第二层插件的步数收敛检查点(见 第二层)。 这条分工有真机依据:实测里有一个子代理烧掉 48,992,135 token(历史证据,来自已移除的 token 两档,不代表当前行为),越过当年那条 300 万预算线后又走了约 300 步 —— 那种跑飞只有拦在 步数上才有效,而砍单条结果的体积只会同时砍掉结论质量。
**刻意没有做的事:**不给任何请求设 maxTokens,也不设 reasoningEffort。理由是输出只占账单的
1%,压它对账单几乎无影响,却会直接损伤回答质量(被截断、推理不足导致返工,反而增加步数)。
在这份 composition 里这两类键一律不出现 —— 改预设的人不要"顺手补上"。
刻意没做的第二件事:没有在本 preset 里再加一行 spill-policy。spill-policy 是宿主plane
的行(dsh-base\cordis.patch.yml,maxInlineBytes: 50000),而 web 组合并没有把它 disabled
(web-app 的 patch 里 spill 零命中),所以它本来就对 Adg 会话生效。它注册的是一个
{ prepend: true } 的 tools/post-execute 监听器,再加一行就是同一条瀑布上叠第二个监听器 ——
重复施加没有验证过,id 按树唯一、也不会报错,只会静默叠加。宿主那一行已经在做同样的
溢出处理,而 preset 侧那三个压低上限的键又已经撤销、回到出厂默认值,所以这里再加一行没有净收益。
(另注:spill-policy 的 model-facing 那一路显式跳过 read,所以它本来也管不到 read
那 1.68M 字符。)
本 preset 不覆盖任何体积旋钮,那三行只声明插件本身(compaction-basic /
tool-result-pruner / tool-web)。生效值就是出厂默认:
| 键 | 生效值(出厂默认) | 曾经的覆盖值 | 现在为什么不再覆盖 |
|---|---|---|---|
compaction-basic.thresholdRatio |
0.8 | 0.6 | 0.6 会让窗口用到 60% 就压缩;摘要一旦生成,细节只剩摘要里那一份,对要精确路径/行号的委派尤其不利 |
compaction-basic.retainRatio |
0.16 | 0.12 | 逐字保留的最近上下文从 16% 缩到 12%,省下的额度买不到等价的摘要质量 |
tool-result-pruner.thresholdChars / headChars / tailChars |
8192 / 4096 / 1024 | 4096 / 2048 / 768 | 单条工具结果超 4096 字符就被掏空中间段。信息少了,模型不是收敛,而是重取、换查询或拿残缺证据下结论 |
tool-web.fetchMaxOutputChars |
200000 | 24000 | 一次 web_fetch 只留 24k 会把调研型委派唯一需要的东西切掉;web_fetch 是子代理里最大的单一上下文来源(3.5M 字符 / 369 次),但"大"不等于"该砍" |
tool-web.searchMaxResults / searchMaxQueries |
8 / 4 | 5 / 3 | 收窄检索面只会让同一件事被检索更多轮,而步数才是账单里的乘数 |
那一轮压低是实测之后加上去的(基线见本节开头),撤销同样有实测理由:
- 截断工具输出会把事实切掉。 被裁掉的是工具已经取到的原文;模型看到残缺结果只有三条路: 重取(更贵)、换个查询再试(还是更贵)、或者拿残缺证据下结论(更差)。
- 提前压缩会让上下文失真。 压缩不可逆:过了压缩点,一切只能基于摘要。
- 写在 persona 里的预算提示会压低输出质量。 「委派 prompt 必须自带读取预算」「结论控制在 2000 字符内」「一次派发不往返」这类纪律把专家的注意力从"把事情做对"挪到"别写太多 / 别多读", 漏项与返工本身就是新的成本。
- 真正压住成本的是
dsh-adg-token-budget。 它按步数干预(子代理进入命中阶梯的某一步时 注入一条可选提醒,让子代理自己判断要不要收敛),不牺牲单条结果的完整度,也不截断产出。 两层的分工因此变成:插件管步数,preset 不再管单条结果的体积。
约束(只在有人把某个键写回去时才相关;写错不是静默生效,而是挂载时抛错):
- 两个 ratio 必须在
(0, 1],且retainRatio < thresholdRatio; - pruner 要满足
headChars + 标记 + tailChars ≤ thresholdChars—— 标记就是@deepseek-ai/dsh-compaction-tool-result-pruner里的PRUNE_MARKER("\n\n[... tool result middle pruned ...]\n\n"),长度正是 39 字符 (自检里的常量PRUNER_MARKER_CHARS = 39);只看head + tail会漏掉这 39 个字符; fetchMaxOutputChars/searchMaxResults/searchMaxQueries都必须是正整数 (tool-web用同一个assertPositiveInteger校验searchMaxResults、searchMaxQueries、fetchTimeoutMs、searchTimeoutMs、fetchMaxOutputChars五个键), 且fetchMaxOutputChars不得超过插件默认上限 200000。
自检的口径也随之改了:它不再钉死这些键的取值(原先顶部的 EXPECTED_BUDGET 已删除)。
现在只做两件事:确认三行结构完好(行在、包名对、没被 disabled 关掉、同一个 id 不重复),
以及"万一某个键被写回"时该值落在插件会接受的范围内;摘要行会明确标出哪些键「已覆盖」。
tools/check-preset.mjs 静态挡住的是这几类(运行它的方式见「怎么加一个智能体」与
「给 AI 的安装指令」第 8 步):
- 三个旋钮行的结构:行必须在、
name:必须是那个包名、不能disabled: true、同一个 id 不能出现两次; - 万一某个旋钮键被写回:它必须直挂在该行的
config:下(不能嵌更深、也不能提到与name:同级 —— 那两种写法运行期都会静默用默认值,而写的人以为它生效了),并且取值必须落在 插件会接受的范围内 —— 两个 ratio 的区间(0,1]与先后次序retainRatio < thresholdRatio; pruner 的带标记算术headChars + 39 + tailChars ≤ thresholdChars且三个数都是正整数;fetchMaxOutputChars/searchMaxResults/searchMaxQueries都是正整数、且fetchMaxOutputChars ≤ 200000(> 60000另给一条 WARN); config:里不能有插件不认识的键(插件自己的validateKeys遇到未知名会直接抛错);- 不检查"取值等于某个数" —— 那正是被撤销的口径(顶部的
EXPECTED_BUDGET已删除)。 现在它只报告生效值,并把显式写回的键标成「已覆盖」。
但要说清它的边界:这个自检是逐行文本扫描器,不是 YAML 解析器。 它证明不了整份文件能被
YAML 解析(例如同一行里写两个键、锚点/别名、flow 风格 {a: 1}、制表符缩进等,它都看不出来),
也证明不了插件真的挂载:包能不能解析、行有没有被 disabled/条件表达式关掉、服务有没有发布
到全局 realm,这些只有重启后按「给 AI 的安装指令」里那套 resolve('adg') /
resolve('adg') 的 .broken 为空 / compositionInventory() 做一次真实挂载才能证明。
换句话说:自检通过 = "这些硬约束在文本上没被破坏",不等于 "运行期一定按这个口径生效"。
旋钮撤销之后,persona 里不再有任何 token/读取预算类的文字。早先版本在调度者 persona 里写了
一整段「委派预算」,八个专家每条 persona 末尾也各有一行「成本纪律」(先 grep 定位再按需 read、
offset/limit 分段读、同一文件不重复读、工具结果被截断时收窄查询、结论控制在 2000 字符内、
不要回贴原始工具输出或正文);这些已全部删除,理由见
为什么撤销 preset 侧的体积闸门。
调度者 persona 里现在还多五条与成本有关的规则(规则 6 / 7 / 13 / 14 / 15:同一实体 + 同一性质的
任务只派一次(含浏览器那半:同一份信息默认只在一个站点取 —— 2026-09-27 追加,一轮浏览下限实测
0.8–2.0 秒)、同一实体的后续任务接给已经读过它的那个专家、大范围改动先定位再动手、跨专家传递大材料
走 digest、派发前过必要性闸门并给未纳入的旁路挂号),外加一条输出/交接去冗余纪律(规则 5 / 10:
分字段写、不回贴原文、不重复、不转述过程,"未验证 / 未纳入"必填)。它们与上面被撤销的那一层不是一回事,
边界写死在 preset/design.md 的 I10 / I13 / I14 / I15:被撤销的是子代理预算("你能读多少、结论
写多长",改的是单个专家的行为,代价是漏项与返工);留下的是编排层规则与输出纪律("派给谁、
派几次、材料怎么中转、要不要做、写下来的东西怎么组织",改的是调度者的计划与交接件的形态,省掉的是
同一份材料被重复买 N 次、根本不必读的那一趟、以及被重发几十遍的重复文字;顺带让多路结论不再互相
矛盾)。前者已被实测判定为净负,后者不限制任何单个专家的读取量与产出量 —— I15 明文禁止被改写成
字数上限。见 多智能体的 token 消耗。
与上面这些同属调度层、但不是预算也不省 token 的还有一条:规则 8 要求委派一律走后台
(run_in_background 不设)—— 理由是阻塞会把这一次委派降级成一次性运行,规则 6 / 7 省下的那笔
"重读"退回原价,用户也无法在界面上给该子代理发消息或停它;口径(没有任何阻塞例外,前台不省时间)与源码依据见
preset/design.md I17。
调度者 persona 里与 dsh-adg-token-budget 配套的那段步数收敛政策也已删除(原先两条:
① 委派 prompt 写明大致收敛规模——检索与阅读类 10–15 步、实现与审查类 15–25 步;② 不要自己
轮询专家步数)。删除的理由是这两条都不是"只有 persona 才能说"的话:
- 注入的那条消息是自足的:
stepNudgeText()把它渲染成"【收敛检查点 n/N】调度代理提醒: 这是你的第 N 步"+ 二选一,子代理不需要委派 prompt 先放一个步数区间才有对照物。 - 那两个数字是第二套真相,而且必然脱钩:阶梯是
[4, 8, …, 280]、从第 4 步就开始提醒, persona 里却写"检索类 10–15 步"。改阶梯是热重载的config:改动,改 persona 要重启 dsh —— 两边不会同时被想起。 - "不要轮询"在更底层已经说过一次:出厂的
job_output/subagent工具说明就写着 settle 时会收到通知、不要忙轮询。
仍然保留的是九个专家 persona 末尾各一句「收敛纪律」——"收到步数检查点提醒时按提醒里的二选一 自己判断:产出够用就收敛汇报,确实还有必需工作就继续做并说明理由 —— 不要为了回应提醒而砍掉 必需的工作"。插件只把提醒推进上下文,不管子代理怎么反应,所以这句仍归 persona;它写的是 自己该怎么应对,与注入方同一套措辞,子代理才不会有"这是谁在说话"的歧义。
(证据要求仍在各自那条「输出要求」里:结论要带 path:line 或 URL 证据、未核实的推断显式标注 ——
那不是预算,是可信度要求,所以没有跟着删。)
这一节回答一个具体问题:在"不压体积旋钮、不给单个专家写预算"的口径下,还能怎么降 token。 每条都标状态(已落地 / 建议 / 不建议)并给出理由与量法。
先看成本结构,否则容易优化错地方:实测基线是 32 个 Adg 会话、983 次请求、94.1M token, 其中 cache-read 占提示 token 的 91%;调度智能体占 59%、22 个子代理占 41%,每个子代理 平均 ≈1.73M。所以乘数是步数、被乘数是上下文体积,而"同一份材料被 N 个子代理各读一遍" 同时抬高两者。
| 手段 | 状态 | 理由 | 省在哪 / 怎么量 |
|---|---|---|---|
| 同一实体 + 同一性质的任务合并成一次委派(调度 persona 规则 6) | 已落地 | 每个子代理都会把同一份材料从头读一遍;N 个同类委派 = 同一份材料买 N 次 | 直接观测量是子代理个数(同一实体的同类委派应只产生 1 个)。规则 6 / 7 合计给调度者 persona 加了 594 字符(按 ~4 字符/token 估算约 150 token/步)——用一次合并换掉一整个子代理的固定开销加全量重读 |
同一实体的后续任务接给已经读过它的那个专家(规则 7,list_agents + send_message) |
已落地 | 被恢复的子代理沿用自己已持久化的会话:dsh-subagent 的 coldResume 从会话日志重建(源码注释:no subagent provider is dispatched),那份材料不必再读一遍 |
复用的代价是一次增量对话,重派的代价是"重读一遍 + 随之而来的步数"。注意恢复不是免费:既有上下文仍会作为 cache-read 重发,省的是重复读取与步数。接给谁之前要先看 status —— 见下一行 |
running 时不得把"另一件事"steer 进去(规则 7 的 status 判据,2026-09-29 追加) |
已落地(提示级) | 模型侧 send_message 恒为 steer(源码级事实:dsh-tool-subagent-control 的 ctx.subagents.sendMessage(...) 没有 delivery 参数,包内固定 deliverToChild(..., { delivery: "steer" }) → agent.steer() → inbox.nextStep),queue 只在宿主级 queuePrompt 里。所以向正在工作的子代理发消息=把新输入插进它当前轮的下一步:新问题与原任务的收尾会合并成同一条 closing message,而结算通知带的正是这段合并文本,事后分不清哪半句答的是哪件事;正在做验证的那一轮还可能被带偏原验收标准 |
判据只能是新输入与在飞任务的语义关系,不能是"它快做完了"(list_agents 只有 running / inactive、不含进度可看)。三分支:修正/补充同一件事 → 现在发(steer 的本用,比重派省下整个"重读 + 步数");同一实体上的另一件事(另一条验收标准 / 另一个交付物)→ 结束本轮、等结算通知把它唤起后再接给同一个它;取代在飞任务 → 先 interrupt_agent 停当前轮再发。代价 +517 字符:prefix 正文(不含换行)7219 → 7736。未观测:真实会话里调度者是否照做(量法见 preset/testing-guide.md I13 N11) |
一两次抓取就能答完的已知 URL 定点核对,调度者自己 web_fetch(规则 1 的补充) |
已落地 | 为一次抓取派出子代理,要多付一整个专家的固定开销(人设 + 工具目录 + 起手读取);web_fetch 还是子代理里最大的单一上下文来源(实测 3.5M 字符 / 369 次) |
看委派调用次数。代价是调度者自己的上下文变长(它占 59%),所以只适合"一两跳能答完"的活 |
| 压缩调度 persona 里与专家重复的浏览器细则(规则 11/12) | 已落地 | 规则 11+12 原先合计 1281 字符(当时占 prefix 块的 35.6%),而失败签名、端口 / profile / CDP 重连这些机制细节在 agent_browser 的 persona 里已各有一份(2030 字符);调度者需要的只是决策 + 转达 |
现已压到 812 字符(−36.6%,约 −117 token/步)。压缩只动措辞不动语义:danger-full-access 判定、ask_user_question 三选项、四分支处置、「同一条路径至多一轮」全部保留(preset/design.md I11/I12;其中的「至多一轮」已于 2026-09-27 按用户要求删除 —— 它是压缩当时的事实,现在已不存在);失败签名与根因现在只在专家 persona 与 浏览器专家需要完全权限 里出现 |
| 探索型委派回传 digest 工件,后续专家读工件而不是重读源材料(规则 14) | 已落地(带清理口径) | 同一批材料要被不同性质的多个专家看时(规则 6 不合并它们),第二次之后的读取可以换成读 digest | 默认档零文件:第一个专家的返回结果直接塞进下一条委派 prompt。只有长到明显撑大调度者上下文(它占账单 59%)才落成文件,且只写平台临时根下的 adg-digest、只传绝对路径、任务结束即删、删不掉要如实说、read-only 下不造工件、绝不写进工作区(散落中间文件会被误提交)。机制前提是包文档级事实(@deepseek-ai/dsh-fs-sandbox「围栏行为」:读取三种模式都不受限,workspace-write 可写工作区 + 平台临时区域,read-only 拒绝一切变更)。清理可行的前提是后续问题按规则 7 接给已经读过它的专家,不依赖遗留工件 |
大仓库改动先让 agent_researcher 产出 path:line,再让 agent_coder 按位改(规则 13) |
已落地 | 两者性质不同(只读调研 vs 写入改动),本来就该拆;researcher 的输出正好是 coder 的输入 | 多一跳,但 coder 不必再全仓搜一遍(省下重复读取与步数)。这是规则 6 的反例校准:拆要看维度,不是一律不拆。仓库很小或改动点已明确时不要多这一跳 |
| 委派 prompt 五项必填:目标 / 验收标准 / 本次不做 / 已知事实 / 期望产出(规则 5) | 已落地 | 没有验收标准,专家只能自己猜边界,猜宽了就去探索旁路;没有"本次不做"清单的"专注"是空白授权 | 可查:转录里能不能指出这条委派的验收标准与排除项。规则 5 由 53 字符扩到 171 字符;规则 6 收紧"实体"锚点后再加 115 字符 —— 两者合计 +233 字符 |
| 必要性闸门 + 强制挂号(规则 15):三问任一"否"就不派;不做的旁路必须在最终交付里挂号 | 已落地 | 四条编排层规则解决的是"不重复读",这一条解决"少读不该读的"。来源是一次真实任务:要"便携小巧的录音笔",调度者为"录音合规性"单独开了一个子代理 —— 那次调研只服务同一条选购需求(同实体同性质),且不在验收标准里 | 主指标是子代理个数(少开一个就是少买一份材料)。代价是规则 15 的 300 字符(≈75 token/步),少开一个子代理就值回票价(单个子代理实测均值 ≈1.73M)。质量侧靠挂号抽查:最终答复里有挂号句「未纳入本次:X(可能影响 Y,未调研)」、转录里没有对应委派 = 遵守;挂号句缺失 = 旁路被静默丢掉(比不做这条规则更糟) |
| 委派 prompt 分字段写 + 输出/交接去冗余纪律(规则 5 / 10) | 已落地 | 输出只占总花费 1%(0.9M / 94.1M),压它本身毫无意义;真正的杠杆是写下的字会变成上下文 —— 每个字都在后续每一步作为 cache-read 重发(cache-read 85.1M = 90.4%)。所以被乘数最大的是两件交接件:委派 prompt(专家每一步都读)与专家返回结果(调度者余下每一步都读、还会成为最终答复的素材);反过来最终答复的措辞后面没有更多步,省不到钱、只影响可读性 | 四条禁止式判据:① 不回贴工具输出原文(给位置就够)② 同一结论只说一次,后文用"见上 / 第 N 条"引用 ③ 不转述中间过程 ④ "未验证 / 未纳入"必填块不许为求简短省略。没有字数上限 —— 一写成"N 字符内"就精确退化成已撤销的那层。观测量:adg 行的 output + 同口径重跑后的 cache-read 增速 + 三个抽查(回贴重合 / 重复率 / 未验证块是否仍齐全)。规则 5 加 106 字符、规则 10 加 130 字符(prefix 块 4584 → 4820) |
| 交付形态 + 截断接续(规则 5 / 7 / 10,2026-09-29 按用户要求追加) | 已落地(机制实测 + 只读扫描量到 6 条 / 285 会话;收益未量) | 截断是 {kind:"max-tokens"} 的正常结局:provider 把 API 结束原因映射过来、agent 循环据此正常 return(不抛错、也不走 agent/request-error,所以没有内建重试),已产出的文本照常落进会话,只是未完成的 tool-call 块被整体丢弃(@deepseek-ai/dsh-llm/lib/index.js:1053)。触发只可能来自子代理自己:用户能从界面点/发"继续",而被委派的子代理发不了、GUI 里也只有一条客户端合成的提示(无按钮),所以"谁去接"只能落在调度者身上 —— 不接,这次委派就停在半句上 |
三处:① 规则 5 的期望产出写明"产出大时分段交付"(先给结论 / 证据位置 / 未验证的梗概,再分段给大正文)② 规则 7 补接续半条并钉死死顺序 —— 被截断立刻 send_message 接给同一个它、请它从断点续写(截断不改变可续性,同一 child session 可直接续跑);"换人"只留给"它已无法接续"或"整段驻留期已厚、要结轮"③ 规则 10 ⑤ 改写成预防式:"输出上限不可预测,所以大产出按规则 5 分段交付、不要憋到单条回答里" —— ⑤ 不再是"被截断后自己接着写"那个恢复动作(恢复归规则 7)。没有任何字数 / 产出量上限(design.md I13 / I15)。观测量:转录里截断后有没有指向同一个子代理的 send_message(有 = 接住;没有、且它之后再无产出的那截内容 = 停在半句)。代价 +1346 字符(prefix 正文 7736 → 9082,≈320 token/步)—— 本仓库历次规则改动里最大的一笔,换"一次截断不必从头重做"。已量到(只读扫描 285 个会话档案):turn/end 的 reason.kind 分布 completed 385 / aborted 36 / max-tokens 6 / error 5 / interrupted 2,6 条全部落在 agentPreset:"adg" + origin:"subagent" 的被委派子代理里(3 条是长产出:51233 字符的长文断在半句上等);而这 6 个会话在截断后记录数为 0(无 assistant/message、无 user/message)⇒ "截断后就地接续"在本机完全没有先例,正因如此才必须写成调度者的动作。读档案的坑:session.v*.jsonl.zstd 是多帧 zstd 拼接,必须先按 magic 28 b5 2f fd 切帧(Node v26 的 node:zlib 自带 zstdDecompressSync,不需要外部 zstd)。本机单条输出上限 ≈ 32768(pi-ai 适配器默认;与上下文窗口 262144 是两件事) |
| 9 份重复的"后台委派"提示段 | 框架侧,preset 改不了 | dsh-tool-subagent 给每个 continuable 委派行注册一段 systemPrompt 段落(lib/index.js 的 install() 里 systemPrompt.section({ name: 'tool:' + toolName … })),文本几乎相同、只差工具名 —— 本 preset 有 9 行 |
调度者系统提示里约 330 字符 × 9 ≈ 2.9 KB/请求(字符数可数,token 按 ~4 字符/token 估算约 0.7k,占 94.1M 的 <1%)。不要为了省这点删专家行;要修只能在框架侧合并成一段共享段落 |
压三组体积旋钮 / 设 maxTokens / 写"结论 N 字符内" |
不建议(已撤销的口径) | 截断会把工具已经取到的事实切掉;输出只占账单 1%,压它只损伤质量并招来返工 | 见 为什么撤销 preset 侧的体积闸门 |
persona 的体积账:prefix 正文(|- 之后的正文行、不含换行)现在 9082 字符。本轮
(交付形态 + 截断接续:规则 5 分段交付、规则 7 接续半条、规则 10 ⑤ 改写)加了 1346 字符
(7736 → 9082,≈320 token/步)—— 本仓库历次规则改动里最大的一笔,理由是它买的是"一次被截断的
委派不必从头重做",而截断频次本轮已经量到:只读扫描 285 个会话档案得 max-tokens 6 条(分布见
上一行的手段表),6 条全是被委派的子代理、且截断后都没有续写 —— 即"机制实测有了、收益仍未量"
("接住之后产出是否完整"仍无证据)。上一轮(输出/交接去冗余纪律:规则 5 分字段写 +
规则 10 四条禁止)加了 236 字符(4584 → 4820);再上一轮(规则 5 五项必填 + 规则 6 实体锚点 +
规则 15 必要性闸门)加了 534 字符(4050 → 4584);加规则 13 / 14 时 +868、压缩规则 11 / 12
减 469,净 +399(3598 → 4050)。注意两套口径不可混用:上面 4584 / 4820 是早先按"整个
prefix: |- 块"量的旧口径,7219 / 7736 / 9082 是顶注第 13 条起改用的"正文行、不含换行"口径
(同一份文件,旧口径比新口径大约多出正文行的换行数)——跨口径不能相减。压缩是为语义腾
位置:不先把与专家重复的机制复述删掉,再加规则就会把调度者的每步系统提示推向 10k。也正因为
persona 每加一条都在加固定成本,新规则只写判据与动作,机制细节一律指向专家 persona 与本文。
四个观测量:怎么判断这些规则真的落地(不许只看总 token —— 语料是活的,preset/design.md
的 I3b 禁止拿基线直接比):
| 观测量 | 取值方式 | 判断什么 |
|---|---|---|
| 子代理个数(主指标) | list_agents 条目数,或审计脚本按会话数子代理会话 |
"同一份材料买 N 次"的乘数就是它;规则 6 与规则 15 生效时掉的就是这个数 |
| 步数 p50 / p90 | 从会话日志取每个子代理的 stepCount 排序取分位(p50 = 中位数:一半子代理 ≤ 它;p90 = 只有最长的 10% 超过它) |
规则 5 的验收标准写好之后,长尾(p90) 应下降。不用平均值:已实测中位数 39 步、p10 只有 6、四分之一 ≤14 步,分布很偏,均值被长尾拉高 |
requests |
用下面「怎么重新测量」的审计脚本看 adg 行 |
步数≈请求数,这是最不容易被语料变化干扰的项;但必须同口径重跑同一批会话,不许拿单次绝对值比 |
| 挂号抽查(质量侧,人工) | 抽 3–5 个含旁路诱因的任务,看两件事 | ① 最终答复里有挂号句 ② 转录里没有以该旁路为主题的委派 —— ①有②无 = 遵守;①无 = 静默丢掉(更糟);②有 = 闸门未生效 |
| 去冗余抽查(质量侧,人工) | ① 结果里有没有大段与工具输出原文重合 ② 相邻 assistant 消息的重复率 ③ "未验证 / 未纳入"块是否仍齐全 | ③ 是反向检查,比前两条重要:它验证"求简洁"没有把诚实护栏删掉。配合 adg 行的 output 与 cache-read 增速一起看 |
未观测:到本次改动为止,还没有一次真实 Adg 会话带着这五条编排层规则 + I15 输出去冗余纪律
跑过,所以"调度者是否真的按规则 6 合并、按规则 7 恢复、按规则 13 先定位再改、按规则 14 用 digest
并清掉工件、按规则 15 过闸门并挂号、按规则 10 去冗余且不删未验证块"全部属于未观测(量法见上表;
digest 那条另看交付后工作区 git status 是否干净)。同样未观测的还有 2026-09-29 追加的那半:
子代理还在 running 时,调度者会不会仍把"同一实体上的另一件事"steer 进去(量法见
preset/testing-guide.md I13 N10 / N11 —— 那半的机制是源码级事实,行为没有真机证据);
以及本轮刚加的截断接续(规则 7 接续半条):机制是实测的({kind:"max-tokens"} 的规范结局 +
tool-call 块被丢弃 + 客户端合成提示 + 本机上限 ≈32768),频次也已经量到(285 个会话里 max-tokens
6 条,全部落在被委派的子代理会话、且截断后记录数为 0 —— 见手段表那一行),但行为仍未观测:
"调度者会不会真的去接"、以及"接住之后产出是否完整"都没有真机证据(量法见
preset/testing-guide.md I13 N12 / I15 N13)。
改动前后都要量,否则无法判断一次改动是帮忙还是添乱(插件侧的改动用同一套口径看):
node D:\dsh\.dsh-token-audit\audit-run.mjs "C:\Users\cenqian\.dsh\sessions"它会把报告写到同目录的 audit-report.txt(覆盖上一次)。重点看 === sessions by preset === 里
adg 那一行的 input / cache / output / requests,以及每个子代理的 toolChars。
为什么有
audit-run.mjs这个副本:原始的audit.js在 ESM 作用域里用了require,直接跑会报错;.mjs那份是改好的可执行版本。
测完对比时注意:上面的基线是改动之前的 32 个会话。改动生效后要重新跑一次,用同一口径
(同样按 preset 分组的 input + output + cache)对比,不要拿单次会话的绝对值下结论。
preset 的改动不会立即生效 —— 必须重启 dsh(见「装完必须重启 dsh」)。
billion-context(下称 bili)是另一个 bundle:一个本机代理 +
DSH 插件,把会话上下文折叠进 pack,并往全局工具层注册 compress / decompress / search_context /
acp_status / acp_cache。它和本 preset 的交界有两处:工具可见性(硬的,见下)与自动压缩的归属
(bili 自己的 dsh.bundle.patch.yml 就带 - id: compaction-basic / config: {auto: false})。两处都由
同一份探测、同一个旗标(--with-billion-context)一起决定,所以口径只有一条:源文件中立、生成物按探测决定。
第一处交界,工具可见性:
toolFilter.allow是真白名单(见「设计要点」),专家只看得见 allow 里列出的名字;- bili 注入给模型的压缩指令与 nudge 只看自己的 config,不看这个请求有没有那些工具。
两头凑起来的后果是:装了 bili 又不给专家那几个名字,专家会收到"去调 compress / acp_status"的指令,
工具目录里却没有它们(实测:调度者自己看得见,因为它是全局层;被裁的是专家)。反过来,把这些名字
手写进 preset/agent.cordis.yml,没装 bili 的人每一次委派都会当场抛 names unknown global tool "compress"
—— 名字不存在时 restrict() 直接抛,那一次委派就废了。
所以口径是源文件中立、生成物按探测决定:
node tools/gen-preset-bundle.mjs --with-billion-context # 四个名字进 9 个专家行的 allow + compaction-basic 的 auto: false(只改生成物)
node tools/has-billion-context.mjs ~/.dsh/profiles web # 探测:每 profile 一行 "<name>\t<0|1>"
sh install.sh # 自动探测并决定旗标install.ps1 / install.sh 会先探测每个目标 profile(判据 = bili 在 dsh.profile.bundles 里 且 装上的那份
真的带 dsh.bundle.patch.yml),然后逐个 profile 决定它拿哪种味道:生成物分两份、各有自己的稳定目录 ——
plain 在 $DSH_HOME/bundles/dsh-adg-preset,注入版在 $DSH_HOME/bundles/dsh-adg-preset-bili(两份的
package.json 逐字节相同、包名都是 dsh-adg-preset,所以 dsh.profile.bundles 那一行两种味道通用),
每个 profile 的 node_modules/dsh-adg-preset 只 link: 自己该拿的那一份。挂 bili 的拿注入版、没挂的拿
plain,两边都不会被砸;探测本身没跑成时脚本直接报错,不会猜。--billion-context=on|off
(PowerShell:-BillionContext on|off)是整体覆盖,覆盖结果与探测不一致时会多打一行黄字警告;探测段在
install.sh / install.ps1 的第 0 节,两份脚本共用同一份判据。装完脚本还用
tools/check-bundle-flavor.mjs 断言那个 profile 实际链接到的那一份的味道(判据不能是"包在不在"——
两种味道的 package.json 逐字节相同)。
手动跑 gen-preset-bundle.mjs 默认不注入,忘带旗标 = 少个能力,不会装坏。
本机(2026-09-28 起由"两种味道"接管):
web应该指向注入版、desktop指向 plain。 本机web挂着 bili、desktop没挂,所以web的node_modules/dsh-adg-preset该链接$DSH_HOME/bundles/dsh-adg-preset-bili,desktop的该链接$DSH_HOME/bundles/dsh-adg-preset—— 重跑install.*就会按探测结果各给一份。 这正是旧结构做不到的事:以前只有一份共享生成物,为了不让desktop每次委派撞names unknown global tool "compress",只能把web那份改成实体目录绕过"共享稳定目录 + 链接"模型, 代价是重跑install.*会把它打平回 plain、而web的专家就再也调不动compress(2026-09-28 用户报告的 就是这件事)。现在那个特例不需要了:web拿注入版、desktop拿 plain,两者都是链接。 复核:node tools/has-billion-context.mjs "$DSH_HOME/profiles" web desktop(应web1 /desktop0)+ 看web的链接目标 +node tools/check-bundle-flavor.mjs "$DSH_HOME/bundles/dsh-adg-preset-bili/cordis.patch.yml" bili。 想彻底避开混装:给desktop也装上 bili(两个 profile 同味道,但仍然各拿各的那一份)。
注入的是四个名字,不含 acp_cache:那份工具只读缓存经济学账本,是调度者诊断"这轮折叠值不值"用的,
并且它能用 conversation_id 代读子代理的账;给每个专家只会加长它们每次请求的稳定 prefix。
第二处交界:挂着 bili 的 profile 要关掉 preset 里的自动压缩。 bili 自带
dsh.bundle.patch.yml(- insert: bili-native 之后就是 - id: compaction-basic / config: {auto: false}),
意思是"有 bili 在折叠上下文时,dsh 自带的自动压缩要让位"。但那一行打在 profile 层,而本 preset 的
compaction-basic / command-compact / tool-result-pruner 三行活在
isolate: {compaction: true} 的 realm 里、是另一份实例 —— 跨 lane 的 id 命中与否从未被观测,
所以不能指望它。生成物于是把同键同值的 config: {auto: false} 直接写进 preset 自己的 compaction 组:
两边都生效也无行为差异(幂等),而只注入名字、不关自动压缩的后果是两套压缩各自折叠同一段历史、互相抢阈值。
语义要说准:auto: false 是「关掉自动压缩与溢出恢复,手动 /compact 仍可用」(该插件的 auto 行:
"set false for manual-only operation";lib/index.js:827 用 if (this.config.auto) 决定要不要注册那两个
listener),不是把这一行 disabled 掉。plain 味道里绝不能出现这个键 —— 没挂 bili 的 profile 里,
dsh 自带的自动压缩是唯一的压缩手段,关掉等于让上下文无限增长。
同一份判据的另一半:挂着 bili 的 profile 不启用 dsh-adg-token-budget。 那个插件按步数档位给子代理下
收敛提醒,bili 的压缩/nudge 是同类指令,两套同时给同一批子代理会互相抢阈值、模型会收到"既该收敛又该折叠"
的矛盾信号。实现方式是把这个 bundle 从该 profile 的 dsh.profile.bundles 里移除(脚本会备份成
package.json.bak-adg-token-budget),不是塞一条 enabled: false 覆盖行 —— 按 id 覆盖是整块替换 config,
为关一个键重写整份 config 太容易丢别的键。注意这条按 profile 的实际挂载状态决定,不受 --billion-context
旗标影响(旗标只管工具注入)。想两套并存:把 dsh-adg-token-budget 加回那个 profile 的 dsh.profile.bundles 即可。
自检:先看这个 profile 的 node_modules/dsh-adg-preset 链接的是哪一份,再断言那一份 ——
node tools/check-bundle-flavor.mjs $DSH_HOME/bundles/dsh-adg-preset-bili/cordis.patch.yml bili(挂 bili 的 profile)
或 node tools/check-bundle-flavor.mjs $DSH_HOME/bundles/dsh-adg-preset/cordis.patch.yml plain(没挂的)——
它同时断言两件事:9 个 agent-* 行的 toolFilter.allow 末尾有没有那四个名字,以及 compaction-basic 行的
config.auto 是不是恰好 false(plain 味道则断言这两个都不存在)。再到新会话里委派任一专家,让它报"工具目录里
有没有 acp_status",并确认它没有被 dsh 自带的自动压缩插过手(手动 /compact 仍应可用)。
两个方向都要测的完整口径见 docs/evidence.md §17「billion-context 的上下文工具对 ADG 专家可见吗」,
按 profile 分味道的落点、这次真机缺陷与修法则见 §18。
上一节那三组旋钮(现已回归出厂默认)压的是单条结果的体积,它们管不到步数。
这一层补的正是步数:一个 host-plane 的 cordis 插件挂在 agent/pre-step 瀑布上,
在子代理进入命中阶梯的某一步时,替调度者注入一条可选的收敛提醒。
preset 侧撤销体积闸门之后,成本纪律就集中在这一层。
本轮改动把另一半整个移除了。 插件原先还有一条累计 token 预算:软档
(budgetTokens × softRatio,默认 210 万)注入收尾指令、硬档(默认 300 万)调
agent.cancel({ kind: 'parent' }) 并返回 { kind: 'reject' }。这两档、以及配置它们的
budgetTokens / softRatio / cacheReadWeight / softNudge / hardDryRun 五个键已全部移除:
插件现在从不调用 agent.cancel、从不 reject 一步、从不读 ctx.get('sessionProjections'),
没有任何破坏性档位。
**为什么移除(设计理由):**输出型任务(写文档、生成报告、消化长语料)本身就需要那么多 token, 按累计 token 阈值介入只会截断产出,省不下有意义的东西;累计量大本身不是"跑飞了"的证据。 步数才是真正会出问题的信号(子代理反复探索不收敛),而且它的提醒是"二选一、可以直接无视", 不是停止指令。所以 preset 的成本纪律现在只剩"步数收敛检查点"这一层。
包名与行 id 是历史名称。
dsh-adg-token-budget/adg-token-budget里已经没有 token 预算了:一行阈值都不再比较。名字与行 id 保留下来,是为了让挂载行 id 和热重载身份 都不变 —— 改名会让已经装好的机器需要重新部署、重新挂行。部署路径则已经变过:现在装的是${DSH_HOME}/bundles/dsh-adg-token-budget(2026-09-28 起,与dsh-adg-preset同一根),${DSH_HOME}/plugins/dsh-adg-token-budget是旧落点、已删除,见 为什么装在$DSH_HOME/bundles。 按名字找"预算"的读者请以上面这段为准。
先说状态,分三段说:
① 步数检查点在真机上真的注入过。 插件当时部署在
${DSH_HOME}/plugins/dsh-adg-token-budget、手贴挂在${DSH_HOME}/profiles/web/cordis.patch.yml上,被运行中的 dsh 加载 (这两处是 2026-09-25 那次实测的历史落点;2026-09-28 起它装成 bundle:落点${DSH_HOME}/bundles/dsh-adg-token-budget/、挂载行来自 bundle 层,迁移后重新实测过,见 ①b)。 2026-09-25 01:37 重启后确认新代码真的加载、01:38:47 武装;此后观测到三次真实检查点注入 (插件日志行 + 子代理转写里的那条消息,毫秒级对齐):两次用的是旧措辞 (fdd55c65/12bf2213,第 12 步、当时只花了 10–12 万 token),一次用的是新的选择式措辞 (41c07ec8,第 12 步、14.3 万 token)。子代理随后的第一条消息分别是: "I'm at step 12. I should converge."、"I have enough evidence. Let me 收敛 and report."、 以及 "Remaining必需工作: … Let me do 2 more fetches … then converge. I'll say I'm continuing briefly." —— 收到命令式措辞的两个都表示要收敛;收到选择式措辞的那个选了"继续"并说明理由, 没有偷偷缩减计划。这正是新措辞两个分支的设计行为(n=1/种,是例证不是效果测量)。settled: released session state …也在 17:55:33 第一次观测到(那个子代理收敛后 29 秒释放状态)。同日阶梯从
[12, 24, 40]换成[4, 8, …, 280]。 依据是重跑审计后的真实分布(中位数 39、 p10 只有 6:按均值放检查点等于不问),而不是"多数子代理不需要那么多步"这个直觉本身 —— 直觉解释了动机,分布决定了数字。代价与风险都写在 步数收敛检查点 一节里:覆盖从 30/37 提到 34/37、注入从 71 条增到 214 条 (约占总账单 0.25%),换来"首次检查点之后还剩 92.6% 的步数"这个可干预面。 这次是代码改动,做法是:先重启、确认激活行出现stepText=,再改stepTiers—— 18:25:14 的激活行就是这么来的,没有第二次重启。 仍然未观测:第一个 tier 之外的任何一档、新阶梯下的任何一次注入、以及"提醒是否让子代理更快收敛"。①b 迁成 bundle 之后重新实测过(真机实测,2026-09-28,UTC;证据
$DSH_HOME/adg-token-budget.log, 逐字行见docs/evidence.md§16.4)。手贴行已删、bundle 已装,日志写下2026-09-28T01:47:48.467Z activation: active … presets=[adg] … stepTiers=[4, 8, …, 280] dryRun=false logFile='C:\Users\cenqian\.dsh\adg-token-budget.log'⇒ bundle 层那一行确实挂载并apply了,logFile的!!js dshHomePath('adg-token-budget.log')也求值成功(落回同一个文件,历史日志连续)。 随后01:59:25/01:59:31两行step stage: nudged tier=1/2 step=1/tier=2/2 step=2与01:59:43的settled: released session state⇒ 迁移后的行真的在计数并注入 —— 但那一次走的是临时覆盖行presets: ['cordis']+stepTiers: [1, 2]的通用委派(通用subagent/subagent_fork委派出去的子代理 session 头记agentPreset: "cordis"、delegationDepth: 1,presets: ['adg']按设计不治理它), 不是出厂那 14 档。迁移后的未观测(量法见docs/evidence.md§16.5):presets: ['adg']对 真实 Adg 专家子代理的注入、冷启动后的 bundle 层(上面那次生效靠的是改写 profile 清单带来的整份 patch 栈重读,不是bundles/被 watch)、desktopprofile 生效(它的patchReload不是live)。② 已移除的 token 两档:当年实测(历史证据,不代表当前行为)。 快照时
logFile里已经有 5 个真实adg子代理走过旧代码的 token 判定(5 个全部 ≥ 300 万,最大 48,992,135),would cancel437 行、would nudge36 行,而soft stage: nudged与settled:都是 0 行。 这在当时是"默认 300 万落在正常分布内部、不能按它武装硬档"的直接证据,也是这次移除这两档的 理由之一;今天读它不能当成插件的当前行为。完整数字见 已移除的 token 两档。③ 步数与"请求数"是同一个单位(阶梯校准的前提):审计对
f7ee3039-…数出 329 个模型请求, 插件对同一个 id 数出约 297 步(当年那个口径是"超预算的步数",逻辑与今天的步数计数一致)—— 两个基于不同事件的独立计数互相吻合。
调度者不该自己轮询子代理:审计里调度者会话占 adg 账单的 59%,而它每轮一次 就要把本会话最大的那段上下文重发一遍 —— 轮询比提醒省下的还贵。而"谁提醒、提醒什么、 第几步提醒"全部归这个插件:调度者 persona 里原先那段配套政策(委派 prompt 写明大致 收敛规模、不要自己轮询步数)已删除,理由见 persona 层保留的政策。提醒以调度者的名义、 按下面的阶梯注入:
| 观测 | 值 |
|---|---|
| 每个子代理的模型请求数(= 步数) | min 1 / p10 6 / p25 14 / 中位数 39 / 平均 51.7 / p75 61 / p90 103 / max 329(37 个子代理) |
| 默认检查点阶梯 | 4 / 8 / 12 / 18 / 24 / 32 / 42 / 55 / 72 / 95 / 125 / 165 / 215 / 280 步(各一次):前 24 步里每 4–6 步一次,之后按约 ×1.3 拉开 |
| 覆盖面 | 37 个子代理里 34 个至少收到一次;中位数那个收到 6 次;329 步那个收到全部 14 次 |
| 提醒自己的成本 | 全部 214 条消息合计约 0.5M token 等量(每条约 180 字符,之后每一步跟着重发一次),对比同一批子代理花的约 205M ≈ 0.25% |
| 每一步注入几条消息 | 最多 1 条:插件一步只评一个触发;即使多个 tier 同时到期,也只发最靠前的那个,下一个到期的 tier 会在下一步补上 |
为什么锚点是分布、不是均值:原来的 [12, 24, 40] 是照"平均 23.4 步"放的,而重跑审计
(audit-steps.mjs,同一份日志、另写一个报告文件)之后,37 个子代理的中位数是 39、p10 只有 6、
四分之一在 14 步内结束。按均值放等于对大量本来早就该问一句的委派一句话都不问。所以阶梯提前、
加密;作为对比:
| 阶梯 | 覆盖到 | 注入条数 | 首次检查点之后的步数占比 | 最后一个 tier 之后的步数 |
|---|---|---|---|---|
[12, 24, 40] |
30/37 | 71 | 79.5% | 872 |
[4, 8, …, 280] |
34/37 | 214 | 92.6% | 49 |
注入的文本是一条可选提醒(署调度者的名),形如:
【收敛检查点 1/14】调度代理提醒:这是你的第 4 步。
这是一条**可选**提醒,不是停止指令。请你自己判断,二选一:
- **如果现有产出已经能回答委派目标,就收尾汇报。**
- **继续**:如果确实还有必须做完的工作,就继续做,**直接无视这条提醒**,不要为了回应它而缩减或改写计划。
选哪个由任务本身决定,不是由这条提醒决定。请在下一条消息开头用一句话说明你的选择,然后按你的选择继续。
为什么必须写成"可选":阶梯提前、加密之后,检查点会落到大量本来就不算长的委派上(中位数
那个会收到 6 条)。如果它是一条"立刻停止探索"的指令,密集的早期检查点就会把正常的长任务也压扁 ——
那是拿结果换 token,是这个功能唯一不被允许做的交易。所以三道性质写死在文本里、由测试钉住:
① 明说可选、可以无视;② 两个分支对称列出,"选哪个由任务本身决定";③ 正文不规定汇报什么 ——
"交付了什么、哪些还没验证"由子代理自己决定(它是唯一知道的人),测试对此下的是反向断言。
另外还有一条反向断言:文本里不许出现"立即停止"这类命令句。
变异 M13 专门把那句"可以无视"反过来写("不要无视"),被测试抓住 —— 它正是"命令披着选择的外衣"
这个失效模式。只有最后一个 tier 多一句(提醒到此为止;若继续请说明预计还要多少步、完成标准
是什么),且各档正文完全一致、不递进(密阶梯 + 逐次加压=强制机器,测试会比较各档正文是否相同)。
计数只算真正进入的步(下游 reject 掉的那一步不算),每个子代理独立计数、subagent/end 时释放,
恢复后进入新的驻留期可以从头再提醒。
措辞是可配置的:stepText 覆盖内置正文(整段替换),而 config: 是热重载的 ——
所以以后调措辞不需要新包、也不需要重启 dsh。激活行会写 stepText=builtin 或 stepText=custom,
敲错键名不会静默。
| 档 | 触发 | 动作 |
|---|---|---|
| 步数检查点 | 子代理进入第 stepTiers 里的某一步(默认 4 / 8 / 12 / … / 280) |
这一步照常放行(先调 next()),然后追加一条 【收敛检查点 n/N】 消息:可选提醒,让子代理自己选"收敛汇报"还是"继续做完必需的工作",并说明选择。每个 tier 在每个驻留期最多一次 |
| — | 已移除:不再有累计 token 阈值、不再注入收尾指令、不再 agent.cancel / reject。历史口径见 已移除的 token 两档 |
触发是闭区间(正好等于某个 tier 就触发)。每条提醒每个子代理会话的每个"驻留期"(residency
epoch)每个 tier 最多一次:标记在 subagent/end 时释放,而子代理层每个驻留期发一次这个事件,
所以一个可续跑的子代理被恢复后会进入新的驻留期、可以被再次提醒。这是有意的
(恢复后的子代理有新的计划、也有新的跑飞机会),代价只是"同一段驻留期内不重复提醒"这条保证。
同一步最多注入一条消息:插件一步只评一个触发,即使多个 tier 同时到期也只发最靠前的那个,
下一个到期的 tier 在下一步补上 —— 这正是 dueStepTier 逐个返回的设计。
dryRun: true 时只算不做:不注入消息,到期的检查点只写一行日志
(dry-run step stage: would nudge …),不消耗任何 tier 标记,而且步数照常计数 ——
所以校准完再把 dryRun 关掉,第一个检查点仍然会提醒,而"第几步会开始提醒"在校准期也看得见。
代价是会为计数建会话状态({ steps, firedTiers });要"连计数都不要"就用 stepNudge: false。
这是"先按自己的流量校准、再武装"的那把开关(见
推荐的上线顺序)。
插件不再计算任何累计用量:没有阈值、没有权重、也不读 provider 报的账单字段。
它数的是 agent/pre-step 被真正进入了几次 —— 这是它自己看见的事实,不依赖任何服务。
(历史口径:累计值是 未缓存输入 + 输出 + cacheRead × cacheReadWeight + cacheWrite,
读 ctx.get('sessionProjections') 的 stateOf(session, 'tokenUsage') —— 这套已随两档一起移除。)
只对被委派的子代理且 preset 命中的会话生效,条件按顺序判:
enabled为真;delegationDepthOf(agent) > 0—— 取session.header.delegationDepth与运行时subagentDepth的较大值, 但两侧都先过Number.isSafeInteger(value) && value >= 0:'1'(字符串)算深度 0、 不是 1,因为'1' > 0在 JS 里为真,一个被 YAML/JSON 往返成字符串的数字若被强转, 就会让一个写错的 header 被当成子代理(旧代码里那会真的砍掉一个子代理);1.5不截断、-1不夹到 0、2 ** 53越界也不认(裸Number.isInteger会放它过去),NaN/Infinity/true/null一律算"没有深度"。 (header 是权威且单调的:被恢复的子代理带着全新的 options 回来,只看subagentDepth会让它当成顶层会话)。 顶层会话(深度 0)永远不动,尤其不会动调度智能体;session.header.agentPreset命中presets。header 里没有agentPreset的子代理也不动 —— 这里故意 fail-open:猜错会给它本来没被指向的会话注入提醒;stepNudge为真 —— 为假时整层关掉:不计数、不建状态、一步原样放行(见配置表)。
步数检查点不依赖任何投影或服务,所以拿不到 token 数据的会话里它照常提醒 ——
这也正是它有独立开关 stepNudge 的原因。
这一整节是历史。 下面的数字全部来自已移除的 token 软/硬两档, 它们证明的是"当年那条路为什么走不通",不代表当前插件的任何行为。当前插件只做步数检查点。
对着 token 成本纪律 那套审计口径看(同一份 audit-report.txt 快照,
32 个 Adg 会话 / 983 次请求):
| 观测量 | 实测值 |
|---|---|
| 每个子代理会话平均 | ≈1.73M(avg tokens per subagent adg session: total(input+output+cache)=1734486) |
| 22 个子代理会话合计 | 38.2M(subInput 2,729,782 + subOut 351,534 + subCache 35,077,376) |
| 快照里最贵的单个子代理会话 | 5.22M(in 228,433 + out 42,094 + cache 4,947,456 = 5,217,983) |
| 当年实测到的最重一次「实现型委派」 | 7.65M(usage=7651807,就是当年那次真机硬停;同一个子代理越过了 300 万,被 agent.cancel 停掉) |
| 快照里 ≥ 300 万 的子代理会话 | 22 个里的 5 个 |
| 最贵的调度者会话(对照) | 38.4M(3,686,879 + 364,731 + 34,336,896) |
那 5 个越过当年默认预算的子代理会话,按总量降序是: 5,217,983 · 4,455,097 · 4,070,916 · 3,904,800 · 3,236,397;第 6 名是 2,204,189。 也就是说边界不在"差一点点"的地方 —— 300 万这条线正好落在这批分布的中段。
所以当年的口径是:300 万不是"谁都不会被砍",它落在分布内部,一定会砍掉正常工作量。 平均每个子代理 1.73M、22 个里已经有 5 个(约四分之一)超过 300 万、当年最重的一次是预算的 2.5 倍。 它防的是尾部(一次跑飞能顶掉一整天的额度),但"正常委派"照样会被波及 —— 这正是这次把两档整体移除的直接动机。
两个让这份分布偏悲观的原因,读数字时要一起记住:
- preset 侧的体积旋钮当时也已经撤销(见 三组体积旋钮), 所以上面这份语料量到的正是"不压低体积"的流量 —— 它对当下这个版本是同口径的, 只是当时插件还没有武装,所以步数那一侧还没被干预。
- 语料是活的、还在长。 这次改文档时重读了一遍同一份会话目录,已经是 32 个子代理会话、 最大 17,022,627、平均 3,942,185 —— 上面每个数字都是下界。
真机 dry-run 快照(2026-09-25T01:1x+08:00,旧代码的 token 判定,比审计语料更狠):logFile 里
已经有 5 个真实 adg 子代理走过旧判定,5 个全部越过了 300 万,其中 2 个 ≥ 600 万,
最大一个 48,992,135(≈预算的 16 倍),would cancel 共 437 行、would nudge 共 36 行,
soft stage: nudged 与 settled: 都是 0 行。逐个子代理的峰值:
| 子代理 | would cancel 行数 |
第一次越过 300 万 | 峰值累计用量 |
|---|---|---|---|
adg/f7ee3039-… |
297 | 3,121,660 | 48,992,135 |
adg/973aca1b-… |
92 | 3,065,764 | 16,607,867 |
adg/7cfa95ae-… |
27 | 3,018,602 | 6,745,603 |
adg/2ec8cfe5-… |
18 | 3,014,252 | 6,230,057 |
adg/5bee9ec3-… |
3 | 3,083,933 | 3,361,608 |
两个可以直接读出来的结论:① 300 万落在真实流量的主体里(5/5 都越过去了),
所以按它武装硬档一定会截断正常委派 —— 这条路因此被放弃;② 步数才是那个真正的杠杆 ——
最大的那个子代理贡献了 297 行 would cancel,也就是它在 300 万之上又走了约 300 步
(而重跑的 37 个子代理里,中位数是 39 步、最大 329 步)。给它 14 次收敛检查点相对于 4899 万 token
是零成本(全部 214 条提醒合起来约占总账单 0.25%),这正是现在这一层要做的事。
dryRun: true 的来历也在这段历史里。 当年打开它(enabled: true + dryRun: true)之后,
插件把旧代码的每一次判定按原样写进 logFile,但不注入、不 cancel,于是这个文件就变成
"按我自己的流量,当年那个预算会砍掉多少、第几步会开始提醒"的实测 —— 看清楚了才有上面那两个结论。
今天 dryRun 仍然是同一把开关,只是它校准的对象只剩步数检查点。
(口径注:报告里那张 top 15 表按 in+out 排序、只列出 13 个子代理会话,
所以「最贵 5.22M」与「5 个越过 300 万」都是这次重读全部 22 个子代理会话算出来的,不是从那张表读出来的;
另外 cacheWriteTokens 在全部 1183 个 usage 对象里都不存在,当年累计口径里的 cache-write 恒为 0 ——
这是「provider 没上报」,不是「没有 cache 写入」,真实账单只会更高。这一节里的 soft / hard / would
行都只可能出现在旧代码的 logFile 里。)
| 键 | 默认值 | 含义 |
|---|---|---|
enabled |
true(本仓库装进去的那一行初始是 false;本机实跑的那一行现在是 true) |
总开关。false 时 apply 在注册任何监听器之前返回:不注册监听器、不写决策日志(但仍写一行加载期激活行,见下) |
presets |
['adg'] |
管哪些 preset 的子代理。裸字符串 presets: adg(YAML 标量的读法)等于单元素列表 |
stepNudge |
true |
步数收敛检查点的总开关。false = 整个功能关掉:连步数都不计,不建任何会话状态,一步原样放行(等于插件不参与)。要"只校准、不注入"请用 dryRun,不要用这个键 |
stepTiers |
[4, 8, 12, 18, 24, 32, 42, 55, 72, 95, 125, 165, 215, 280] |
在第几步注入检查点。裸数字(stepTiers: 12)等于单元素列表;非法项丢弃、去重、升序、上限 16 个;清洗后为空则回落到默认。取值依据见 步数收敛检查点:提前、加密,因为真实分布的中位数(39)远低于均值,而 p10 只有 6 |
stepText |
null |
可选:自定义检查点正文,整段替换内置正文(含内置只在最后一档追加的那句)。空值/非字符串回落到内置正文(不会让提醒变成空话);超过 4000 字符截断。存在的理由:调措辞是热重载的 config: 改动,不需要新包、也不需要重启 dsh。激活行写 stepText=builtin / stepText=custom,敲错键名不会静默 |
dryRun |
false |
校准开关。到期的检查点只记一行日志、不注入消息,也不消耗 tier 标记(所以之后武装仍然会送达那一次);步数照常计数(否则校准不出"第几步会提醒"),因此会为计数建会话状态 |
logFile |
null |
绝对路径;设了就追加加载期激活行 + 每个决策事件一行。没有检查点到期的普通一步什么都不写(所以文件不会膨胀)。相对路径会被关掉文件日志并告警 |
已移除的五个键:budgetTokens / softRatio / cacheReadWeight / softNudge / hardDryRun。
normalizeConfig 对不认识的键一律忽略,所以一条还带着这五个键的旧组合行仍然能加载,
只是它们不再有任何作用 —— 插件读都不读。本机那一行当时还留着这五个键(历史:
2026-09-28 迁成 bundle 时手贴行已删,bundle 行只有 6 个键、没有这些旧键),取的是刻意的惰性值
(budgetTokens: 1000000000000000、softRatio: 1、hardDryRun: true):只是为了在重启前那段窗口
里挡住还在内存中的旧代码(缺了 hardDryRun 它会把硬档真武装),重启加载新代码后可以整段删掉 ——
见怎么确认它已经武装里"切换窗口里曾经留着的 5 个惰性旧键"那一段。
这也是"插件从不导出 Config schema"的另一个好处:旧键既不会让 profile 加载失败,也不会静默改变行为。
logFile 里会出现的行只有这几类:activation: active|inactive …(每次 apply 一行,包括 enabled: false)、
注册提示(registration skipped: … / warning: N registration(s) are already active …)、
步数检查点(step stage: nudged tier=n/N step=S label=… /
step stage (no nudge injected: decision kind=…) … / step stage (no nudge injected: nudge construction failed) …)、
dry-run(dry-run step stage: would nudge … / dry-run step stage: would not nudge (decision kind=…) …)、
以及 settled: released session state label=…。
旧代码留下的 soft stage: … / hard stage: cancel … / dry-run hard stage: would cancel … /
no budget data: … 这几类不会再出现。
第一条决策行还会以 dsh-adg-token-budget: first decision: … 的形式同时进一次宿主日志。
插件不导出 cordis Config schema:它自己手写归一化,所以打错的值回落到默认、不认识的键被忽略,
而不是让 profile 加载失败。
默认值也在 plugin/dsh-adg-token-budget/examples/cordis.patch.yml 里以注释形式列了一遍 ——
那份例子现在的角色是键参考 + 手工覆盖模板,不再是"可以直接贴的挂载行"(挂载行本体是包自带的
cordis.patch.yml,出厂即武装;贴覆盖行必须把要留的键全部重写,见
挂载行住在哪一层)。
挂载行本体现在住在 bundle 层 —— 包自带的
${DSH_HOME:-~/.dsh}/bundles/dsh-adg-token-budget/cordis.patch.yml(仓库里对应
plugin/dsh-adg-token-budget/cordis.patch.yml,由 package.json 的 dsh.bundle.patch 声明)。
安装脚本不再往 profile 的 patch 层写这一行:${DSH_HOME:-~/.dsh}/profiles/web/cordis.patch.yml
现在只放覆盖行,"选进来"这个动作写在 profiles/web/package.json 的 dsh.profile.bundles 里。理由:
- 机器级那一层(
dsh-windows-notifier就住在那里)套在本机每一个 profile 上:web、headless、sdk、 自建 profile 都会去 import 这个包,而且它还会挡住 Plugins 页保存(这条没变)。要全机生效就把包选进 各个 profile 的dsh.profile.bundles—— 一行本来就是包自带的,不要把它搬进机器级。 - 只选进
web的爆炸半径就是你现在正在用、随时能重启的那一个 profile —— 这条理由也没变,变的只是 它现在写在 profile 清单里、不再写在 patch 文件里。 - profile 层是热重载层、bundle 层不是:
web是patchReload: live,所以在 profile 层写config:覆盖行改完立即生效、不用重启 —— 这正是"先enabled: false确认、再改true"这种两步操作还能成立的前提 (机器级那层同样热重载,但它的爆炸半径是全部 profile)。而改 bundle 层那一行本体要以重启 dsh 为准: 没有任何东西 watchbundles/,单独编辑它不会自己触发重读(禁止宣称"不重启也会生效")。 两条相关的坑:bundle 行出厂即武装(enabled: true+dryRun: false),想"先挂后开"必须写一条enabled: false的覆盖行;而那条覆盖行是整块替换config、不是深合并,要留的键必须全部重写。 - 旧机制那条手贴行留在 profile 层,是这条链路上唯一的静默失效模式(真机实测,
docs/evidence.md§16.4 第 6 条):它不多挂一行,但整块接管那一行的config:(实测把dryRun顶成true⇒ 注入当场停掉,而fiberPhase仍是active、日志看起来完全正常),并让该行脱离管理(list_plugins的patchId消失、readOnlyReason变成"unaddressable"⇒ Plugins 页与set_plugin都点不动它)。list_bundles的overrides发现不了它 —— 判据就是list_plugins里这一条还有没有patchId。 所以install.*只报告、不代删,这一刀由人来下。
部署目标是 ${DSH_HOME:-~/.dsh}/bundles/dsh-adg-token-budget/ —— 2026-09-28 起与 dsh-adg-preset
同一个根(旧落点 ${DSH_HOME:-~/.dsh}/plugins/dsh-adg-token-budget/ 已删除),再由目标 profile 用
pnpm add link:<那个目录> 把它链进 profiles/<profile>/node_modules/,并把包名写进该 profile 的
dsh.profile.bundles(光有依赖不算选中)。理由都是本版 dsh 的实测口径:
profiles/node_modules/这个"所有 profile 共享的解析根"在本版 dsh 里已经不被采纳。 2026-09-28 实测:插件照旧拷在${DSH_HOME}/plugins/dsh-adg-token-budget/时, 挂载行解析不到这个包;改放到 profile 自己的node_modules(或link:过去)才起得来。 本机当时就是这个形状:profiles/web/node_modules/dsh-adg-token-budget是指向$DSH_HOME/plugins/…的链接(现在这条链接指向$DSH_HOME/bundles/dsh-adg-token-budget—— 落点迁移见docs/evidence.md§16.1),而profiles/web/node_modules/@deepseek-ai/*一个目录都没有(当时实测 0 个),全部@deepseek-ai/*行照样 active —— 因为包名解析是两段锚定: 先从 dsh 安装目录解析,再落到当前 profile(@deepseek-ai/dsh-app-boot/lib/index.js:477-481)。- 部署出来的插件要独立于仓库。 稳定目录在
$DSH_HOME下,link:指的是它、不是仓库, 所以删掉、挪走、重命名这个仓库都不会让 dsh 启动失败(install.*每次都先把插件拷到稳定目录再链)。 link:也不会被 dsh 启动时的那次 fallback 修复清掉(源码级事实:@deepseek-ai/dsh-app-boot/lib/index.js:596-598明文写"只有目标落在<profile>/.dsh-module-fallback/node_modules里的链接会被删;pnpm 装的包与其它所有符号链接都保留", 且没有这个目录的 profile 根本不动)。所以重启之后 bundle 与插件仍然在 profile 的node_modules里。
install.ps1 / install.sh 每次安装先删稳定目录再拷,所以重复执行是干净覆盖,
不会留下上一个版本的残留文件。已知限制(本机实测):dsh 正在运行时 pnpm 会失败 ——
它发现 node_modules 不是自己管的(.modules.yaml 缺失)就想整目录重建,而文件被运行中的 dsh
占着(os error 32 / ERR_PNPM_PACKAGE_MANAGER_REMOVE_MODULES_DIR);脚本会把这一条如实报告
并继续(包已在位就不算失败),要真正装/换依赖请先关掉 dsh。
bundle 层那一行出厂即武装(enabled: true、dryRun: false、presets: ['adg']、默认 14 档阶梯),
"把包选进 dsh.profile.bundles"就是那个刻意动作,install.* 只做这一步。要按 profile 开 / 关 / 调,
就在 profile 层写一条覆盖行(它整块替换 config,要留的键必须全部重写):
# ${DSH_HOME:-~/.dsh}/profiles/web/cordis.patch.yml —— profile 层的覆盖行,不是挂载行本体
- id: adg-token-budget # ← 命中 bundle 层那一行的 id:这份 config 会整块替换它
config:
enabled: true # ← 只改这一行
dryRun: true # ← 建议先只开到这一步:只记录、不注入改完立即生效、不用重启(这个文件是 patchReload: live):宿主会重新 apply 这一行并写下一行
activation: active …。改 bundle 层那一行本体(bundles/dsh-adg-token-budget/cordis.patch.yml)就不是
这样 —— 那个要重启 dsh(没有任何东西 watch bundles/,见
挂载行住在哪一层)。前提是包里 src/ 的代码没变过 —— 热重载不会重新 import 已经加载过的
模块(见 装完必须重启 dsh)。本机已经这样跑过(见下)。想小范围试:
把 stepTiers 调到 [1, 2],就会在子代理的头两步各出现一次检查点。
enabled: false—— 写在 profile 层的覆盖行里(要留的键全部重写):装上了,但apply在注册任何 监听器之前返回,一步都不参与。它照样写一行加载期激活行,所以"宿主确实加载过这个包"看得见。 bundle 行出厂不是这个值(出厂即enabled: true+dryRun: false),所以这一步是主动退回校准态, 不再是装完的默认状态。enabled: true+dryRun: true—— 拿你自己的流量校准:到期的检查点只写一行日志, 不注入任何东西,也不消耗 tier。看清楚了再往下走。enabled: true+dryRun: false—— 提醒真的注入。这就是本机现在的状态 (stepNudge: true+ 默认 14 档阶梯)。
没有第四步。 已经没有"只武装一半"的开关了(hardDryRun 随硬档一起移除),也没有可武装的
破坏性动作 —— 唯一的动作是注入一条可选提醒。反过来,不要在"代码还是旧版"的状态下把
dryRun 关掉:旧代码会把那条已移除的硬档真武装(见 装完必须重启 dsh)。
三种,按「关得有多彻底」排:
enabled: false(写在 profile 层的覆盖行里,要留的键全部重写)—— 插件自己的开关,apply在 注册任何监听器之前返回:不注册监听器、不写决策日志,但照样写一行加载期激活行。推荐这一条 —— 它可撤回、看得见证据。注意 bundle 行出厂是enabled: true,安装脚本也不再往 patch 里写任何值, 所以这个false得你自己写。disabled: true(loader 层字段)—— 在 profile 层写一条只含- id: adg-token-budget+disabled: true(不带config:)的条目:连包都不 import。本机实测可用:list_plugins里include:adg-token-budget变成enabled: false/fiberPhase: null、条目总数仍是 190、日志不写任何行 (⇒ 这种关法不能靠"日志里没有新行"判活,判据是list_plugins);删掉这条覆盖行后日志立刻重新写出activation: active …(2026-09-28T02:19:52.105Z,docs/evidence.md§16.4 第 5 条)。 不要再"整行删掉":那一行现在在 bundle 层,删它等于改包 —— 想不 import 就用这条disabled, 或者把包名从dsh.profile.bundles里摘掉(第 3 条)。- 还原备份:
…/profiles/<profile>/package.json.bak-adg-token-budget(安装脚本改 profile 清单前会写这份, 摘回包名就是取消选中);迁移时删掉的那条手贴行另有备份…/profiles/<profile>/cordis.patch.yml.bak-adg-token-budget-bundle-migration。
部署出来的包本身不影响启动(没有 profile 选它就不会被 import);想清干净就删
${DSH_HOME:-~/.dsh}/bundles/dsh-adg-token-budget/(以及它在 profiles/<profile>/node_modules/ 下的链接),
或用 plugin_manager 的 remove_bundle(本机当次读数 removable: true)。
加载期有一行激活行,enabled: false 时也写。 这一行是"宿主确实加载过这个插件、用的是这份配置"的证据:
-
logFile=${DSH_HOME:-~/.dsh}/adg-token-budget.log—— 这一项现在写在 bundle 行里,值是!!js dshHomePath('adg-token-budget.log'),由 Loader 求值 ⇒ 任何机器都落到自己 dsh 用户根下的同一个 文件名(安装脚本不再往 patch 里写绝对路径)。本机实测的求值结果logFile='C:\Users\cenqian\.dsh\adg-token-budget.log'与迁移前手贴行写的值逐字相同 ⇒ 历史日志连续 (真机实测,docs/evidence.md§16.4)。插件自己的默认值是null,即完全不写文件。 每次apply先追加一行 ISO-8601 时间戳开头的激活行,形状是:activation: inactive (enabled: false) presets=[adg] stepNudge=true stepTiers=[4, 8, 12, 18, 24, 32, 42, 55, 72, 95, 125, 165, 215, 280] stepText=builtin dryRun=true logFile='C:\Users\cenqian\.dsh\adg-token-budget.log' activation: active createUserMessage=profile-fallback:web presets=[adg] stepNudge=true stepTiers=[4, 8, 12, 18, 24, 32, 42, 55, 72, 95, 125, 165, 215, 280] stepText=builtin dryRun=false logFile='C:\Users\cenqian\.dsh\adg-token-budget.log'(真正的行都在行首带 ISO-8601 时间戳,这里为了读清楚省掉了。)第 1 行是"宿主加载了、但开关是关的", 第 2 行是"已武装、
createUserMessage由 running profile 的 fallback 目录解析"(profile-fallback:web)。 这两行的阶梯就是当前的 14 档。 旧字段已经不在这一行里:budgetTokens=/softThreshold=/softRatio=/cacheReadWeight=/softNudge=/hardDryRun=都不再出现。所以这一行同时也是"运行中的宿主到底加载了哪一版代码"的判据:- 出现
stepNudge=+stepTiers=+stepText=,且没有hardDryRun=→ 当前这版代码; - 还在写
budgetTokens=/softThreshold=/hardDryRun=→ 旧代码。你部署了新代码却发现 激活行还是旧形状,就是"热重载重放了 config、但没有重新 import 模块",需要重启 dsh (见 装完必须重启 dsh)。 一行过期的激活行不是错误,它只说明那一刻加载的是旧代码;判断"现在跑的是哪一版"要以最后一行为准, 而且不能只看到一个熟悉的字段名就下结论。旧日志文件里还会留着当年的soft stage: …/hard stage: cancel …/dry-run hard stage: would cancel …行 —— 那是已移除功能的历史记录,不会再有新的。
- 出现
-
除激活行之外,
logFile只记决策事件:到期的步数检查点、dry-run 判定、settled: released session state …。没有检查点到期的普通一步什么都不写,所以enabled: true之后文件不会因为"步子多"而膨胀。注意 dry-run 下每个到期的检查点都会写一行 (dry-run step stage: would nudge …),所以那个行数是"到期过几次",不是"注入几条", 也不是"子代理个数"。 -
宿主日志里同一行带前缀
dsh-adg-token-budget:;第一条决策行还会额外以dsh-adg-token-budget: first decision: …进一次宿主日志。看到dsh-adg-token-budget: apply failed (…); the step checkpoints are inactive就是坏消息: 插件降级成 no-op(profile 照常启动,这正是「永不抛」的设计);同理context has no event API; the step checkpoints are inactive。 -
真实触发行为:步数检查点的真机注入已经观测到(三次,且拿到了子代理转写里的原文与它的回应);
dryRun: true期间的判定行也观测到过(dry-run step stage: would nudge …,且没有注入任何消息)。 未观测:第一个 tier 之外的档、新阶梯下的注入、以及"提醒是否让子代理更快收敛"。 见 现在的证据到哪为止。
当前这一行是武装状态(enabled: true、dryRun: false、stepNudge: true、
stepTiers: [4, 8, 12, 18, 24, 32, 42, 55, 72, 95, 125, 165, 215, 280]),
加载与注入都已是实测:18:18:22 的激活行出现 stepText=builtin(只有新代码会写这个字段),
18:25:14 的激活行把阶梯换成 14 档且没有重启(config 热重载)。所以接下来日志里每出现一行
step stage: nudged …,都是这条链路上的一次真实注入。
切换窗口里曾经留着 5 个惰性旧键(历史,不代表当前)。 手贴行当时还带着
budgetTokens: 1000000000000000、softRatio: 1、cacheReadWeight: 1、softNudge: true、
hardDryRun: true —— 这不是配置意图,是重启前那段窗口的安全网:进程里跑的还是旧代码,如果这里少了
hardDryRun,旧代码会按默认值 false 把已删除的硬档真武装;把 budgetTokens 抬到 10^15 且
softRatio: 1,旧代码的软档也永不触发。2026-09-28 迁成 bundle 时这条手贴行已删除,bundle 行只有
6 个键、不含这些旧键;那五行的原文仍在备份 profiles/web/cordis.patch.yml.bak-adg-token-budget-bundle-migration
里(本机当次读数见 docs/evidence.md §16.5)。
**最容易误判的一点:**校准期的那个到期检查点在日志里长得像
dry-run step stage: would nudge tier=1/14 step=4 label=… ——
它证明的是计数到了、不是消息发出去了。要区分"提醒真的注入"和"只是记账",看行首:
step stage: nudged … 是真的注入了,带 dry-run 前缀的都是没注入的。
武装之后这条判断仍然适用:只有 nudged 那一种算注入。
哪些是实测、哪些是设计意图,与上一节一个口径 —— 不写没量过的结论:
| 事项 | 证据 |
|---|---|
| 决策逻辑(配置归一化、步数计数与 tier 判定、筛选条件、状态释放、每步最多一条消息、注入消息的来源) | 单元测试:cd plugin/dsh-adg-token-budget && node --test test,只依赖 node:test / node:assert(checkout 里没有 node_modules 也能跑)—— 实测 54 个用例全过(2026-09-28 迁成 bundle 后从 51 增到 54:新增三条钉住 bundle 挂载行的形状;同一跑 tests 54 / pass 54 / fail 0,见 docs/evidence.md §16.5);另做过变异验证:步数档 18 个变异(M1..M18,含自定义措辞路径与那条消息来源)。2026-09-28 复核新跑的 M18 被 4 条断言抓住;同一跑里 M1 / M3 / M4 / M5 / M6 报 NOT-APPLIED —— 它们的变异串是对 token 两档移除之前的代码写的,是 harness 漂移、不是回归(存活档由 M8 / M13 罩住,同跑仍被抓住)。详见插件 README 的「Mutation verification」与 docs/evidence.md §15 |
package.json 形状、ESM 可 import |
从当时模拟的部署位置(…/$DSH_HOME/plugins/dsh-adg-token-budget/src/plugin.js —— 该落点已废弃,现在装的是 …/$DSH_HOME/bundles/…)import 起来验过;迁移后真机也 import 成功(就是下面那行 activation 写的) |
迁成 bundle 之后这一行仍然挂载并 apply |
真机实测(2026-09-28,UTC;证据 $DSH_HOME/adg-token-budget.log,逐字行见 docs/evidence.md §16.4):手贴行删除 + install_bundle 装上之后(没有重启 dsh:那次生效来自改写 profile 清单带来的整份 patch 栈重读,不是 bundles/ 被 watch),01:47:48.467Z 写下 activation: active … presets=[adg] … stepTiers=[4, 8, …, 280] dryRun=false logFile='C:\Users\cenqian\.dsh\adg-token-budget.log' ⇒ bundle 层的行确实被加载,!!js dshHomePath(...) 也求值成功。配套读数:list_bundles 里 dsh-adg-token-budget version 0.3.0 / enabled: true / rows rowId: adg-token-budget(迁移前这条根本不存在)、list_plugins 条目总数迁移前后都是 190(没有重复挂载)。未观测:冷启动后的 bundle 层(量法见 docs/evidence.md §16.5 / §8) |
| profile 层覆盖行热重载 + 迁移后的行真的计数并注入 | 真机实测(同上,§16.4):加 / 删 profile 层覆盖行后 01:57:12 / 01:58:46 各写一行激活行 ⇒ 不用重启;01:59:25 / 01:59:31 两行 step stage: nudged tier=1/2 step=1 / tier=2/2 step=2 加 01:59:43 的 settled: released session state ⇒ 计数与注入都真的发生了。注意归属:那一次走的是临时覆盖行 presets: ['cordis'] + stepTiers: [1, 2] 的通用委派(通用 subagent / subagent_fork 委派的子代理 session 头记 agentPreset: "cordis"、delegationDepth: 1),不是出厂 presets: ['adg'] 的那 14 档 ⇒ 迁移后对真实 Adg 专家子代理的注入仍是未观测(量法见 docs/evidence.md §16.5);desktop profile 生效同样未观测(它的 patchReload 不是 live) |
createUserMessage 的五个解析锚点都解析到同一份模块 |
实测(详见插件自己的 README) |
| 行能被 dsh 加载、不报 fatal | 实测:activation: inactive (enabled: false) 就是宿主加载成功后写的 |
agent/pre-step 真的走到这个监听器 |
实测:真机的三次检查点注入,以及几百行 dry-run 判定 |
dryRun 真的不注入 |
实测:dry-run 期只有 dry-run step stage: would nudge … 行,0 条注入的消息 |
步数检查点(step stage: nudged)在真机上发生 |
已实测:三次注入(17:45:49 / 17:55:04 / 18:22:13,都在 tier=1/3 step=12),且能在对应子代理的 session.v3.jsonl.zstd 里找到那条消息本身(role: user、source: {kind:'plugin', plugin:'dsh-adg-token-budget'}),毫秒级对齐。未覆盖:第一个 tier 之外的档、新阶梯下的注入、dry-run step stage |
| 注入消息的来源在 session format v4 下合法 | 源码级事实 + 真机实测(2026-09-28):v4 废弃了上面那行的 {kind:'plugin', plugin} 包装,准入拒在持久化写入路径上(format v4 message requires a producer-owned source kind),异常在监听器返回之后抛出、插件接不住 —— 当时新阶梯的第一档是第 4 步,凡走到第 4 步的受管子代理全部在那里失败,调度者看到 subagent run failed … SessionFormatError。现在写的是生产者自有的 {kind:'plugin:dsh-adg-token-budget'}(与 v4 读 v3 记录时分配的 kind 逐一相同):用装好的宿主 assertV4RowAdmission 复测通过、旧包装被拒(docs/evidence.md §15)。上面那行 v3 摘录仍是历史原文,不许改写成新形状 |
| 注入的提醒被子代理读到并回应 | 已实测(3 例):两个收到命令式措辞的表示要收敛;收到选择式措辞的那个明确选择"继续"并列出剩余必需工作、没有缩减计划 |
| 更早更密的阶梯 + 选择式措辞到底有没有用 | 未观测,而且是这个功能的核心问题:分布(37 个子代理,中位数 39、p10 6)、成本(214 条约占 0.25%)、以及"第 12 步时只花了 10–14 万 token"都是实测的;"收到 14 条检查点的子代理是否比不收到时更早收敛"没有任何证据。量法写在插件 README 里:audit-steps.mjs 前后各跑一次,比分位数 |
settled: released session state … / 恢复的子代理被再次提醒 |
settled: 已实测(17:55:33,收敛后 29 秒);恢复的子代理被再次提醒仍未观测 |
已移除的 token 两档(agent.cancel 会被调用、软档会注入、阈值比较按真实账单在跑) |
历史实测,不代表当前行为:hard stage: cancel(usage=7651807)、would nudge 最早 2,128,454、would cancel 最早 3,014,252,usage=9824410 / usage=48992135 是真实累计值;437 行 would cancel + 36 行 would nudge,0 次 dry-run 期发出的 agent.cancel;soft stage: nudged 从未观测。这些代码路径已删除,今天不会再产生任何一行 |
adg preset 改动后能组合 |
实测(把改好的文件部署到 .agent-presets/adg/ 之后重跑的那一次):resolve('adg') → broken 为空、compositionInventory() → 34 行(当时那一步还写着 standingKeyFor,该 API 在本版已不存在) / 10 个 tool-subagent 模块行里 8 行启用 / tool-subagent-fork 0 行(行数不变是预期的:这次改的是 persona 文本与政策,不是行的增删)。2026-09-28 加第 9 个专家后按同一套口径重测:mounted OK、35 行 / 11 个 tool-subagent 模块行里 9 行启用(新增的 agent-general 与其余 8 行同为 enabled: true、fiberState 相同)/ 0 行 fork |
agent_general 的子代理侧工具目录 = 它的 allow 名单(16 项) |
未观测(源码级事实 + 静态自检,待一次真实委派确认):机制与其余专家完全相同(子代理 composeFrom 继承组合 → tools.restrict({allow}) 取交集),而已实测过的 agent_coder 正是"可见目录恰好等于 allow 名单";本次的 read_image 是条件性注册名,tools/check-preset.mjs 会为它出一条 WARN。真机验收点:重启后在 Adg 新对话里说"把这件事交给子代理",看它是否照规则 17 派发、以及它的任务末尾是否被自动追加 "Your parent agent id is … send_message" 那段指引 |
这不是代码风格偏好,是 cordis 的硬约束:apply 抛出去、又没有声明 schema,就是 fiber 失败,
dsh 把加载失败的行报成 fatal 启动错误;更糟的是对一个没挂载的服务写死 static inject,
那条 entry 会永远停在 pending,同样是 fatal。所以这个插件的写法是:
apply永不抛。 整个函数体包在 try 里,任何失败都走ctx.logger?.warn并降级成 no-op。- 每个事件处理器都包了 try。 检查点逻辑里的 bug 只会被抓住、记一次日志、然后
return next()—— 它没法中断谁的回合。 - 没有
static inject,也不ctx.get任何服务。 它只ctx.on('agent/pre-step')和(可选的)ctx.on('subagent/end');这两个事件拿不到时降级成 no-op,靠的是typeof ctx?.on !== 'function'的检查,不会让行停在pending。 - 没有静态 import 任何
@deepseek-ai/*。 插件是以普通目录部署在$DSH_HOME/bundles/下(旧落点$DSH_HOME/plugins/已于 2026-09-28 删除)、再link:进 profile 的, 需要什么就在调用时用createRequire解析,解析失败也都可存活(必要时回落到本地构造函数)。 - 没有顶层副作用,没有
process.exit,没有网络,除了配置的logFile不写任何文件。 - 状态有界:每会话状态放在按
agent.id索引的Map里,条目只记两个小字段 (steps/firedTiers:已进入的步数与已触发的 tier 索引),subagent/end时释放,ctx.effect的 disposer 再整体清一次。stepNudge: false时一步都不建(整层关掉);stepNudge打开时,被治理子代理进入的第一步就会建条目 —— 计数需要它,dryRun也不例外 (校准要看得见"第几步会提醒")。
不导出 Config schema 也是安全设计的一部分(理由见本节开头那段 cordis 约束):有 schema 的话,
一个打错的值就会让整个 profile 加载失败。
preset/
preset.yml # 在模式选择器里显示的名称与简介
agent.cordis.yml # 调度智能体 persona + 九个专家智能体行
skills/
adg-add-agent/
SKILL.md # 「给 Adg 加一个智能体」的操作手册
plugin/
dsh-adg-token-budget/ # host-plane 插件,**现在是一个 bundle**:子代理的步数收敛检查点(名字里的 "token-budget" 是历史名称,已不比较任何 token 阈值)
package.json # 部署单元:ESM 包,无运行期依赖;dsh.bundle.patch 指向 cordis.patch.yml
cordis.patch.yml # bundle 层 = 挂载行本体(出厂即武装;logFile 用 !!js dshHomePath(...) 求值)
src/ # config.js(归一化)/ budget.js(纯判定)/ plugin.js(注册监听器)
examples/
cordis.patch.yml # 键参考 + 手工覆盖模板(**不再是"可直接贴的挂载行"**;贴之前必须整块重写所有键)
LICENSE # MIT(package.json 的 files 里列了它,必须真的存在)
README.md # 插件自己的说明:口径、筛选、安全设计、验证方式
INSTALL.md # 部署/启用/确认/回滚的操作清单(仓库文档,不进部署)
test/ # 单元测试(node --test),**不进部署**
tools/
check-preset.mjs # 静态自检:专家行字段、toolName 唯一、allow 合法性、
# 通用委派行、调度名册与专家行双向一致,以及三组
# 体积旋钮所在行的结构与"被写回时的合法性"(不钉死取值)
gen-preset-bundle.mjs # 构建脚本:preset/ 三份源文件 → bundle/adg-preset/(生成物,gitignore)。
# --with-billion-context = 四个上下文工具进 9 个专家行的 allow
# + compaction-basic 注入 config.auto=false(都只改生成物,见红线 11)
check-bundle-flavor.mjs # 产物自检:按 plain|bili 断言两件事 —— 9 个专家行的 allow 里有没有那四个
# 名字,compaction-basic 的 config.auto 是否为 false(plain 则断言都不存在)
# (check-preset.mjs 只看源文件,产物是它的盲区)
has-billion-context.mjs # 判据:某 profile 到底挂没挂 billion-context。install.* 用它同时决定两件
# 相反的事 —— 注入那四个工具 / 不启用 dsh-adg-token-budget
browser/ # 浏览器工具链(有头 Chrome + 最小 CDP 驱动,零依赖,Node >= 22)
cli.mjs # 唯一入口:launch / status / tabs / profile / open / text / eval / shot / close-tab / close
lib/target.mjs # 纯函数:profile / 端口 / Chrome 探测 / 启动参数 / 复用决策
lib/cdp.mjs # 最小 CDP 通道 + 会话便捷层(socketFactory 可注入,便于无浏览器测试)
test/browser.test.mjs # 36 个单元用例(不需要浏览器)
AGENTS.md # 模块路由:命令、模块特有红线、跨模块路由、生效方式
design.md # 对象设计:BrowserTarget / BrowserInstance / PageSession / PageTab 与 I1..I10
testing-guide.md # 不变量→用例全表、三个状态机迁移矩阵、消费侧契约、未观测清单
install.ps1 # Windows 安装脚本(preset bundle + 技能 + 插件 bundle + browser 工具链;挂载行由包自带,不再手贴)
install.sh # macOS / Linux 安装脚本(同上,行为等价)
- 从 DSH 出厂 preset
standard(标准模式)复制而来,实质改动是十四处:persona增加调度名册与分派规则(步数收敛不写在调度者 persona 里,交给下面的插件在运行期 注入);delegation组由通用委派行换成专家行;九个专家的 persona 末尾各留一句收敛纪律;compaction/tool-web三行不覆盖任何体积旋钮(回归出厂默认, 理由见 为什么撤销 preset 侧的体积闸门);agent_browser多一条权限前置闸门(本机沙箱下浏览器起不来,见「浏览器专家需要完全权限」);agent_browser的登录墙/验证码多一条人工介入协议(子代理问不了用户,改由调度者转达, 见「登录墙与验证码:人工介入协议」);agent_browser的浏览器操作收敛到仓库里的browser/工具链(cli.mjs一个入口、零依赖、 有头、profile 固定在<DSH_HOME>/browser-profile且与工作区无关,见 浏览器工具链与登录态资产);persona多两条派发拓扑规则(同一实体 + 同一性质的任务只派一次;同一实体的后续任务接给 已经读过它的那个专家 —— 2026-09-29 给后半条补了status判据与"running三分支":send_message恒为 steer,所以先看list_agents,只有"修正/补充同一件事"才在running时 现在发,"另一件事"等结算通知唤起后再接给同一个它,"取代在飞任务"先interrupt_agent);再加两条编排层规则(大范围改动先让agent_researcher出path:line再让agent_coder按位改;跨专家传递大材料走 digest,工件只落平台临时根、 任务结束即删);再加必要性闸门 + 强制挂号(规则 15)与委派 prompt 的五项必填 (含验收标准与本次不做);再加输出/交接去冗余纪律(规则 5 的五项分字段写 + 规则 10 的四条禁止,且不设字数上限);同时把浏览器权限闸门与人工介入两条(规则 11 / 12) 压缩措辞(1281 → 812 字符,判定与分支语义未变);最后加第 9 个专家agent_general(交接专用、全功能、叶子)与调度 persona 规则 17(只在用户显式要求时派发;派发时重申 "结束前用send_message回报上级";回报后按规则 7 接给同一个它)—— 这些规则与压缩的量化见 多智能体的 token 消耗。 agent_browser需要danger-full-access是本机的硬约束,不是本 preset 的选择。 它无法从 preset 侧修(父智能体不能指定子智能体权限、子代理不能自己升权、沙箱行在 host-plane), 所以闸门做在调度侧、且是提示级的:见「浏览器专家需要完全权限」一节的三问三答与取舍。browser/是第三条链路(另两条是 preset 与插件):它是${DSH_HOME:-~/.dsh}/browser/下的 普通文件,不是 preset 也不是插件 —— 改完重新跑一次安装脚本就生效,不用重启 dsh; 要求 Node ≥ 22(用全局WebSocket,lib/cdp.mjs的assertRuntime()会显式报错、不静默降级), 零第三方依赖(旧形态装的playwright-core已不再需要)。逐条不变量与实测见browser/design.md与docs/evidence.md§13。- 人工介入也只能由调度者转达:
dsh-user-questions的ask()只认 live runtime root, 被委派的子代理拿到DELEGATED_CALLER。这条同样是机制约束,不是本 preset 的选择。 - 依赖标准模式本来就有的出厂包(
@deepseek-ai/dsh-tool-subagent、@deepseek-ai/dsh-persona、@deepseek-ai/dsh-skill-filesystem、@deepseek-ai/dsh-tool-subagent-control等)。 - 新增/删除/修改智能体后需要重启 dsh 才生效,这是 preset 挂载机制决定的,不是缺陷。
- 插件是另一条链路(名字
dsh-adg-token-budget是历史名称,它不比较任何 token 阈值): 它是 host-plane 的单半边行(没有浏览器半边),2026-09-28 起这一行由包自己的cordis.patch.yml在 bundle 层提供(不再手贴进 profile 的 patch 层);部署出来的是$DSH_HOME/bundles/下的稳定目录 (与dsh-adg-preset同一根,再link:进 profile、并选进该 profile 的dsh.profile.bundles), 所以仓库被删/被挪都不影响已经装好的 dsh。生效口径分三层:改 bundle 层那一行本体 → 以重启 dsh 为准(没有任何东西 watchbundles/,别承诺"不重启也会生效");改 profile 层的config:覆盖行 → 热重载、不用重启;改src/里的代码则要重启 —— 热重载不重新 import 已加载的模块。它只依赖宿主本来就有的agent/pre-step(状态释放在可选的事件subagent/end上),拿不到时它自己降级成 no-op (见 安全设计)。 install.ps1带 UTF-8 BOM,是有意的,不要去掉。 Windows PowerShell 5.1 在没有 BOM 时 会按系统 ANSI 代码页读取脚本,中文会变成乱码并直接解析失败(本仓库已实测复现并修复)。 这条同样适用于任何新写的、含中文的.ps1。 而且编辑工具会悄悄把它去掉:本次交付里一次普通的文本替换就删掉了 BOM (前三个字节从EF BB BF变成23 20 E5),补回来之后Parser::ParseFile才重新 0 错误。 改完这个文件请单独确认前三个字节仍是EF BB BF——git diff在"两边都有 BOM"时看不出差别, 但一次丢 BOM 的提交会让安装脚本在 5.1 上直接解析失败,而 diff 里只会看到几行注释改动。
(人类可忽略;把仓库地址交给 AI 时让它照此执行。)
2026-09-28 起形状变了 —— 先读这一段,别照旧文档装。 dsh 0.1.7-rc.2 移除了
$DSH_HOME/.agent-presets/<id>/那套目录发现机制(旧的@deepseek-ai/dsh-agent-presets包也已被移到一边)。旧装法(往.agent-presets/adg/拷preset.yml+agent.cordis.yml)装出来的东西没有任何组件会去读 —— 这就是"更新 dsh 之后预设加载不出来"的根因,不是配置写错了。现在 preset 是一个 bundle:包清单声明dsh.bundle.patch,patch 里insert一行@deepseek-ai/dsh-agent-preset声明行 (config里是id/name/description/order/plugins)。 同一版 dsh 还改过一个包名(引擎行@deepseek-ai/dsh-workflow-worker-thread→@deepseek-ai/dsh-workflow-ptc),用旧名会让整份 preset 变成broken而不可用 —— 装完必须按第 8 步做真实挂载校验。实测依据:docs/evidence.md§14。 同一天插件也换成了同一个形状:dsh-adg-token-budget现在是一个 bundle,挂载行由包自己的cordis.patch.yml提供(旧落点${DSH_HOME:-~/.dsh}/plugins/dsh-adg-token-budget/与那条手贴行都已删除), 见第 5、6 步与docs/evidence.md§16。
git clone <repo-url> <tempdir>- 生成 bundle:
node <tempdir>/tools/gen-preset-bundle.mjs。 它读preset/preset.yml(显示元数据)+preset/agent.cordis.yml(整个插件条目列表)+preset/bundle.package.json(包清单模板),写<tempdir>/bundle/adg-preset/{cordis.patch.yml,package.json}。bundle/是构建产物(在.gitignore里),任何情况下都不要手改生成物 —— 要改就改preset/下的源文件再重跑;install.ps1/install.sh每次安装都会把两种味道都重跑一遍。 目标 profile 挂着 billion-context 时还要生成注入版:node <tempdir>/tools/gen-preset-bundle.mjs --with-billion-context(同一份源文件的第二种味道,见红线 11);手工装的时候别只装 plain。 - 装 bundle:用
plugin_manager的install_bundle,target给 bundle 目录的绝对路径 —— 包安装与dsh.profile.bundles选中由它自己完成,不要用 shell 命令复刻这两步。 想让"仓库被删/被挪也不影响已装好的 dsh",先把生成物整个拷到它自己的稳定目录(plain →${DSH_HOME:-~/.dsh}/bundles/dsh-adg-preset/,注入版 →.../bundles/dsh-adg-preset-bili/),再拿那个 稳定目录当target(install.ps1/install.sh就是这么做的)。每个 profile 只装它该拿的那一份 (挂 bili = 注入版,没挂 = plain),不要把两份都link:进同一个 profile。装完list_bundles里应能看到dsh-adg-preset。 - 复制
<tempdir>/skills/adg-add-agent/SKILL.md→${DSH_HOME:-~/.dsh}/skills/adg-add-agent/SKILL.md - 部署插件 —— 它现在是一个 bundle(包名
dsh-adg-token-budget是历史名称,它不比较任何 token 阈值):把<tempdir>/plugin/dsh-adg-token-budget/里的package.json、cordis.patch.yml、src/、README.md、examples/、LICENSE(六项,与package.json的files一致)复制到${DSH_HOME:-~/.dsh}/bundles/dsh-adg-token-budget/(与dsh-adg-preset同一根;旧落点${DSH_HOME:-~/.dsh}/plugins/dsh-adg-token-budget/已删除),然后在目标 profile 里pnpm add link:<那个目录>(等价于dsh plugin --profile <name> add link:<dir>),并把dsh-adg-token-budget写进该 profile 的dsh.profile.bundles—— 光有依赖不算选中(用第 3 步那个plugin_manager install_bundle、把target给那个稳定目录,会连这两步一起做)。test/与INSTALL.md不要拷;先删目标目录再拷(重复执行要干净覆盖)。cordis.patch.yml是这六项里的挂载行本身:缺了它,这个包只是普通依赖。 不要再往$DSH_HOME/profiles/node_modules/放:那个"所有 profile 共享的模块解析根" 在本版 dsh 的模块解析里被显式排除(本机实测放那儿解析不到,插件行挂不起来), 改成 profile 自己的node_modules或link:稳定目录,理由见 为什么装在$DSH_HOME/bundles。 - 不要再"补挂载行" —— 那一行现在由包自己的
cordis.patch.yml在 bundle 层提供(package.json的dsh.bundle.patch: ./cordis.patch.yml)。AI 不该再往<dshHome>/profiles/<profile>/cordis.patch.yml手贴insert:块(<dshHome>=${DSH_HOME:-~/.dsh};install.*默认处理所有"能装 preset 的 profile", 判据是该 profile 的dsh.profile.bundles含@deepseek-ai/dsh-web-app—— 声明agentPresets服务的agent-preset-registry正是它提供的)。- profile 的 patch 文件里如果还留着旧机制那条手贴行(
- insert:/- id: adg-token-budget): 只报告、不代删(脚本也一样:不猜用户手改过的文件),并告诉用户它必须删 —— profile 层在所有 bundle 层之后应用,按 id 命中的 patch 整块替换config(不是深合并),留着它,bundle 行写的那些键就被它 盖掉了。本机迁移时删掉的那份手贴行留有备份cordis.patch.yml.bak-adg-token-budget-bundle-migration。 这种残留本机实测过(真机实测,docs/evidence.md§16.4 第 6 条;别与 §14.3 混引 —— 那条讲的是"同一个 profile 里同一个 preset 声明行只能有一个家",本条是跨层同 id 合并):它不会多挂一行 (条目总数仍 190、日志只写一行激活行),但它整块接管那一行的config:—— 实测把dryRun顶成true⇒ 注入当场停掉,而fiberPhase仍是active、日志看起来完全正常(最难查的地方), 并且让这一行脱离管理(list_plugins里patchId消失、多出readOnlyReason: "unaddressable"⇒ Plugins 页与set_plugin都点不动,只能改 patch 文件;删掉残留行后patchId回来)。list_bundles的overrides发现不了它(本次仍是[])—— 最快的信号是list_plugins里这一条还有没有patchId。 - bundle 层那一行出厂即武装:
enabled: true、presets: ['adg']、stepNudge: true、stepTiers: [4, 8, 12, 18, 24, 32, 42, 55, 72, 95, 125, 165, 215, 280]、dryRun: false、不写stepText(用内置正文)、logFile: !!js dshHomePath('adg-token-budget.log')—— 由 Loader 求值成$DSH_HOME/adg-token-budget.log,任何机器都一样,不用再拼绝对路径。所以"三步上线"(enabled: false→+ dryRun: true校准 →dryRun: false)现在是一条可选的校准路径,不再是装完的必经步骤。 - 要按 profile 改配置就在 profile 层写覆盖行:
- id: adg-token-budget+ 一份完整的config:(所有要留的键都要重写,漏掉的键回落到src/config.js的DEFAULT_CONFIG);键参考见plugin/dsh-adg-token-budget/examples/cordis.patch.yml—— 那份例子现在是键参考 + 手工覆盖模板, 不再是"可直接贴的挂载行"。写文件用不带 BOM 的 UTF-8。行 id / 包名里的 "token-budget" 是历史名称(现在不比较任何 token 阈值),照抄即可,不要改名。 - 生效方式分三层,别承诺错:改 bundle 层那一行本体(
bundles/dsh-adg-token-budget/cordis.patch.yml) ⇒ 以重启 dsh 为准(没有任何东西 watchbundles/,禁止宣称"不重启也会生效");改 profile 层的config:覆盖行 ⇒ 热重载、不用重启(web是patchReload: live;Plugins 页保存写的就是这一层); 改包里src/的代码 ⇒ 必须重启(热重载只重放 config,不会重新import已经加载过的模块 —— 见 装完必须重启 dsh)。不要在"代码还是旧版"的状态下把dryRun关掉:旧代码不认新字段, 缺省的budgetTokens就是 300 万,会把那条已移除的硬档真武装。 - 验收判据(两条都要,缺一条就不算验过):①
plugin_manager list_bundles里有dsh-adg-token-budget这一条(version 0.3.0、enabled: true、installed: true、removable: true、rows[{rowId: adg-token-budget, moduleName: dsh-adg-token-budget, entryId: include:adg-token-budget}]、overrides: [])—— 手贴时代这一条根本不存在(挂载行只活在 profile 的 patch 文件里,Plugin Manager 的 bundle 清单看不到它),那正是"看起来没生效"的直接原因;②logFile里新出现一行activation: …。 真机实测的形状(证据C:\Users\cenqian\.dsh\adg-token-budget.log,UTC;逐字行见docs/evidence.md§16.4):2026-09-28T01:47:48.467Z activation: active createUserMessage=profile-fallback:web presets=[adg] stepNudge=true stepTiers=[4, 8, 12, 18, 24, 32, 42, 55, 72, 95, 125, 165, 215, 280] stepText=builtin dryRun=false logFile='C:\Users\cenqian\.dsh\adg-token-budget.log'—— 这一行写在手贴行已删、install_bundle已装之后,当时进程里唯一能提供这一行的层就是 bundle 层。 想关掉它:在 profile 层写一条只含- id: adg-token-budget+disabled: true(不带config:)的 覆盖行 —— 实测可用:list_plugins里include:adg-token-budget变成enabled: false/fiberPhase: null、 条目总数仍是 190、日志不写任何行;删掉那条覆盖行后日志立刻重新写出activation: active …(2026-09-28T02:19:52.105Z,docs/evidence.md§16.4 第 5 条)。disabled与enabled: false是两个 不同的开关:前者 Loader 根本不 import(所以没有日志证据,判活要看list_plugins),后者照样写一行activation: inactive (enabled: false) …。 - 未观测,不许说成实测(
docs/evidence.md§16.5):冷启动后的 bundle 层(上面那次生效靠的是改写 profile 清单带来的整份 patch 栈重读,不是bundles/被 watch;量法:重启 dsh →list_bundles仍有 这条 + 日志新出现一行activation: …);presets: ['adg']对真实 Adg 专家子代理的注入(量法:新对话里 走一次 Adg 专家委派,看step stage: nudged … label=adg/…);desktopprofile 生效(它的patchReload不是live,要下次启动才生效;量法:启动 desktop profile → 看同一日志的activation行)。 机制前提(本机实测,docs/evidence.md§16.4):通用subagent/subagent_fork委派出来的子代理 session 头记agentPreset: "cordis"、delegationDepth: 1(adg专家行委派出来的记adg/<uuid>)⇒presets: ['adg']按设计不治理通用委派(fail open);上面那次实测到的注入正是临时把presets覆盖成['cordis']才走通的。
- profile 的 patch 文件里如果还留着旧机制那条手贴行(
- 目标目录通常在工作区之外,写入会被沙箱拒绝一次;按提示用
sandbox_permissions升级重试同一条命令(用户会在界面上批准)。 - 校验(只有真实挂载算证据):挂一个注入
agentPresets的临时插件 (见技能editing-cordis-compositions),await agentPresets.resolve('adg')的broken必须为空 —— 它是"这份组合能不能用"的唯一判据, 报的是具体哪一行起不来(例如workflow-ptc (@deepseek-ai/dsh-workflow-ptc): never started);await agentPresets.list()里应能看到adg(standingKeyFor在本版 dsh 里已经不存在, 别照旧文档调它);await agentPresets.compositionInventory()里adg必须出现 9 条启用的专家行:agent-file、agent-computer、agent-app、agent-browser、agent-search、agent-researcher、agent-coder、agent-reviewer、agent-general,且没有tool-subagent-fork行。判据要写准:compositionInventory()报的是模块名,所以@deepseek-ai/dsh-tool-subagent会出现 11 次 (上面 9 条 +tool-subagent-codex/tool-subagent-claude-code这两条enabled: false的), 按"模块名出现 9 次"去断言会误报失败 —— 本机实测就是这个 11/9/0 的形状 (加第 9 个专家之前是 10/8/0)。2026-09-28 实测的完整形状:broken为空、adg共 35 行、 32 行启用且fiberState === 2(真挂载)、3 行关闭(tool-bash与两条 codex/claude-code)、 9 条@deepseek-ai/dsh-tool-subagent启用、fork 0 条、引擎行workflow-ptc处于挂载态。- 也可以直接
node tools/check-preset.mjs(零依赖,exit 0 表示通过;现在只有仓库这一份文本, 不再有"已安装的第二份"可以传路径)。注意它只是文本扫描器:exit 0 不等于"文件能解析、 插件已挂载",所以上面那条真实挂载的校验不能省。 - 插件那半边它完全没覆盖:插件能不能 import、行有没有激活,只能按
怎么确认它已经武装 看宿主日志与
logFile;再加两条 bundle 层的判据 (都在第 6 步里):plugin_manager list_bundles必须看得到dsh-adg-token-budget这一条, 且list_plugins里include:adg-token-budget还在写patchId——patchId消失就等于 profile 层残留了同 id 的手贴行、把这一行整块接管了。
- 明确告诉用户:preset 改动仍按"重启 dsh + 新会话"验收(bundle 层不是只在启动时读 ——
实测 profile 的
cordis.patch.yml或 profile 清单变动会触发整份 patch 栈重读、并让声明重新注册, 但已挂载的会话不会中途换组合,所以验收口径不变)。重启后在新建对话里选择「Adg 多智能体模式」。 (插件写在 profile 层的那条config:覆盖行不用等重启 —— 但只有覆盖行是这样:改 bundle 层 那一行本体(bundles/dsh-adg-token-budget/cordis.patch.yml)要重启 dsh,没有任何东西 watchbundles/(禁止宣称"不重启也会生效")。若本次交付的插件代码是新的 (激活行多了stepNudge=/stepTiers=/stepText=,且不再出现budgetTokens=/softThreshold=/softRatio=/cacheReadWeight=/softNudge=/hardDryRun=),则要: 部署代码 → 重启 dsh → 确认logFile里的激活行正在写新字段、且不再有任何 token 字段 → 这时才把dryRun从true改成false(上线顺序的第三步,也是终点)。 别把"装了"说成"验过了":步数检查点已经在真机上注入过,但新阶梯下的注入、 以及"提醒到底有没有效果"都还没有证据,而且 token 两档已经不存在了。) - 如果用户还需要在创造模式里说「给 Adg 加一个智能体」被识别,确认第 4 步的技能已就位——
<dshHome>/skills是dsh-skill-filesystem的用户技能根(rank 400),两种模式都会扫描且热加载。