Skip to content

Repository files navigation

Adg 多智能体模式(DSH agent preset)

一个 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 里若还留着旧机制那条手贴行,脚本只报告、不代删,而它必须被删掉(理由见 挂载行住在哪一层)。插件那一层是什么、怎么开、怎么确认已武装见 第二层:子代理的步数收敛检查点。

方式 A:把仓库地址交给 AI(推荐)

把这个仓库的地址发给 dsh 里的 AI,说一句「按仓库 README 装到本机」即可 —— 下面的 给 AI 的安装指令 一节就是写给它看的。

方式 B:手动

git clone <repo-url> ~/adg-multi-agent
sh ~/adg-multi-agent/install.sh                     # macOS / Linux
git clone <repo-url> $HOME\adg-multi-agent
powershell -ExecutionPolicy Bypass -File $HOME\adg-multi-agent\install.ps1   # Windows

装完必须重启 dsh

预设改动按"重启"来验收,别赌热重载。 旧版本文档里写过"已实测: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 问一次,再按回答决定。

根因:受限令牌禁止创建浏览器内部 IPC 必需的有名管道

沙箱在 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。

为什么不能从 preset 侧修(三个问题的答案)

问题 结论 源码依据(源码级事实)
父智能体能否给子智能体指定权限范围? 不能 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 事件, 而切换权限正是写入这条事件的动作(部署默认值不会被继承)。

现在的处置:调度侧两道闸门(提示级,不是权限强制)

  1. 派发前(调度 persona 规则 11):不是 danger-full-access 就先 ask_user_question, 选项是「已切到完全权限,继续派发」/「改用降级方案:只做 web_fetch 静态抓取(不能交互)」/ 「暂不做这项网页操作」。用户答已切换后,先确认上下文那行真的变了再派发;没变就如实说没切成功。
  2. 失败时(agent_browser persona):命中上面那张表的任一签名就立刻停手,如实报 「本会话不是完全权限,浏览器自动化不可用」+ 报错原文,不许反复换参数重试、不许假装完成。

这是流程闸门,不是安全边界:它靠 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 都通)但 本仓库未实测,不许写成可行 —— 而且既然全访问下浏览器本来就能用,这条路只在「用户不愿切权限」时才有意义。

怎么用

  1. 新对话选择 Adg 多智能体模式,直接说需求。
  2. 调度智能体自己负责意图理解、任务拆解、调度与汇总,先判断范围再按 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 接给同一个它(沿用已 持久化的会话与上下文),要换角色或你想再开一个新上下文时才另派。

  1. 需要多个专家时(实体不同或性质不同才拆),它在同一条回复里并行启动多个委派,不串行等待; 而且一律以后台方式派出(不设 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 消耗。
  2. 专家越界时会回一句「超出能力范围,需要 agent_X」;调度智能体据此再派发下一步,链路可追踪。 专家之间不能直接互相转交 —— 它们看不到 agent_* 名册行(见「设计要点」)。
  3. 专家的返回值只是给调度智能体汇总用的中间材料,不是给你的最终答复;最终交付由调度智能体整理后给出。

怎么加一个智能体

在任何模式里说,例如:

给 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 不给收集工具会让"后台跑了但取不回来"。

token 成本纪律(这些上限是怎么来的)

这一节的每个数字都是实测的,不是估算。测量基线: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 收窄检索面只会让同一件事被检索更多轮,而步数才是账单里的乘数

为什么撤销 preset 侧的体积闸门

那一轮压低是实测之后加上去的(基线见本节开头),撤销同样有实测理由:

  • 截断工具输出会把事实切掉。 被裁掉的是工具已经取到的原文;模型看到残缺结果只有三条路: 重取(更贵)、换个查询再试(还是更贵)、或者拿残缺证据下结论(更差)。
  • 提前压缩会让上下文失真。 压缩不可逆:过了压缩点,一切只能基于摘要。
  • 写在 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 层保留的政策:专家侧的收敛纪律与调度侧的编排层规则

旋钮撤销之后,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 消耗:已落地与可选手段

这一节回答一个具体问题:在"不压体积旋钮、不给单个专家写预算"的口径下,还能怎么降 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 协同(可选能力)

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(应 web 1 / desktop 0)+ 看 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。

第二层:子代理的步数收敛检查点(插件 dsh-adg-token-budget)

上一节那三组旋钮(现已回归出厂默认)压的是单条结果的体积,它们管不到步数。 这一层补的正是步数:一个 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)、desktop profile 生效(它的 patchReload 不是 live)。

② 已移除的 token 两档:当年实测(历史证据,不代表当前行为)。 快照时 logFile 里已经有 5 个真实 adg 子代理走过旧代码的 token 判定(5 个全部 ≥ 300 万,最大 48,992,135), would cancel 437 行、would nudge 36 行,而 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 软档 / 硬档 — 已移除:不再有累计 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 命中的会话生效,条件按顺序判:

  1. enabled 为真;
  2. 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)永远不动,尤其不会动调度智能体;
  3. session.header.agentPreset 命中 presets。header 里没有 agentPreset 的子代理也不动 —— 这里故意 fail-open:猜错会给它本来没被指向的会话注入提醒;
  4. stepNudge 为真 —— 为假时整层关掉:不计数、不建状态、一步原样放行(见配置表)。

