dsh-if
tlewa/dsh-if
Adds an /if slash command to DSH: the first line is a condition and the remaining lines are an instruction. A fork subagent that inherits the session tools, system prompt and message history judges the condition and answers YES or NO, and only on YES is the instruction sent to the main agent as an ordinary user message, while a NO sends nothing. The condition, judge prompt and verdict never enter the main transcript. While the agent is busy the command queues with ordinary messages, editable, removable and steerable from the queue panel, and a setting switches to the 0.2.x background mode. The judge runs read-only, and falls back to a one-shot model call over a plain transcript when the host has no context-inheriting fork provider. Supports DSH 0.2.1-rc.5 and later (verified on 0.2.1-rc.5 and 0.2.2-rc.1) and Node.js 20.3 or later.
Installazione
dsh plugin --profile web add github:tlewa/dsh-ifREADME
dsh-if
给 DeepSeek Harness(DSH)加一个自定义 /if 命令:
先判断条件是否成立:判断器是一个 fork 子代理,继承当前会话的工具、system prompt 与消息历史, 只在末尾追加一条判断指令;成立时,把下方的指令作为一条普通用户消息发给主 agent;不成立时什么都不做。
判断请求与主会话的下一次请求共享同一个前缀(tools → system → messages 逐字节一致),
因此能复用 provider 的 prompt cache / KV cache。/if 命令、条件、判断用的 prompt 和判断结果
都不会写进主会话;条件成立时,主会话只在历史末尾多一条普通用户消息,不会改写已有前缀。
功能
| 能力 | 行为 |
|---|---|
| 上下文条件判断 | 判断器继承当前会话的完整上下文(工具 + system prompt + 历史消息),按条件回答 YES 或 NO |
| 复用 KV 缓存 | fork 子代理只追加一条尾部 user 消息,tools / system / messages 与主会话逐字节一致,命中 provider 前缀缓存 |
| 只读判断 | 工具目录保持不变(这正是缓存命中的前提),执行期 guard 只放行 read / glob / grep 等只读工具;写入、命令、子代理一律拒绝 |
| 与普通消息同队列 | 模型工作时,/if 作为一条普通消息进入输入框上方的队列面板:可以编辑、删除、↑ 插话;回合开始时才判断,成立就把这条消息原地换成指令 |
| 两种排队模式 | 默认 message(与普通消息同队列);background 保留 0.2.x 的行为:命令立即返回,判断在后台跑 |
| 条件成立才发送 | 只有 YES 才把指令作为用户消息发给主 agent,NO 完全不触发主 agent |
| 判断不进上下文 | 判断在独立子会话里完成,不写主会话事件,不调用 agent.inject |
| 命令不泄露参数 | recordInput: false,log-only 的 command/run 也不记录条件或指令 |
| 多行条件 | 第一行是条件,其余行是指令;条件有多行时用单独一行 --- 分隔 |
| 自动回退 | host 没有可继承上下文的 fork provider 时,回退到一次性 ctx.llm.stream + 约 24k 字符纯文本记录 |
| 防止误发 | 条件为空、指令为空、判断输出不是 YES / NO、或判断超时,都返回错误且不发送 |
安装
要求:
- DSH
0.2.1-rc.5或更新版本; - Node.js
20.3或更新版本(使用AbortSignal.any/AbortSignal.timeout)。
从 npm 安装(推荐)
-
打开侧边栏 插件 页 → 安装插件;
-
输入包名并确认:
dsh-if -
安装完成后重启 DSH(host 插件在 profile 启动时加载)。
固定版本:
dsh-if@0.3.4
命令行等价写法:
dsh plugin --profile desktop add dsh-if@0.3.4
从 GitHub 安装
lib/ 是可直接运行的源码,不需要本地构建:
https://github.com/TLEWA/dsh-if
固定到标签或 Release 附件:
dsh plugin --profile desktop add github:TLEWA/dsh-if#v0.3.4
https://github.com/TLEWA/dsh-if/releases/download/v0.3.4/dsh-if-0.3.4.tgz
从源码安装
git clone https://github.com/TLEWA/dsh-if.git
cd dsh-if
npm test
dsh plugin --profile desktop add .
重启 DSH 后,在输入框输入 /if,斜杠面板会出现命令。
用法
基本形式:第一行是条件,其余行是指令。
/if 1+1=2
请只回复 OK
条件本身有多行时,用单独一行 --- 分隔条件和指令:
/if 用户已经确认方案 A
并且测试已经通过
---
请开始执行方案 A
也可以只写一行条件和一个单行指令;/if 后面的第一个换行就是分隔点。
分隔符优先:只要出现过单独一行的
---,它右边的部分一定全是指令。所以指令正文里 不要单独成行写---(Markdown 水平线),否则它前面的内容会被当成条件的一部分。
判断语义
- 判断由当前会话配置的模型完成,不是 JavaScript 或数学求值器。
1+1=2只是最小示例;条件可以是“用户是否已经确认方案”“上一条回复是否包含错误”等。 - 判断器只被要求输出
YES或NO。输出里没有可识别结果时按判断失败处理,不发送指令。 - 判断器可以只读地查看文件、检索代码来确认事实(
read/glob/grep等),但不能做任何修改。 - 条件需要的信息在当前上下文里找不到、或者条件含糊无法确定时,判为不成立:宁可不动,也不误发指令。
- UI 只显示通用结果,不回显条件、指令或判断原文。
配置
# profile 的 cordis.patch.yml
- id: if-command
name: 'dsh-if'
config:
mode: message # 默认。background = 0.2.x 的老行为
mode: message(默认):/if先作为一条普通用户消息排队,等轮次真正开始时再判断。 条目的内容是/if <条件>+ 指令原文,出现在输入框上方的队列面板里,编辑它即等同改条件与指令。mode: background:0.2.x 行为。命令立即返回“已排队”,判断丢给后台任务,判断过程不占队列。 适合不想让判断占用一轮队列位置的场景。判断失败时会在会话里记一张if命令卡片说明原因 (后台模式没有排队条目可以改写)。
也可以不改文件:插件 → dsh-if → 配置 里有「判断方式」下拉框,切换即时生效(mode 声明为
volatile 字段,设置页直接改写运行中的配置)。
插件导出 Config(@deepseek-ai/schemastery)描述这个字段;mode 缺省即为 message,
未知字段被忽略。
工作方式
- 通过
ctx.effect(() => ctx.commands.register(...))注册/if,并设置recordInput: false; 注册挂在插件的 fiber 上,停用时一并注销,重新启用不会撞名。 - 入队。
message模式下,命令立即把原文/if <条件>\n<指令>作为一条普通 user 消息agent.followup()到会话队列:它进的是宿主 durable inbox,所以客户端排队面板照常显示、 照常可编辑 / 删除 / 插话,输入框也不再被命令占住。 - 轮次边界拦截。 回合开始时宿主按
agent/pre-stepwaterfall 领取这批消息;插件拿到 claim 到的 批次后,把以/if开头的条目挑出来:/if原文不会进入模型历史,而是先在原地判断—— 成立就把这条消息的 content 换成指令,不成立就从批次里删掉。判断失败(超时、输出不可解析、 子代理起不来)时,这条消息同样从批次里删掉,并在会话里记一张【/if 未执行】的错误命令卡片 (command/run+command/done,log-only,不进模型请求):说明失败原因,并保留原始输入供 点失败卡片右侧的「取回 /if」一键放回。卡片记在这一轮结束之后,所以不会被折进收起的 turn-process 里;批次因此可能变空,宿主会直接结束这一轮 —— 判断失败的/if不会给主线模型发任何请求,也不会凭空消失。 - 首选:fork 子代理判断。 通过
ctx.subagents.getProvider('fork')取 provider(要求inheritsParentContext === true),再用startActivation({ provider: 'fork', childId, request: { parent, agentOptions, prompt }, delivery: 'caller' })启动判断子会话。请求里只有一条尾部 user 消息(判断规则 + 条件);tools、system prompt、历史消息全部原样继承, 前缀与主会话下一次请求逐字节一致。 - 丢掉继承来的待办。 fork 的种子是父会话到最后一个
turn/end为止的全部事件,而 turn 结束前 入队的 next-turn 消息、以及还没被领走的 next-step 插话也落在里面。子会话会把它们当成自己的任务 先跑一遍(判断结果就会错位到别的回合,插话甚至会变成判断子会话的执行内容)。所以拿到子 agent 的那一刻,插件把子会话 inbox 里nextTurn和nextStep两条队列中除自己判断 prompt 以外的 待办全部remove掉;父会话的队列不受影响。 - 执行期只读限制。 子会话继承完整工具目录,但
ctx.tools.guard注册的 guard 只对判断子会话生效:read/glob/grep/web_search/web_fetch/skill等只读工具放行, 写入、修改、执行命令、子代理等一律返回拒绝文案。工具目录不裁剪,所以缓存前缀不变。 - 判断超时 60 秒;只有
YES时把指令作为一条带id和role: 'user'的已识别消息交给主 agent;NO时这条排队消息被丢弃,完全不触发主 agent。 - 回退路径。 host 没有可继承上下文的 fork provider 时,
historyTranscript()读取agent.session.deriveMessages(),只保留 user / assistant / tool 消息,生成最多约 24k 字符的纯文本记录 (每条消息最多 3k 字符,保留头尾),再用ctx.llm.stream()发起一次性判断请求。 该请求不带主会话sessionId,因此命中不了主缓存,只作为兼容退路。 background模式跳过第 2、3 步:命令把判断交给后台任务并立刻返回“已排队”, 判断结果直接以agent.followup交付(成功是指令;失败是一张【/if 未执行】命令卡片)。 多个/if按发送顺序串行判断,不会因为同一时刻被唤醒而乱序。
KV 缓存与上下文隔离
这是设计约束,不是可选项:
/if命令、条件、指令、判断 prompt、判断结果都不写入主会话;command/run和command/done是 log-only 事件,不进入模型请求。- 判断在 fork 子代理里完成:tools、system prompt、历史消息全部继承,只在末尾追加一条 user 消息。 因此判断请求的前缀与主会话下一次请求逐字节一致,provider 的 prefix cache / KV cache 可以直接复用, 不会因为一次判断而让整段上下文重新计费。
- 判断子会话是独立会话,不追加主会话的 session 事件,只读历史;工具目录也不裁剪 (裁剪工具定义会改变前缀,反而破坏缓存),只读限制放在执行期 guard。
- 代价是透明可见的:每次判断都会把整段上下文重发一次,token 计数按全量算,命中缓存的部分 按 provider 的缓存价计费。会话越长,单次判断的绝对 token 数越大(缓存命中率决定实际花费)。
- 条件不成立(
NO)时,主会话没有任何变化。 - 条件成立(
YES)时,唯一的改动是通过agent.followup在历史末尾追加一条普通用户消息。 之前的消息、system prompt、工具声明都不变,因此主请求的可复用前缀保持字节一致, provider 的 prefix cache / KV cache 不会被这次命令改写。 - 插件不会调用
agent.inject把判断结果塞进上下文,也不会编辑、替换或删除已有 session 事件。
兼容性
- 已在 DSH
0.2.1-rc.5与0.2.2-rc.1、Windows 桌面端验证; - 0.2.2 起宿主在 pre-step 之前固定本步模型:判断优先使用该步的
modelSelection或agent.model.current(),再把同一路由钉进 fork 子会话(0.2.1 没有这些 API 时退回请求头与agent.options,行为不变); - 命令使用
@deepseek-ai/dsh-commands的公开注册接口; - 判断优先使用
ctx.subagents的forkprovider +ctx.tools.guard;host 不提供时自动回退到ctx.llm.stream; - 不依赖客户端 bundle,不需要构建步骤。
测试
npm test # 进程内跑全部套件,不启动真实模型
npm run test:slow # 单独跑「provider 忽略 abort」的 60 秒截止用例
测试使用 mock 的 ctx.commands、ctx.llm、ctx.subagents、ctx.tools 和 agent,覆盖:
- 注册信息与
recordInput: false - fork 判断只追加一条尾部消息、
agentOptions.maxTokens收敛、子会话被释放 YES只发送指令、NO不发送- 判断输出里的 tool-call 标记会被剥离,
MAYBE之类的输出按失败处理 - guard 只拦判断子会话:
write/pwsh被拒,read/grep放行,其它会话不受影响,判断结束即释放归属 - fork 子会话会丢弃从父会话队列继承来的待办——
nextTurn和nextStep都清,只留自己的判断 prompt mode: message:默认把命令转成普通排队消息;轮次边界把/if换成指令、NO时丢弃、普通消息不判断mode: background:命令立即返回"已排队",判断异步完成;多个/if保持发送顺序- 没有可继承上下文的 fork provider 时回退到 transcript 判断,且绝不复用不继承前缀的 provider
- fork 启动失败时回退到 transcript 判断;两条路都失败时记一张
【/if 未执行】命令卡片而不是静默丢弃, 且不会给主线模型发消息 - 判断超时(provider 完全忽略
AbortSignal)也会在 60 秒后结束,不会卡住回合 ---多行条件- 缺少指令 / 空指令
- 过滤 system / developer 消息
- 不支持
reasoningEffort时的重试 - 判断 prompt 不含指令,结果文本不含条件或判断原文
- 发送的指令消息带
id和role: 'user',持久化 session 可以正常重载 - 假回合循环(
test/loop.test.mjs):按宿主的真实领取顺序(先 next-step、再一条 next-turn)跑一轮, 验证指令在 step 提交前就被替换、NO不会打开 step、判断失败时这一轮不打开 step 且留下命令卡片 - 客户端(
test/client.test.mjs):设置行读写、宿主拒绝写入时不显示“已保存”、加载中不显示只读文案、 失败卡片右侧的「取回 /if」(注册为conversation.chat.commandview的ifkey)把卡片里的原文插回输入框、卸载时释放订阅
隐私
- 插件不含遥测、统计上报或第三方远程请求。
- 判断时会把“当前会话已有上下文 + 条件”发送给当前会话配置的模型提供商,与正常对话走同一条模型通道。
- 条件和上下文可能包含敏感信息;不要把不希望发送给模型提供商的内容写进
/if条件。
已知限制
- 判断在宿主里是一个真实的子代理回合:桌面端会在会话时间线上显示一张子代理卡片
(label 为
if judge,含耗时、费用与 token 用量),判断结束即释放。主会话的消息列表 不会因此多出任何 judge 消息——那张卡片属于独立子会话。 /if在排队面板里是一条普通用户消息的副本:内容是/if <条件>+ 指令原文。它会在回合开始前被 替换成指令(成立)或丢弃(不成立),所以不会出现在模型历史里;编辑它就等于同时改条件与指令。- 判断失败时不会静默丢弃,也不会打扰主线模型:那条
/if会从批次里移除,同时在会话里记一张【/if 未执行】命令卡片(失败原因 + 可展开的说明),点这张卡片右侧的「取回 /if」即可把原文 放回输入框重发。background模式没有排队条目,失败时同样记一张命令卡片。日志里也会留一条 warning。 - 只读白名单是静态名单:未列出的工具(包括 MCP 只读工具)会被拒绝;需要放宽时请提 issue。
- 回退路径(host 没有 fork provider)看到的是纯文本记录,图片和文件只显示
[图片]/[文件]占位符, 条件需要的信息如果不在最近约 24k 字符的记录里,会被判为不成立。 - 判断使用当前会话的 provider/model;当前会话没有可用路由时返回错误。
- UI 结果是通用的,不展示条件和判断理由;需要排查时请查看 DSH 日志或会话事件。
- 该命令适合“条件明确、指令单一”的场景;需要复杂分支时,建议直接用普通对话。
License
Plugin correlati
dsh-web-ui (dsh-chat-recovery)
zhu1090093659/dsh-web-ui
billion-context
ranxianglei/billion-context
dsh-synapse
liangmianya/dsh-synapse
dsh-archive-manager
michengai/dsh-archive-manager