zhuto666/dsh-compact-agents

dsh-compact-agents

模型可调用的 compact_agents 工具:强制压缩进程内所有活会话的上下文(主会话、普通子代理、AgentTeams 成员一视同仁),忙的目标自动排队、本轮结束立即补压,逐目标回报被遮蔽的节点数与估算 token 数;浏览器侧在设置里自成一页,提供触发阈值、保留比例、受控阶段输出预算、自动续写次数与「回合结束预压」比例的编辑表单(一轮结束时上下文已接近触发线就先压一次,避免下一轮回答开头空等)。

Uncategorizeddeepseek-harnessdsh

dsh-compact-agents

DeepSeek Harness 会话上下文强制压缩插件:一个模型可调用的 `compact_agents` 工具 + 一块设置界面里的参数表单

强制压缩忽略自动阈值 · 覆盖进程内所有活会话(主会话 / 普通子代理 / AgentTeams 成员) · 忙的目标自动排队、本轮结束立即补压 · 逐目标回报遮蔽节点数与估算 token 数 · 压缩过程在对话区可见 · 一轮结束时提前压缩,回答开头不再干等 · 被输出上限截断时自动续写 · 参数在设置里自成一页直接改 · 零网络、零依赖、无构建步骤

English | 中文

安装

需要 Node.js ≥ 20 与带 agent-presets 的 DeepSeek Harness。源码检出与桌面版(打包在 `app.asar` 里)都支持;桌面版的 profile 由 Electron 独占管理,插件请从「侧栏 → 插件」或市场装。

装包与"把挂载行写进 preset"是两件事 —— compact_agents 工具必须挂在 preset 的 compaction 隔离组里才拿得到 ctx.compaction,这一行无法由插件包自带。

平时只要装包就行:插件第一次启动时会自己把挂载行补齐(lib/self-heal.js,每次启动检查一遍、缺什么补什么,补完用 hmr 热重组,新开会话即可用)。所以从社区更新版本这一件事就够了,不需要再手跑任何脚本。

# 官方通道装包:登记 profile 依赖与 bundle(浏览器 half,即设置里那张表单)
dsh plugin --profile web add github:zhuto666/dsh-compact-agents

--profile web 换成你实际启动的 profile 名;dsh plugin 把其余参数原样转发给该 profile 目录里的 pnpm。桌面版直接在插件页点安装即可。

想让自愈立刻跑而不是等下次启动(或想先看差异再决定),跑离线版:

# 幂等;--dry-run 只预览不落盘
node <profile-dir>/node_modules/dsh-compact-agents/scripts/install.mjs

是该 profile 的目录,默认为 ~/.dsh/profiles/。~ 只在 macOS、Linux 与 Git Bash 里展开,这几种环境可以照写;Windows 的 PowerShell 与 cmd 不展开它,要写完整路径,如 .dsh\profiles\web\node_modules\dsh-compact-agents\scripts\install.mjs`。

从本地 checkout 安装(开发本仓库时):

# 在仓库根目录执行;add 后面必须写绝对路径 —— dsh 会切到 profile 目录里执行 pnpm
dsh plugin --profile web add /absolute/path/to/dsh-compact-agents
node scripts/install.mjs

install.mjs 是幂等的,按安装形态做不同的事:源码检出上依次是建 node_modules/@deepseek-ai/{dsh-tools,cordis,schemastery} 三个联接 → 覆盖 preset → 登记 profile 依赖与 bundle → 调官方通道 dsh plugin add(--no-cli 跳过);桌面版上没有可指的检出、profile 也被 Electron 独占,于是跳过联接与 dsh plugin add,只做 preset 覆盖与登记。preset 一份都没找到时,用 --preset 显式指定。

preset 挂载行写的是绝对路径:preset 里的裸包名是从 harness 安装位置解析的,指向用户目录的包根本解析不到。项目移动或改名后自愈会按当前安装位置改写这一行(也可以重跑 install.mjs)。

它解决什么问题

DSH 的手动压缩入口只有一个人机命令 /compact:

  • headless 的子代理 / AgentTeams 成员没有命令面,执行不了 /compact;
  • 也没有任何模型侧工具能替别的会话压缩 —— packages/compaction/ 下没有一个 registerTool;
  • 自动压缩(compaction-basic)按阈值触发:阈值没到就不压。

因此"会话越跑越贵"过去只能靠换人(退役成员、新建成员)解决。本插件补上缺失的模型侧入口。

实测数据:一个成员会话每次调用重发约 40 万 tokens 的上下文,跑到 348 次调用、累计 1.4 亿 cacheRead、¥10.87 —— 它的成本与它是否被压缩直接相关。

装完请重启一次 dsh

宿主组成变了(profile 里多了一个 bundle),而客户端模块的 rev 只在 dsh 启动时计算;不重启,设置里的表单不会出现。

首次安装有一个次序问题:若要让现有的成员 / 子代理也被压缩,就不要急着重启 —— 重启会丢弃所有活会话(ctx.agents.list() 只覆盖活着的会话)。先压完再重启,或者重启后重开成员。compact_agents 工具本身不依赖这次重启:preset 行装好后,新开一条对话即可使用。

之后只改 preset 里的参数不必重启(见下面的生效时机)。

怎么用

模型侧调用,不是人机命令:

compact_agents(scope = "others" | "all" | "self" | "ids", ids?: string[], whenBusy = "queue" | "skip")

scope · 含义 others(默认) · 除调用者以外的所有活会话 all · 所有活会话,含调用者自己 self · 只压调用者自己 —— 没有子代理时也有效 ids · 只压 ids 里列出的会话

对应说法:「把其他会话都压一遍」→ others;「我这条对话也一起压」→ all(调用者自己会被排队,本轮结束补压);「只压我自己」→ self。

返回每个目标一行:

compact_agents: 3 compacted, 1 queued, 0 skipped, 0 failed (of 4 selected).
- sess_ab12: compacted, ~213,400 → ~49,800 tokens — shadowed surface 12-107
- sess_ef56: noop, ~0 tokens shadowed — nothing safely compactable (empty session, or one oversized retained unit)
- sess_gh78: queued — mid-turn; queued and will be compacted as soon as it goes idle

beforeTokens / afterTokens 是用 ctx.tokenMeter 实测的表面估算;测不到时为 -1。

行为边界:只有顶层 agent 能扫描他人(子代理只能用 scope: "self");每个目标必须 idle 才能立即压缩(忙的按 whenBusy 排队,skip 则直接报 busy);一次扫描串行执行,超时 30 分钟(排队那部分在 turn 之后跑,不计入)。

工具描述里已写明"用户要求压缩上下文时立即调用、不要反问",模型仍可能先确认一次。点名调用必定执行:

调用 compact_agents,scope=all,把所有会话压一遍

压缩会在对话区留下可见提示

任何压缩(包括阈值触发的自动压缩)都会在对话区留下一行提示;compaction/start 是在摘要模型调用之前写的,因此它正好盖住原本不可见的等待:

▸ 上下文注入 · dsh-compact-agents · 正在压缩上下文…(当前 213,400 tokens) · 触发线 ×0.35
▸ 上下文注入 · dsh-compact-agents · 上下文压缩完成:约 213,400 → 49,800 tokens,已遮蔽 37 个历史节点

末尾的 触发线 ×0.35 是本会话实际生效的值;只有热同步确实进不去时才会多一行说明两个值与出路:

⚠️ 预设文件里现在是 ×0.5,本会话这个实例仍按 ×0.2(热同步没成功):新开一条对话才会用上新值。

用行配置 notice: false 关闭。提示本身是一条 user/message,模型下一轮也看得到 —— 这是有意的,让模型知道上下文刚被压过。见设计说明 §6。

被输出上限截断时会自动续写

一轮以 turn/end{reason: 'max-tokens'} 结束时,插件替用户发一句"继续"(agent.followup,与人在界面上发言同一条路径),让对话继续走下去:

▸ 上下文注入 · dsh-compact-agents · 上一轮被输出上限截断,已自动续写(1/2)
继续                                    ← 插件以用户身份发出的(与用户手打的一样)

次数上限由 maxAutoContinues 控制(默认 2,0 / false 关闭),用满后改为提示,避免无止境消耗 token;任一轮正常结束即清零,所以额度是"连续"次数。为什么需要它:压缩后 preset 常把下一个请求的输出预算压得很小,若模型开着高推理,思考 token 与正文共享这份预算、很容易整份被吃光 → 正文 0 字被判截断(设计说明 §7)。

回答开头不再干等:回合结束预压

DSH 的自动压缩挂在每个模型请求之前判定(agent/pre-step):占用一到达触发线,摘要模型就要在这一次请求的第一个 token 之前跑完。而这个判定点在你的新消息已经进了会话之后,所以最常见的那种"上一轮结束时差一点点、新消息一进来正好跨线"的情形,表现成回答开头卡着不动,几秒到几十秒后才开始出字。

本插件把判定点前移一个回合:一轮结束时占用已达触发线的 90%(新参数 preemptiveRatio)就先压一次。摘要耗时于是落进"回答已交付、你正在读"的空档里,下一轮直接开跑;达线的兜底仍归 DSH 自己,预压没触发或没压动都不影响正确性。对话区会标明这一次是预压:

▸ 上下文注入 · dsh-compact-agents · 正在压缩上下文…(当前 190,000 tokens) · 回合结束预压

三件事如实说明:

  • 延迟是被搬走,不是被消灭:回答完立刻追问,等待照样发生 —— 等的是上一轮的尾巴,而不是这一轮的开头。
  • 只覆盖一半情形:如果跨线是你的新消息自己造成的(上一轮结束时离得远、你贴了一大段),预压当时不在带内,回答开头仍会卡;要提前判断你还没发的那条消息,做不到。
  • 可能多花一次摘要费:默认贴在线附近(0.9)才动手,所以只有"已经快满了"的会话会提前压;压不动的会话(单个超大保留单元那种 noop)会退避,不会每轮白试。

不想用它:设置里把 回合结束预压比例 设成 0,或行配置 preemptiveRatio: 0。机制与取舍见设计说明 §10。

在界面里改这些参数

首选入口是设置里自成一页:打开设置 → 侧栏「压缩与自动续写」,它与「通用设置 / 模型 / 内置插件 / Agent 预设」同级(DSH 0.1.5 与 0.1.6 都在)。另外两处是同一份表单、同一个控制器:DSH ≥ 0.1.6 侧栏「插件」页里本包的详情页;DSH ≤ 0.1.5 的设置 → 插件 → 可配置折叠卡片。

图里「设置」左栏能看到它与「内置插件」同级。图中数值是实拍界面上该 preset 的现值(两张比例带「已覆盖」标记),不等于出厂默认 —— 默认值见下面表格。DSH ≤ 0.1.5 的老卡片长什么样,见 docs/images/settings-card-annotated.png(仅存档)。

这个表单是纯参数表单:插件在界面上没有自己的按钮,压缩与自动续写都是自动发生的;框架自带的「重置」只是还原入口。DSH 里唯一的手动压缩入口是自带的人机命令 /compact(不是本插件)—— 本插件提供的是模型侧工具 compact_agents。

字段名与生效时机 —— 界面按生效时机分成三组,组标题就是下面这三行:

立即生效 · 动作发生时读取,改完下一次压缩或续写即生效。

字段 · 含义 · 取值 压缩提示播报 · 压缩完成后是否在对话区播报遮蔽节点数与估算 token 数,默认开启 · 下拉 开启 / 关闭 自动续写次数上限 · 回复被输出上限截断时自动续写的次数上限,0 表示关闭,默认 2 · 下拉 0 1 2 3 5 10 回合结束预压比例 · 一轮结束时占用已达触发线的这个比例就先压一次,把摘要耗时挪进回答之后的空档;0 表示关闭,默认 0.9 · 0 – 1 的数(建议 0.8 – 0.95)

写入预设并热同步 · 写进预设配置,同时热同步给正在运行的会话,下一次步边界即生效。

字段 · 含义 · 取值 压缩触发阈值比例 · 上下文占用达到窗口的这个比例时触发压缩;默认 0.35,即窗口 100 万 tokens 时约在 35 万处触发 · 0.05 – 0.95(DSH 出厂值是 0.8,1M 窗口 ≈ 800K,实际等于几乎不触发) 压缩后保留比例 · 压缩后保留的上下文比例,越小压得越狠;默认 0.05,即窗口 100 万 tokens 时至少留 5 万 tokens 的原文(按整条消息对齐,实际会略多) · 0.01 – 0.5

新建会话生效 · 写进预设配置,只在新会话读取,已在运行的会话不受影响。

字段 · 含义 · 取值 受控阶段输出预算 · 压缩后那一小段"受控阶段"里,单次请求的输出 token 预算;它是输出预算,max_tokens 把思考(reasoning)token 也算在内,所以 1024 时可能光思考就吃满、正文一个字都出不来 · 整数,1024 – 200000

表单还会标出哪些字段是你覆盖过的,每个字段旁的「重置」回到预设里的值。

调参速查(机制与取舍见设计说明 §9):

你想要的 · 怎么调 更省钱、更快 · 阈值调小(早压)+ 保留比例调小 少打断、记得住刚说的 · 保留比例调大(0.08~0.1) 受控阶段老被截断(反复自动续写) · 输出预算调大(32768) 压缩太频繁、嫌它老在总结 · 阈值调大(0.3~0.4) 回答开头老要等压缩 · 保持预压开启,比例调低(0.8,更早动手;0 关闭)

行配置 notice: false / maxAutoContinues: 0 / preemptiveRatio: 0 / livePresetParams: false 分别关掉提示、自动续写、回合结束预压与热同步;settings: false 整体关掉设置面(工具与提示不受影响)。

排错

下表的 scripts/install.mjs 指插件安装目录里的同名脚本:源码 checkout 是 scripts/install.mjs,从 GitHub 安装的是 /node_modules/dsh-compact-agents/scripts/install.mjs。

症状 · 原因 · 处理 设置里没有「压缩与自动续写」 · 装完没重启 dsh;客户端模块的 rev 只在启动时算 · 重启一次 dsh 表单能打开、但 compact_agents 工具不在、压缩也不播报 · preset 覆盖块没写进当前 profile(多 profile 并存时最容易:覆盖写去了另一个 profile) · 插件每次启动会自愈;看日志里 compact-agents: 已自动修复 preset 覆盖。仍不生效就跑一次 scripts/install.mjs,再重启 桌面版(E:\DeepSeek 那种打包安装)里完全不生效 · 随包 preset 封在 resources/app.asar 里,旧脚本按源码检出找不到它 · 已修(0.10.0 起脚本与自愈都能读 asar);升级插件版本后重启即可 表单在,但侧栏「插件」页里整条看不到本包 · 包名只写进了 dsh.profile.bundles、没写进 profile dependencies —— 新版插件页只列 installed \ · \ · optional \ · \ · error 的 bundle,且毫无报错 · 重跑 scripts/install.mjs(两处都写),再重启;validate-presets.mjs 会把这种半装状态判成 FAIL 界面上既没有命名空间也没有表单,且毫无报错 · 只挂了 preset、没登记成 profile bundle —— 浏览器根本收不到 lib/client.js · 同上(§8.4) 老设置页(DSH ≤ 0.1.5)里卡片凭空消失,也不报错 · 老槽位 settings.plugin.item 在 ≥ 0.1.6 已无渲染方;slots.inject 对没人声明的槽位是静默的,注册上去既不抛错也不显示 · 用「设置 → 压缩与自动续写」或插件页详情页,不要依赖老槽位(§8.5) 预设文件里明明是新阈值,会话仍按旧值压 · 热同步没进去(配置形状变了/写不进去);提示里会同时报出两个值 · 新开一条对话才会用上新值(§6) 压缩提示里出现"新开一条对话才会用上新值" · 自我反证发现引擎仍按旧阈值判(证据是下一次由策略自己决定的压缩) · 同上 升级 @linxin666/* 之后 compact_agents 工具没了 · 随包发行的 preset 被它自己的升级整份重写,我们的挂载行丢了 · 插件启动时自愈会补回;也可重跑 scripts/install.mjs,validate-presets.mjs 会先告诉你是哪一份 自己改的 preset 不生效 · 同 id 的发行 preset 会遮蔽用户 preset(agent-presets 的 roots 顺序是随包最先、用户最后) · 改随包那份(自愈会保留你手工追加的行与调过的阈值),或给自建 preset 换个 id 插件页里关掉本插件,compact_agents 却还能用 · 那个开关只动宿主组成里的 bundle 行,工具由 preset 行提供 · 要整体停用:跑 scripts/uninstall.mjs

更新 / 卸载

更新就是装新版本这一个动作:插件启动时自愈会把 preset 覆盖刷新到与新随包 preset 一致(新预设、新插件行、以及你调过的阈值都照顾到),补完热重组,新开对话即生效。

# 源码 checkout
git -C <checkout> pull && node scripts/install.mjs

# 从 GitHub 安装
dsh plugin --profile web update dsh-compact-agents

# 卸载(先 --dry-run 预览)
node <插件目录>/scripts/uninstall.mjs

卸载只删自己加的东西:preset 里的注释、!!js 表达式、其它行一律不动(实测安装 → 卸载后文件逐字节回到原状),profile package.json 按行摘除、保留原排版。插件目录与 .bak / .bak-compact-agents 备份不删,自行处理。卸载同样改了宿主组成,重启 dsh 后表单才消失。

深入设计

正文只留结论;根因、契约出处、踩过的坑与验证矩阵都在 docs/design.md:

想知道的 · 看 为什么 DSH 没有现成的模型侧压缩入口 · §1 覆盖范围、事件语义、忙碌与排队 · §2 挂载与路径解析、ESM 模块缓存等坑 · §3 验证矩阵:每个脚本断言什么 · §4 与自动压缩(compaction-basic)的关系 · §5 压缩提示与阈值热同步 · §6 输出上限截断与自动续写 · §7 设置面:宿主组成那一行(§8.4)、两处静默失败(§8.5)、已知限制(§8.6)、为什么自成一页(§8.7) · §8 压缩参数的机制与调参取舍 · §9

代码结构:index.js(preset 行入口:注册工具 + 会话监听)、client-host.js + cordis.patch.yml(宿主组成行入口:下发浏览器 half + 注册设置命名空间 + 排定自愈)、settings.js(设置命名空间与 preset 参数读写)、lib/self-heal.js(启动自愈:把挂载行补进当前 profile 的 preset 覆盖并热重组)、lib/client.js(浏览器 half,手写单文件、无构建步骤,界面文案的唯一权威)、scripts/*(安装 / 卸载 / 校验 / 测试)。