步数检查点不依赖任何投影或服务,所以拿不到 token 数据的会话里它照常提醒 —— 这也正是它有独立开关 stepNudge 的原因。

已移除的 token 两档:当年的实测证据

这一整节是历史。 下面的数字全部来自已移除的 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 为准: 没有任何东西 watch bundles/,单独编辑它不会自己触发重读(禁止宣称"不重启也会生效")。 两条相关的坑: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/bundles(而不是链到仓库)

部署目标是 ${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],就会在子代理的头两步各出现一次检查点。

推荐的上线顺序(三步)

  1. enabled: false —— 写在 profile 层的覆盖行里(要留的键全部重写):装上了,但 apply 在注册任何 监听器之前返回,一步都不参与。它照样写一行加载期激活行,所以"宿主确实加载过这个包"看得见。 bundle 行出厂不是这个值(出厂即 enabled: true + dryRun: false),所以这一步是主动退回校准态, 不再是装完的默认状态。
  2. enabled: true + dryRun: true —— 拿你自己的流量校准:到期的检查点只写一行日志, 不注入任何东西,也不消耗 tier。看清楚了再往下走。
  3. enabled: true + dryRun: false —— 提醒真的注入。这就是本机现在的状态 (stepNudge: true + 默认 14 档阶梯)。

没有第四步。 已经没有"只武装一半"的开关了(hardDryRun 随硬档一起移除),也没有可武装的 破坏性动作 —— 唯一的动作是注入一条可选提醒。反过来,不要在"代码还是旧版"的状态下把 dryRun 关掉:旧代码会把那条已移除的硬档真武装(见 装完必须重启 dsh)。

怎么关

三种,按「关得有多彻底」排:

  1. enabled: false(写在 profile 层的覆盖行里,要留的键全部重写)—— 插件自己的开关,apply 在 注册任何监听器之前返回:不注册监听器、不写决策日志,但照样写一行加载期激活行。推荐这一条 —— 它可撤回、看得见证据。注意 bundle 行出厂是 enabled: true,安装脚本也不再往 patch 里写任何值, 所以这个 false 得你自己写。
  2. 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 条)。
  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" 那段指引

安全设计(为什么它坏了也拖不垮 GUI)

这不是代码风格偏好,是 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 为准(没有任何东西 watch bundles/,别承诺"不重启也会生效");改 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 的安装指令

(人类可忽略;把仓库地址交给 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。

  1. git clone <repo-url> <tempdir>
  2. 生成 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。
  3. 装 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。
  4. 复制 <tempdir>/skills/adg-add-agent/SKILL.md → ${DSH_HOME:-~/.dsh}/skills/adg-add-agent/SKILL.md
  5. 部署插件 —— 它现在是一个 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。
  6. 不要再"补挂载行" —— 那一行现在由包自己的 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 为准(没有任何东西 watch bundles/,禁止宣称"不重启也会生效");改 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/…);desktop profile 生效(它的 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'] 才走通的。
  7. 目标目录通常在工作区之外,写入会被沙箱拒绝一次;按提示用 sandbox_permissions 升级重试同一条命令(用户会在界面上批准)。
  8. 校验(只有真实挂载算证据):挂一个注入 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 的手贴行、把这一行整块接管了。
  9. 明确告诉用户:preset 改动仍按"重启 dsh + 新会话"验收(bundle 层不是只在启动时读 —— 实测 profile 的 cordis.patch.yml 或 profile 清单变动会触发整份 patch 栈重读、并让声明重新注册, 但已挂载的会话不会中途换组合,所以验收口径不变)。重启后在新建对话里选择「Adg 多智能体模式」。 (插件写在 profile 层的那条 config: 覆盖行不用等重启 —— 但只有覆盖行是这样:改 bundle 层 那一行本体(bundles/dsh-adg-token-budget/cordis.patch.yml)要重启 dsh,没有任何东西 watch bundles/(禁止宣称"不重启也会生效")。若本次交付的插件代码是新的 (激活行多了 stepNudge= / stepTiers= / stepText=,且不再出现 budgetTokens= / softThreshold= / softRatio= / cacheReadWeight= / softNudge= / hardDryRun=),则要: 部署代码 → 重启 dsh → 确认 logFile 里的激活行正在写新字段、且不再有任何 token 字段 → 这时才把 dryRun 从 true 改成 false(上线顺序的第三步,也是终点)。 别把"装了"说成"验过了":步数检查点已经在真机上注入过,但新阶梯下的注入、 以及"提醒到底有没有效果"都还没有证据,而且 token 两档已经不存在了。)
  10. 如果用户还需要在创造模式里说「给 Adg 加一个智能体」被识别,确认第 4 步的技能已就位—— <dshHome>/skills 是 dsh-skill-filesystem 的用户技能根(rank 400),两种模式都会扫描且热加载。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages