Adg 多智能体模式(DSH agent preset)
一个 DSH 自建 agent preset:一个调度智能体 + 八个专家智能体名册。 任务由调度智能体判断范围后分派给对应专家;专家之间不能直接互相转交,越界时由调度智能体再派发下一步。
八个专家各一行职责:
agent_file|文件管家:文件与文档的检索定位、阅读理解与问答、批量整理归类、格式转换与文档生成agent_computer|系统与应用运维专员:系统与硬件信息查询、系统设置修改、优化清理、故障排查、进程与服务控制;桌面软件启停/安装卸载与命令行接口调用、Android 模拟器上的手机 App、微信小程序agent_browser|网页交互专员:需要登录、多步表单、点击选择、多页跳转抓取的网页操作agent_search|全网搜索专员:多轮联网检索与多源资料综述,结论带来源链接;只联网,不碰本地文件与系统agent_researcher|代码与仓库事实检索员:在本仓库/本机文件里定位实现、配置与出处,只读、必须带行号agent_coder|实现工程师:按已确定的方案改动工作区代码,并运行验证证明改动有效agent_reviewer|审查验证员:对已有改动做对抗性审查,只报告不修改agent_general|全功能智能体(交接专用):把一整件工作交接给一个独立上下文里的通用智能体,由它独自做完(读写文件、跑命令、联网检索、整理产出都在范围内);只在用户显式要求「交给子代理 / 另开一个上下文 / 换个智能体接手」时才派,它是叶子、不会再往下委派
第 8 个专家与前 7 个有本质区别:它不是"某个专项的专家",而是用户点名要"交接"时才派的全功能
角色,用途是上下文隔离(上层把活交给下一个智能体、另开一个上下文)。它拿的是本 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 加一个智能体」就能新增专家。
省 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-plain/、bundle/adg-bili/、bundle/adg-save-token/、bundle/adg-bili-save-token/,都在 .gitignore 里),各装到它自己的稳定目录(${DSH_HOME:-~/.dsh}/bundles/dsh-adg-preset = plain,以及 ...-bili / ...-save-token / ...-bili-save-token,见红线 10),再把 dsh-adg-preset 写进目标 profile 的 dsh.profile.bundles(四份 package.json 逐字节相同、包名都是 dsh-adg-preset,所以那一行四种味道通用) |
skills/adg-add-agent/SKILL.md |
${DSH_HOME:-~/.dsh}/skills/adg-add-agent/SKILL.md |
browser/(浏览器工具链,零依赖) |
${DSH_HOME:-~/.dsh}/browser/(重新跑一次安装脚本即生效,不用重启 dsh) |
安装脚本的参数:不带参数 = 对每个"能装 preset 的 profile"逐个注入组探测(判据与两组名字见
与构建期注入组协同);位置参数(sh install.sh web desktop;
PowerShell 用 -Profiles)指定 profile 子集;--billion-context=on|off(PowerShell:-BillionContext on|off)
可整体覆盖 bili 组的探测结果,覆盖与探测不一致时脚本会多打一行黄字警告。save-token 组与 bili 组一样走探测。
方式 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,
并通过上面的挂载校验确认它可组合;
只有在重启代价很高时,才值得去测"不重启会不会也能生效"。
专家名册
名册分两组:前四个(agent_file / agent_computer / 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 |
系统与硬件信息查询、系统设置修改、优化清理、故障排查、窗口与桌面管理、进程/服务/计划任务控制;桌面软件启停/安装卸载与内部功能调用、Android 模拟器上的 App、微信小程序(2026-10-01 起并入原 agent_app) |
不依赖模拟点击的 Windows API 路线可用(PowerShell / CIM / P-Invoke);软件侧只能走 CLI / adb / winget / 软件自带接口 —— GUI 视觉识别 + 模拟点击在 DSH 没有对应工具,凡是「看界面点按钮」类需求必须明说不具备,并给出替代(应用 CLI、adb 命令、官方 API、或请用户手动完成)。会改变系统状态的操作要先说明影响与回退;不可逆或高风险操作必须先停下、写明「需要用户确认后才能执行」 |
agent_browser |
登录态下的站点操作、多步表单、点击与下拉选择、多页跳转抓取 | 本会话必须是「完全权限」(danger-full-access)—— 硬约束,理由与源码依据见下一节「浏览器专家需要完全权限」:在 workspace-write / read-only 下本机 Chrome / Edge 根本起不来(受限令牌禁止创建 Chromium 内部 IPC 必需的有名管道;Brave 同一机制但未实测),所以调度者会先停下来问用户。能跑起来时走仓库里的 browser/ 工具链(cli.mjs 一个入口、零依赖、有头、profile 固定在 <DSH_HOME>/browser-profile,见「浏览器工具链与登录态资产」);工具链不可用、或目标本来就静态可取时降级成 web_fetch 单次抓取(只能取静态内容、不能交互),并在回答里说明是降级执行。遇到登录墙 / 验证码 / 二次验证按「登录墙与验证码:人工介入协议」办:专家开好有头窗口后停手并如实报,由调度者转达用户。shot --out 拍下的截图可以用 read_image 自己看(视觉校验,2026-10-01 起) |
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 根本起不来(Brave 走同一机制、但未实测)—— 这不是配置问题,
也不是 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 §6):
| 探测 | 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,且当时(2026-09-26)机器上只装了 Chrome 与 Edge,
其它浏览器未测试;全访问那一列只有 Chrome 做了完整的「启动 → 连 CDP → 导航 → 取回文本」,
Edge 只做到 --dump-dom 退出码 0。2026-10-01 补注:候选次序改为 Chrome → Brave → Edge 并新增 Brave(browser/design.md「非功能红线」);
本表是 2026-09-26 的读数、不回改,而 Brave 在受限令牌下如何失败属于未观测(量法见 browser/testing-guide.md 第 4 节)。
为什么不能从 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"、不弹窗。所以专家侧的升权重试是失败关闭,不是弹出审批 |
| 父级切换权限后,已经在跑的子代理会跟着变吗? | 不会 —— 新权限只对"切换之后新开的子代理"生效(2026-09-30 补,用户要求写进调度 persona 规则 11) | 权限在委派那一刻就被捕获:captureDelegatedPolicyOverrides()(dsh-subagent/lib/index.js:524-541)在子代理"首次 await 之前"同步取当时的父会话状态,并在子会话尚未发布的窗口里写成 source: 'delegation' 的 sandbox/mode / approval/policy / permission/preset 事件(:552-562 的 appendDelegatedPolicyOverrides());同一函数的注释逐字为 a later parent switch belongs to the parent's future, not to this child ⇒ 用户切到 danger-full-access 之后要新建委派,等旧子代理是等不到的 |
唯一能把子代理送进完全权限的路径是:用户在会话里把权限切到 danger-full-access。
子会话只继承父会话的显式覆盖值 —— captureDelegatedPolicyOverrides() 取的是
parent.ctx.get('sandboxPolicy')?.overrideOf(parent.session),也就是那条 sandbox/mode 事件,
而切换权限正是写入这条事件的动作(部署默认值不会被继承)。捕获发生在委派那一刻(同一函数的注释逐字为 a later parent switch belongs to the parent's future, not to this child)⇒ 父级之后切换权限,不会改变已经开始跑的子代理;新权限只对切换之后新开的子代理生效。
现在的处置:调度侧两道闸门(提示级,不是权限强制)
- 派发前(调度 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 §7 的复核)。
分工是:专家把窗口开好并停下来说明 → 调度者用 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 §8。
专家侧的窗口是怎么开的(真机实测,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 §8);仍未观测的是真实站点上真的走完这一步。
浏览器工具链与登录态资产
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= 行是路径,可能是 Chrome / Brave / Edge)
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 §8) |
标签页不堆积:text/eval/shot --url <新地址> 自己开的临时页读完自己收(--keep 才留);存量用 close-tab 点名清;不点名不关、也不许关到只剩 0 个页面 |
一次真实的酒店比价任务在窗口里留下 19 个标签页(12 个携程详情页 + 只差 query 的列表页),"读得越多、页越乱";而关到 0 个页面等于绕过 close,用户开着登录表单的页更绝不能被自动关掉(见 docs/evidence.md §8) |
边界(不做的事):不代填账号密码、不读取 profile 的 cookie 库、不做验证码识别与指纹伪装、不加 --no-sandbox 之类降权旗标、不引入 playwright / puppeteer(旧形态三条伪装旗标齐全,见 browser/design.md「非功能红线」);不判断哪一页"已经不需要了"—— 只关自己刚开的页与调用方点名的页。登录永远由人在有头窗口里完成 —— 短信与图形验证码都靠"把窗口开好 → 停手 → 调度者转达"这条人工介入链路(见上一节),自动化只负责把页面开到那一步。
未观测:真实站点的登录墙端到端(用户真的登录 → 专家真的抓到登录后内容)没有跑过;专家是否真的照 persona 用这套工具(含收尾点名清理),也没有真实 Adg 会话为证。实测到的是机制 —— 逐条见 docs/evidence.md §8。
证据档位与未观测
- 真机实测(2026-09-26,同一台机器上的 A/B,只改会话文件策略):上面那张两列探测表;
原始输出与复现脚本见
docs/evidence.md§6。「全访问下浏览器确实可用」是实测(Chrome 走完了 启动 → 连 CDP → 导航 → 取回页面文本),不再是用户报告。 - 源码级事实:三个问题的结论,两处「子代理被钉死」的位置(路径见上表),以及人工介入为什么
只能由调度者转达(
ask()只认agents.roots(),子代理拿DELEGATED_CALLER)。 - 未观测(人工介入):真实站点的登录/验证码流程没有端到端跑过;「调度者是否真的每次都转达」 同样没有真实会话为证;「关掉浏览器后再靠 profile 复用登录态」也没有证据(cookie 30 秒内没落盘)。 已实测的只是机制:有头窗口存活 + 跨工具调用 CDP 重连 + 能继续驱动。
- 未观测:「调度者是否真的每次都先问」没有实测 —— 闸门刚落地,还没有一次真实 Adg 会话走过它;
「用户在沙箱外自己拉起带
--remote-debugging-port的浏览器、专家只连那个 CDP 端口」这条路 设计上可能可行(受限策略下网络不受限,受限进程自建监听与本地fetch都通)但 本仓库未实测,不许写成可行 —— 而且既然全访问下浏览器本来就能用,这条路只在「用户不愿切权限」时才有意义。
怎么用
- 新对话选择 Adg 多智能体模式,直接说需求。
- 调度智能体自己负责意图理解、任务拆解、调度与汇总,先判断范围再按 8 个专家的范围派发:
| 需求范围 | 派给 |
|---|---|
| 文件与文档(检索、整理、转换、生成) | agent_file |
| 系统 / 硬件 / 设置 / 清理 / 故障排查;软件与 App 操作(CLI、adb、winget、小程序) | agent_computer |
| 网页登录 / 填表 / 点击 / 多页抓取 | 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_reviewer(后者多web_search/web_fetch)的工具面相近,它们的边界靠 persona 与调度 规则维持 —— 2026-10-01 起agent_app已并入agent_computer(两行的allow原来逐字符相同), 名册里不再有单独的 App 操作专家。 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 侧的体积闸门)。
**刻意没有做的事:**不给任何请求设 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 字符内」「一次派发不往返」这类纪律把专家的注意力从"把事情做对"挪到"别写太多 / 别多读", 漏项与返工本身就是新的成本。
约束(只在有人把某个键写回去时才相关;写错不是静默生效,而是挂载时抛错):
- 两个 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 的安装指令」第 6 步):
- 三个旋钮行的结构:行必须在、
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。
(证据要求仍在各自那条「输出要求」里:结论要带 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 按用户要求追加;2026-09-30 补父级侧触发信号) | 已落地(机制实测 + 只读扫描量到 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 可直接续跑);"换人"只留给"它已无法接续"或"整段驻留期已厚、要结轮"(2026-09-30 补:父级那侧的触发信号逐字是结算通知开场白 Background subagent <id> ran out of room before it finished. —— @deepseek-ai/dsh-subagent/lib/types/continuation-messages.js:57-78 的 settlementSummary() 按 stopReason 分支,completed / aborted / refusal / error 各有一句、只有 max-tokens 是这句;被截断 ≠ 被终止,回一条继续消息它就能接着做)③ 规则 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 是两件事) |
| 8 份重复的"后台委派"提示段 | 框架侧,preset 改不了 | dsh-tool-subagent 给每个 continuable 委派行注册一段 systemPrompt 段落(lib/index.js 的 install() 里 systemPrompt.section({ name: 'tool:' + toolName … })),文本几乎相同、只差工具名 —— 本 preset 有 8 行 |
调度者系统提示里约 330 字符 × 8 ≈ 2.6 KB/请求(字符数可数,token 按 ~4 字符/token 估算约 0.66k,占 94.1M 的 <1%)。不要为了省这点删专家行;要修只能在框架侧合并成一段共享段落 |
压三组体积旋钮 / 设 maxTokens / 写"结论 N 字符内" |
不建议(已撤销的口径) | 截断会把工具已经取到的事实切掉;输出只占账单 1%,压它只损伤质量并招来返工 | 见 为什么撤销 preset 侧的体积闸门 |
persona 的体积账:prefix 正文(|- 之后的正文行、不含换行)现在 8005 字符。最新一轮
(2026-09-30 用户复核:两处新增"有必要这么长吗")把规则 7 的识别信号与规则 11 的捕获时刻两句按
"只留判据"再压一次 —— 样板前缀、子代理侧视角、同一事实的第二遍复述都删掉,判据与动作保留:
8126 → 8005(−121 字符 / −1.5%,≈−30 token/步);于是今天两轮对 persona 的净增回到 7972 → 8005(+33)。
上一轮
(2026-09-30 权限闸门补捕获时刻,用户要求)在规则 11 末尾补了"新权限只对切换之后新开的子代理生效、
切换后必须新建委派",7972 → 8126(+154 字符 / +1.9%,≈+38 token/步);同一轮的顶注第 5 条另加 4 行
源码补记(顶注不在这个口径里,故不计入)。再往前一轮
(2026-09-30 截断接续的父级侧信号,用户要求)在规则 7 里补了一句"结算通知的开场白就是被截断的触发信号",
7814 → 7972(+158 字符 / +2.0%,≈40 token/步),专家 persona 都没动(口径:正文行、含行首缩进、不含换行)。
再往前一轮(2026-09-30 全字段提示词压缩,按用户要求"从语言上缩减字数、降低 token 消耗、语义不能有任何损耗")
把它从 9082 压到 7814(−1268 字符 / −14.0%,按 ~4 字符/token 约 −317 token/步),九个专家
persona 合计 8940 → 8140(−800 / −9.0%),
两者合计 18022 → 15954 字符(−2068 / −11.5%);做法与"哪些没被压缩"见 preset/agent.cordis.yml 顶注的同一段(同义改写 +
删掉同一块内重复的复述;规则编号与全部规范性内容不动,仍没有任何 token/字数上限)。上一轮
(交付形态 + 截断接续:规则 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 / 7814 / 7972 / 8126 / 8005 是顶注第 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 / save-token,可选能力)
有两个外部插件往全局工具层注册工具,而它们给模型看的指令/通知只看自己的 config、不看这个请求有没有那些工具,
所以本 preset 要按"这个 profile 到底装没装它"决定给不给专家那几个名字。这样的一组东西在仓库里叫
构建期注入组(清单只有一份,写在 tools/flavors.mjs 的 INJECTION_GROUPS,见根 AGENTS.md 红线 10):
- 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})。 - save-token ——
dsh-plugin-save-token:在工具结果进入历史的那一刻把大输出换成[save-token #id] …通知,并注册save_token_expand让人把原文取回来。交界只有一处:工具可见性 (它不碰compaction-basic,也就不该动任何体积旋钮,见下)。
两组都由同一份探测、各自的旗标(--with-billion-context / --with-save-token,可叠加)决定,所以口径只有一条:
源文件中立、生成物按探测决定。味道键 = 装着的组的集合:
| 味道键 | 稳定目录($DSH_HOME/bundles/) |
注入 8 个专家行的工具名 | compaction-basic 的 config.auto |
|---|---|---|---|
plain |
dsh-adg-preset |
无 | 不写(这些 profile 里 dsh 自带的自动压缩是唯一的压缩手段) |
bili |
dsh-adg-preset-bili |
compress / decompress / search_context / acp_status |
false |
save-token |
dsh-adg-preset-save-token |
save_token_expand |
不写 |
bili+save-token |
dsh-adg-preset-bili-save-token |
上面五个名字 | false |
四份生成物的 package.json 逐字节相同、包名都是 dsh-adg-preset(所以 dsh.profile.bundles 那一行四种味道通用),
只有 cordis.patch.yml 不同;目录名不要写死,以 tools/flavors.mjs 的 dirNameFor(key) 为准
(味道键里的 + 换成 -)。
第一处交界,工具可见性:
toolFilter.allow是真白名单(见「设计要点」),专家只看得见 allow 里列出的名字;- bili 注入给模型的压缩指令与 nudge 只看自己的 config,不看这个请求有没有那些工具;save-token 同理 ——
它在工具结果进入历史的那一刻就把大输出换成
[save-token #id] …通知,通知正文点名save_token_expand(该插件lib/index.js:487),也不看接话的那位调不调得动。
两头凑起来的后果是:装了 bili 又不给专家那几个名字,专家会收到"去调 compress / acp_status"的指令,
工具目录里却没有它们(实测:调度者自己看得见,因为它是全局层;被裁的是专家)。反过来,把这些名字
手写进 preset/agent.cordis.yml,没装 bili 的人每一次委派都会当场抛 names unknown global tool "compress"
—— 名字不存在时 restrict() 直接抛,那一次委派就废了。
所以口径是源文件中立、生成物按探测决定:
node tools/gen-preset-bundle.mjs bundle/adg-plain # plain:不注入任何组
node tools/gen-preset-bundle.mjs --with-billion-context bundle/adg-bili
node tools/gen-preset-bundle.mjs --with-save-token bundle/adg-save-token
node tools/gen-preset-bundle.mjs --with-billion-context --with-save-token bundle/adg-bili-save-token
node tools/has-bundle.mjs ~/.dsh/profiles web # 逐组探测:一个组问一次,每 profile 一行 "<name>\t<0|1>"
node tools/has-bundle.mjs ~/.dsh/profiles web --package=dsh-plugin-save-token # 换一组问同一个 profile
node tools/resolve-flavor.mjs --billion-context --save-token # "<味道键>\t<稳定目录名>\t<gen 旗标>"
sh install.sh # 自动探测并决定旗标
install.ps1 / install.sh 对每个目标 profile、每个注入组各探测一次,探测的就是"这个环境里到底有没有装这个
插件":包名在该 profile 的 dsh.profile.bundles 里 且 装上的那份包里真有它自己的补丁文件(文件名从包自己
package.json 的 dsh.bundle.patch 读:billion-context 是 ./dsh.bundle.patch.yml、dsh-plugin-save-token 是
./cordis.patch.yml;读不到才退回历史名 dsh.bundle.patch.yml)。装了哪个组,才把这个组注册在全局层的工具名
注入 8 个专家行的 allow;没装就不注入 —— 两组都走这个口径,不是只对 bili。探测结果用
tools/resolve-flavor.mjs 翻成味道键,再决定这个 profile 拿哪一份:四个稳定目录
$DSH_HOME/bundles/dsh-adg-preset(plain)、...-bili、...-save-token、...-bili-save-token(四份
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 不带旗标 = plain,忘带旗标 = 少个能力,不会装坏。
本机(2026-09-30 复测):
web两组都装着,正确味道是bili+save-token(稳定目录dsh-adg-preset-bili-save-token);desktop/headless两组都没挂 ⇒ plain(dsh-adg-preset)。 实测:node tools/has-bundle.mjs C:\Users\cenqian\.dsh\profiles web→web<TAB>1;同一条命令加--package=dsh-plugin-save-token→web<TAB>1;node tools/resolve-flavor.mjs --billion-context --save-token→bili+save-token<TAB>dsh-adg-preset-bili-save-token<TAB>--with-billion-context --with-save-token。web现在链接的 还是旧的$DSH_HOME/bundles/dsh-adg-preset-bili,重跑一次install.ps1就会迁到...-bili-save-token。 这正是旧结构做不到的事:以前只有一份共享生成物,为了不让desktop每次委派撞names unknown global tool "compress",只能把web那份改成实体目录绕过"共享稳定目录 + 链接"模型, 代价是重跑install.*会把它打平回 plain、而web的专家就再也调不动compress(2026-09-28 用户报告的 就是这件事)。现在那个特例不需要了:web拿它该拿的那一份、desktop拿 plain,全都是链接。 复核:node tools/has-bundle.mjs "$DSH_HOME/profiles" web desktop,再加--package=dsh-plugin-save-token问一次 (web两个都是 1、desktop两个都是 0)+ 看web的链接目标 +node tools/check-bundle-flavor.mjs "$DSH_HOME/bundles/dsh-adg-preset-bili-save-token/cordis.patch.yml" bili+save-token。 目录名不要写死,以tools/flavors.mjs的dirNameFor(key)为准(味道键里的+换成-)。
注入的是 bili 的四个名字,不含 acp_cache:那份工具只读缓存经济学账本,是调度者诊断"这轮折叠值不值"用的,
并且它能用 conversation_id 代读子代理的账;给每个专家只会加长它们每次请求的稳定 prefix。
save-token 组的名字必须注入给专家(理由不同于 bili):dsh-plugin-save-token 在工具结果进入历史的那一刻
把大输出换成 [save-token #id] … 通知,通知正文写着
Call the save_token_expand tool with id "…", or read the FULL ORIGINAL at: <locator>(该插件 lib/index.js:487)。
被委派的专家确实会收到这种通知:tools/post-execute 钩子只跳过 exec.parent !== undefined 的执行,而
exec.parent 是 PTC/run_code 子派发的 token,普通子代理委派不设它。所以专家的 allow 里没有
save_token_expand,它就会去调一个不存在的工具 —— 注入的理由是"通知已经发给它了"。
save-token 与体积旋钮的职责划分(与红线 3 同一口径):它的入历史改写与内置 tool-result-pruner 动的是
同一格(都在"大结果进历史"这一步缩文本),两个都开等于在已经缩过的文本上再裁一道。但那三个体积旋钮
(compaction-basic / tool-result-pruner / tool-web)归插件出厂默认值管 —— preset 不去关那一行,
要不要把 pruner 行 disabled 是宿主 profile 自己的决定,生成物不管这件事。
第二处交界(只有 billion-context 组涉及;save-token 组不碰这个键):挂着 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 掉。只有 billion-context 组激活的味道(bili / bili+save-token)才写这个键;
plain 与 save-token 味道里绝不能出现它 —— 这两类 profile 里,dsh 自带的自动压缩是唯一的压缩手段,
关掉等于让上下文无限增长。
自检:先看这个 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)、
... dsh-adg-preset-save-token/cordis.patch.yml save-token(挂了 save-token 的)、
... dsh-adg-preset-bili-save-token/cordis.patch.yml bili+save-token(两组都挂的,本机 web 就是这档)
或 ... dsh-adg-preset/cordis.patch.yml plain(两组都没挂的)。它逐组断言:9 个 agent-* 行的
toolFilter.allow 里该在的组全有、不该在的组一个都不能出现(并拒绝 acp_cache),以及 compaction-basic
行的 config.auto 只在 billion-context 组激活时恰好是 false(其余味道断言这个键不存在)。再到新会话里委派
任一专家,让它报"工具目录里有没有 acp_status"(装了 save-token 的还该有 save_token_expand),并确认它没有
被 dsh 自带的自动压缩插过手(手动 /compact 仍应可用)。
两个方向都要测的完整口径见 docs/evidence.md §10「billion-context 的上下文工具对 ADG 专家可见吗」,
按 profile 分味道的落点、这次真机缺陷与修法则见 §11。
目录结构
preset/
preset.yml # 在模式选择器里显示的名称与简介
agent.cordis.yml # 调度智能体 persona + 八个专家智能体行
skills/
adg-add-agent/
SKILL.md # 「给 Adg 加一个智能体」的操作手册
tools/
check-preset.mjs # 静态自检:专家行字段、toolName 唯一、allow 合法性、
# 通用委派行、调度名册与专家行双向一致,以及三组
# 体积旋钮所在行的结构与"被写回时的合法性"(不钉死取值)
gen-preset-bundle.mjs # 构建脚本:preset/ 三份源文件 → bundle/adg-<味道>/(生成物,gitignore)。
# --with-billion-context / --with-save-token(可叠加)= 把该组注册在全局层的名字
# 追加进 8 个专家行的 allow;billion-context 组还要给 compaction-basic 注入
# config.auto=false(都只改生成物,见红线 10)。不带旗标 = plain
check-bundle-flavor.mjs # 产物自检:按 plain|bili|save-token|bili+save-token 逐组断言 —— 8 个专家行的 allow
# 里该在的组全有、不该在的组一个都没有(并拒绝 acp_cache),compaction-basic 的
# config.auto 只在 bili 组激活时恰好为 false
# (check-preset.mjs 只看源文件,产物是它的盲区)
flavors.mjs # 构建期注入组的单一事实来源:组表 / 组顺序 / 味道键与稳定目录名的拼法 / 探测判据
has-bundle.mjs # 逐组探测:node tools/has-bundle.mjs <profilesDir> <profile...> [--package=<包名>]
# 每 profile 一行 "<name>\t<0|1>";一个组问一次(缺省 billion-context,换组用 --package),
# install.* 用它决定该 profile 拿哪份味道
resolve-flavor.mjs # 映射(不探测):--<组> → "<味道键>\t<稳定目录名>\t<gen 旗标>"
browser/ # 浏览器工具链(有头 Chromium 系浏览器 + 最小 CDP 驱动,零依赖,Node >= 22)
cli.mjs # 唯一入口:launch / status / tabs / profile / open / text / eval / shot / close-tab / close
lib/target.mjs # 纯函数:profile / 端口 / 浏览器探测(Chrome → Brave → Edge)/ 启动参数 / 复用决策
lib/cdp.mjs # 最小 CDP 通道 + 会话便捷层(socketFactory 可注入,便于无浏览器测试)
test/browser.test.mjs # 37 个单元用例(不需要浏览器)
AGENTS.md # 模块路由:命令、模块特有红线、跨模块路由、生效方式
design.md # 对象设计:BrowserTarget / BrowserInstance / PageSession / PageTab 与 I1..I10
testing-guide.md # 不变量→用例全表、三个状态机迁移矩阵、消费侧契约、未观测清单
install.ps1 # Windows 安装脚本(preset bundle + 技能 + browser 工具链)
install.sh # macOS / Linux 安装脚本(同上,行为等价)
兼容性
- 从 DSH 出厂 preset
standard(标准模式)复制而来,实质改动是十三处:persona增加调度名册与分派规则;delegation组由通用委派行换成专家行;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§8。- 人工介入也只能由调度者转达:
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 挂载机制决定的,不是缺陷。
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而不可用 —— 装完必须按第 6 步做真实挂载校验。实测依据:docs/evidence.md§9。
git clone <repo-url> <tempdir>- 生成 bundle(四种味道都生成,或只生成这个 profile 该拿的那一份):
node <tempdir>/tools/gen-preset-bundle.mjs bundle/adg-plain(plain;--with-billion-context= bili、--with-save-token= save-token、两个旗标叠加 =bili+save-token,见红线 10),输出目录是位置参数;不传位置参数才落到缺省<tempdir>/bundle/adg-preset/,两种写法别混。它读preset/preset.yml(显示元数据)+preset/agent.cordis.yml(整个插件条目列表)+preset/bundle.package.json(包清单模板),写{cordis.patch.yml,package.json}。bundle/是构建产物(在.gitignore里),任何情况下都不要手改生成物 —— 要改就改preset/下的源文件再重跑;install.ps1/install.sh每次安装都会按探测结果把需要的味道重跑一遍。 目标 profile 装着哪个注入组,就装哪个味道的产物(手工装的时候别只装 plain);生成完顺手node <tempdir>/tools/check-bundle-flavor.mjs <那份文件> <味道键>验一遍。 - 装 bundle:用
plugin_manager的install_bundle,target给 bundle 目录的绝对路径 —— 包安装与dsh.profile.bundles选中由它自己完成,不要用 shell 命令复刻这两步。 想让"仓库被删/被挪也不影响已装好的 dsh",先把生成物整个拷到它自己的稳定目录(plain →${DSH_HOME:-~/.dsh}/bundles/dsh-adg-preset/,注入版 →.../dsh-adg-preset-bili//...-save-token//...-bili-save-token/;目录名不要写死,以tools/flavors.mjs的dirNameFor(key)为准),再拿那个 稳定目录当target(install.ps1/install.sh就是这么做的)。每个 profile 只装它该拿的那一份 (拿不准就先跑node <tempdir>/tools/has-bundle.mjs "$DSH_HOME/profiles" <profile>逐组问一遍,再用node <tempdir>/tools/resolve-flavor.mjs --<组> …翻成味道键),不要把多份都link:进同一个 profile。 装完list_bundles里应能看到dsh-adg-preset。 - 复制
<tempdir>/skills/adg-add-agent/SKILL.md→${DSH_HOME:-~/.dsh}/skills/adg-add-agent/SKILL.md - 目标目录通常在工作区之外,写入会被沙箱拒绝一次;按提示用
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必须出现 8 条启用的专家行:agent-file、agent-computer、agent-browser、agent-search、agent-researcher、agent-coder、agent-reviewer、agent-general,且没有tool-subagent-fork行。判据要写准:compositionInventory()报的是模块名,所以@deepseek-ai/dsh-tool-subagent会出现 10 次 (上面 8 条 +tool-subagent-codex/tool-subagent-claude-code这两条enabled: false的), 按"模块名出现 8 次"去断言会误报失败 —— 名册 9 行时本机实测就是这个 11/9/0 的形状 (加第 9 个专家之前是 10/8/0)。2026-09-28 实测的完整形状(当时名册 9 行):broken为空、adg共 35 行、32 行启用且fiberState === 2(真挂载)、3 行关闭 (tool-bash与两条 codex/claude-code)、9 条@deepseek-ai/dsh-tool-subagent启用、 fork 0 条、引擎行workflow-ptc处于挂载态。2026-10-01agent_app并入agent_computer后未重测行数:按算术应各少一行 ⇒ 34 行 / 31 行启用 / 3 行关闭、模块名 10 次; 引用行数前请先重测,resolve('adg').broken为空这一条与名册行数无关、仍必须成立。- 也可以直接
node tools/check-preset.mjs(零依赖,exit 0 表示通过;现在只有仓库这一份文本, 不再有"已安装的第二份"可以传路径)。注意它只是文本扫描器:exit 0 不等于"文件能解析、 插件已挂载",所以上面那条真实挂载的校验不能省。 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" 那段指引。
- 明确告诉用户:preset 改动仍按"重启 dsh + 新会话"验收(bundle 层不是只在启动时读 ——
实测 profile 的
cordis.patch.yml或 profile 清单变动会触发整份 patch 栈重读、并让声明重新注册, 但已挂载的会话不会中途换组合,所以验收口径不变)。重启后在新建对话里选择「Adg 多智能体模式」。 - 如果用户还需要在创造模式里说「给 Adg 加一个智能体」被识别,确认第 4 步的技能已就位——
<dshHome>/skills是dsh-skill-filesystem的用户技能根(rank 400),两种模式都会扫描且热加载。
No comments yet. Be the first to write one.