dsh-startlux-decision
Verifieddsh-startlux-decision · v0.3.0 · MIT · Web UI
StartLux Decision: a cheap judgement layer for DeepSeek Harness — all nine decision points (tool authorization, model routing, delegation, compaction, turn continuation, retrieval, skill pick, session title, COT translation) are implemented and individual
Install
dsh plugin add dsh-startlux-decision Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
dsh-startlux-decision
给 DeepSeek Harness 的 System-1 决策守卫,专为 Bamboo Relay / StartLux 打造。
在每次工具调用前问模型四个问题(风险、可逆性、任务相关性、注入嫌疑),据此放行、
询问或拦截。默认 shadow:只记录判断,绝不打断你干活。
安装
从 npm 安装(推荐)
dsh plugin add dsh-startlux-decision
或手动装进 profile:
cd ~/.dsh/profiles/desktop
pnpm add dsh-startlux-decision
然后在 profile 的 package.json → dsh.profile.bundles 里加入
"dsh-startlux-decision",重启 DSH。
本地开发
git clone https://github.com/zuosus/dsh-startlux-decision.git
cd dsh-startlux-decision
# 复制到 profile 的 node_modules,或 pnpm link
配置
插件配置页
插件 → 已安装 → dsh-startlux-decision → 点进该插件,配置区(「包含的组件」
上方)会出现 StartLux 卡片,布局与内置 dsh-rewind-plugin 一致:
- 两个开关:
启用守卫、真正拦截 - 一个 API Key 输入框 +
保存按钮 - 一个
测试端点按钮 - 决策模型参与点列表:每个决策点一行,右侧是
关闭 / 观察 / 生效三态 自身 token 预算输入框,以及实时的缓存命中率与每个点的 token / 延迟统计
改完点保存即写入 startlux.yml,不需要重启:监听器常驻注册,模式在
每次调用时现读;预算与缓存参数同样是取值函数而非启动期快照。
决策点(参与哪些环节)
这个插件的定位不是「工具调用风险闸门」,而是一个便宜的判断层:用一次 约 1.9k token 的小模型调用,去替代或阻止更贵的 LLM 开销。因此判断必须克制—— 只在「替代更贵的调用」或「阻止更大的开销」处介入,绝不为了「多一道检查」而介入。
八个决策点全部已实装,由你决定让决策模型进入哪些环节。
| # | 决策点 | 挂载位置 | 触发闸(为什么不是每次都问) | 生效方式 | 省 token 机制 |
|---|---|---|---|---|---|
| 1 | 工具授权 | tools/pre-execute |
只读工具 / auto 预设 / 排除名单直接放行 | ask / deny |
替代 auto-review 的满上下文审查 |
| 2 | 模型路由 | agent/request |
每轮仅前 N 步 + 路由未变不问 + 新用户输入不问 | 替换 provider/model |
机械步骤下沉到便宜模型 |
| 3 | 子任务分派 | agent/pre-step |
仅每轮第一步且带新用户输入 | 注入一条提示 | 少开一个 subagent = 省一整个上下文 |
| 4 | 上下文压缩 | agent/pre-step → ctx.compaction |
仅实测 token 达触发线 ×70% | 先跑确定性裁剪,让摘要被跳过 | 省掉一次摘要调用 |
| 5 | 轮次延续 | agent/turn-stopping |
同一轮最多续一次 | agent.steer() |
避免空转一整轮 |
| 6 | 检索必要性 | tools/pre-execute |
仅检索类工具;其余工具零开销 | deny |
省搜索与结果 token |
| 7 | 技能选择 | tools/pre-execute |
仅 skill 工具且候选 ≥2 |
deny + 指路 |
省误加载的技能正文 |
| 8 | 思维链翻译门槛 | 包住 translator.translate() |
本地预筛 + 同文本缓存 | 跳过并返回原文 | 按条省翻译调用 |
三态的含义
| 模式 | 行为 |
|---|---|
关闭 |
连请求都不发。省 token,也省那约 2 秒延迟 |
观察 |
照常判断并记录「本该怎样」,但不改变任何行为 |
生效 |
按判断执行该点的处置 |
新接入的决策点都应当先跑 观察,确认与人工判断一致后再切 生效。
阈值是实测校准的,不是拍脑袋的
每个决策点的阈值都用真实端点跑过对照样本(「该判 A」与「该判 B」成对输入), 只有两类输入真的分得开才留着。校准过程发现并修掉了三个问题,值得记下来:
choice的confidence是被选中项的概率。三选项平分时天然只有 0.33, 所以分派点的门槛不能设 0.6,否则永远判不出advise。现在按「显著高于均分」 取 0.5。- question 的问法决定一切。原来问「这个信息是否已知道」,两类输入给出 0.30 / 0.12 —— 完全没有区分力。改问「recent 里是否逐字覆盖了这个事实, 只依据那段文本回答」之后变成 0.92 / 0.01。
- 判据顺序会改变结果。轮次延续若先看
closed,「明确未完成」那组 因为closed=0.28不达线就会掉进默认stop——而它恰恰该续轮。 现在先看unfinished。
校准后的实测对照(scripts/verify-endpoint.mjs + 成对样本):
| 决策点 | 输入 A | 判定 | 输入 B | 判定 |
|---|---|---|---|---|
| 模型路由 | 机械步骤 | switch |
新架构设计 | keep |
| 子任务分派 | 三个包并行补测试 | advise |
变量改名 | inline |
| 上下文压缩 | 结论已落地 | prune |
对话承载状态 | summarize |
| 轮次延续 | 任务已完成 | stop |
明确还有三步 | continue |
| 检索必要性 | 上下文已覆盖 | skip |
完全没提过 | allow |
| 思维链翻译 | 英文→中文 | translate |
中文→中文 | skip |
为什么不是「所有 hook 都开」
决策模型每次约 1.9k token、p50 约 0.5 秒。agent/pre-step 与 agent/request
都是每步触发,两个都开且不加闸,等于每步多等 1 秒以上。所以热路径上的
决策点(注册表里标了 hotPath,配置页显示「热路径」角标)都带明确的触发闸,
不能无条件调用。
同时有两道护栏兜底:
- 自身预算(
budgetTokens,默认每小时 40 万):超限后所有决策点自动退回 启发式并放行。省钱的东西不能反过来成为主要消耗方。 - 双层缓存:见下节。
模型选择
配置页的「模型」是下拉框,由服务商实时提供,另有「自动(服务端默认)」选项。
默认就是「自动」(startlux.yml 里 model: 留空):插件不发送该字段,
服务端用它当前的默认决策模型。实测省略 model 返回 200,响应里回填实际使用的模型名:
{"answers":{...},"model":"startlux-decision-27b-autothink","usage":{...}}
这样即便服务商将来更换模型名,留空配置也不会因此 400。
注意:
startlux.yml里的model:必须写出来但留空(model:后面不跟值)。 解析器把显式空值保留为"";若整行删掉,键会缺失并回落到内置的硬编码模型名, 「自动」就不生效了。
列表为何要过滤:/v1/models 返回全部 20 个模型,其中多数是 chat 模型,
拿去打 /v1/decisions 会 400 model ... is not served by this plugin。
本插件按 supported_endpoint_types 只保留声明了 StartLux Decision /
StartLux SystemOne 的模型。实测 typesafe/jev-1.14 虽声明 jev-decisions,
仍被本端点拒绝,因此也排除。
超时与自动回退
每次请求都带硬超时(默认 8000ms),超时不是错误,而是降级放行:
AbortSignal.timeout(budget) → 超时 → degrade("timeout") → 钩子 next() 放行
三层保护,保证守卫永远不会拖住工具执行:
- 单次超时:
timeoutMs(默认 8000ms,可在配置页调整,最小 1000ms)。 实测该端点 p50≈0.7s、冷启动 p95≈6.4s,8s 覆盖冷启动又不至于让用户干等。 - 连续失败冷却:连续
failureThreshold(默认 3)次失败后进入cooldownMs(默认 60s)冷却,冷却期内直接放行、0ms 返回, 不再对着故障端点反复消耗。 - 调用方取消:若上层传入
AbortSignal且被中止,立即降级返回caller-aborted。
实测(指向不可路由地址,timeoutMs: 1500):
第1次: 1504ms → 降级 第4次: 0ms → 降级 ← 冷却生效
第2次: 1502ms → 降级 第5次: 0ms → 降级
第3次: 1502ms → 降级(触发冷却)
没有配置 key 时根本不发请求,直接 no-key 降级,零延迟。
失败降级(fail-open)
插件的设计原则是任何不确定都退回原生行为,绝不因为守卫自身故障而卡住工具调用。
以下情形全部 next() 放行,并在 telemetry.jsonl 记一条 action=degraded:
| 情形 | detail |
|---|---|
插件未启用(enabled: false) |
不进入判断,直接放行 |
| 没有 API key | no-key |
| 命中失败冷却 | cooldown |
| 端点 4xx / 5xx | http-404 / http-500 等(附 endpoint、body) |
| 网络不可达 | network-error |
| 超时 | timeout(附 budgetMs) |
| 返回非 JSON / 非对象 | invalid-json / invalid-response |
缺 answers 字段 |
missing-answers |
答案形状不符(类型错、缺字段、choice 非法、概率越界/NaN) |
answer-shape-mismatch |
| 守卫内部抛异常 | 捕获后放行,仅 logger.warn |
| 只读工具 / auto 权限预设 / 被排除工具 | 不判断,直接放行 |
默认 mode: shadow 时,即使判断出 deny 也只记录不执行。
省 token 的设计
- 双层缓存:
精确:完整state+ 问题集一致即复用,绝对安全。同类复用:键是「工具名 + 命令族 + 风险标记」的归一化签名, 只有低风险放行的裁决才进这一层;ask/deny永不复用, 否则会把一次危险判断错误地扩散到同类操作。这一层是必要的:旧实现只用精确键,而键里含
args_preview, 于是实测 309 次调用命中 0 次——等于缓存完全没工作。 现在 4 条同类node -e '...'变体只需 1 次真实请求(命中率 60%), 而rm -rf不会复用任何缓存裁决。
thinkMode: never:实测主要省延迟(约 1.9s vs 7.2s);输入 token 基本不变 (该端点output_tokens恒为 0,思考开销未计入计量)。- 只读工具直接放行:
read/grep/glob/todo_write等不发请求。 - state 截断:
args走头尾摘要,recent只带最近 4 条、每条 240 字符。 - 失败冷却:连续 3 次失败进入 60s 冷却,避免对着故障端点烧钱。
- 自身预算:
budgetTokens限制决策模型每小时的花费,超限自动退回启发式。 - 默认只观察:新决策点先跑
observe,确认与人工判断一致后再切enforce。
验证链路
自动化验收(三条,全部可重复跑)
pnpm test # 78 项单元测试:纯函数 + 适配器接线 + 降级语义
pnpm verify:wiring # 39 项接线检查:用真实 schemastery 加载插件
pnpm verify:endpoint # 8 个决策点逐个打真实端点,校验题型与答案形状
pnpm verify # 前两条一起跑
verify:wiring 需要 profile 的依赖可解析。仓库里没有 node_modules 时先建链接:
ln -s ~/.dsh/profiles/desktop/node_modules node_modules
它检查的是「宿主到底给不给这个服务」——包括缺 translator / sessionTitle
时 apply() 不抛、所有监听器都注册上、每个新点默认 off(升级不改变行为)。
verify:endpoint 要网络与 BAMBOO_API_KEY,它逐点校验:
端点接受该点的问题集(choice 必须有 criteria、score 必须 2–10 档、
题型只认三种)、答案能被 parse() 解析、decide() 产出合法 kind。
这一步不能省:题型写错会在运行时 400 并静默降级,光看日志很难发现
「这个点其实一直没生效」。
手工验证
「测试端点」按钮→ 看到HTTP 200和x-oneapi-request-id,说明配置与网络都对。插件配置页→ 确认端点、模型、密钥 已配置 ✅; 决策点列表里每个点都应有关闭 / 观察 / 生效三态(没有「即将支持」)。 若某个点显示宿主缺服务,说明宿主没有它依赖的服务(如translator), 该点无法开启——这不是 bug,是不适用的部署。- 把某个点切到
观察,触发一次对应动作 → 看~/.dsh/dsh-startlux-decision/telemetry.jsonl:action=degraded, detail=no-key→ key 没读到detail=http-404/http-400→ 地址或模型不对(记录里带endpoint和body)detail=answer-shape-mismatch→ 端点返回的答案形状不对(该点的问题集要复核)point=route, action=switch→ 该点判出了「本该下沉到便宜模型」- 每条的
point字段标明是哪个决策点,scores里是原始判断分数
- 服务商后台 → 出现
type=2 · Action decision记录。
阈值
工具授权
| 键 | 默认 | 含义 |
|---|---|---|
guardDenyConfidence |
0.85 | 高风险 + 高置信 + 不可逆 → deny |
guardIrreversibleDenyFloor |
0.5 | 判定不可逆的门槛 |
guardIrreversibleCeiling |
0.2 | 低风险但不可逆 → 升级询问 |
guardConfidenceMin |
0.6 | 中风险置信下限,低于则放行 |
guardInjectionAskThreshold |
0.8 | 注入嫌疑 → 升级询问 |
其余八个决策点
全部阈值都在 startlux.yml 里可覆盖,默认值按实测校准(见上文「阈值是实测
校准的」)。下表列出「改动它会怎样」:
| 键 | 默认 | 调高会怎样 | 调低会怎样 |
|---|---|---|---|
routeCheapProvider / routeCheapModel |
空 | 留空即不路由,点开着也只记录 | — |
routeMaxStepsPerTurn |
3 | 每轮判断更多步,更准但更慢 | 更快但后续步骤沿用旧裁决 |
routeMaxScore |
0.4 | 更少下沉(更保守) | 更多下沉(更省但可能降质) |
routeMechanicalFloor |
0.6 | 更难判为机械(更保守) | 更易下沉 |
routeRiskFloor |
0.3 | 风险稍高就保留强模型(更保守) | 更敢下沉 |
delegateConfidenceFloor |
0.5 | 更少建议分派 | 更多建议分派 |
delegateBlockingFloor |
0.6 | 更容易判「被阻塞」→ 更少分派 | 更敢分派 |
delegateParallelFloor |
0.5 | 组队需要更强并行证据 | 更容易建议组队 |
compactContextWindow |
0 | — | 设成实际窗口才能按比例提前介入 |
compactThresholdRatio |
0.7 | 更晚介入(贴近 compaction 自己的 0.8) | 更早介入 |
compactDistilledFloor |
0.6 | 更少裁剪(更保守) | 更多裁剪 |
compactVerbatinFloor |
0.6 | 更少因「需要原文」而改走摘要 | 更保守(更容易摘要) |
turnStopMaxSteers |
1 | 一轮可续多次(注意空转风险) | 0 = 永不续轮 |
turnStopUnfinishedFloor |
0.6 | 更少续轮 | 更易续轮 |
turnStopValueFloor |
0.35 | 更少续轮 | 更易续轮 |
retrievalCoveredFloor |
0.5 | 更少跳过检索(更安全) | 更敢跳过 |
retrievalWouldAddCeiling |
0.8 | 更敢跳过(更省) | 更少跳过 |
skillPickConfidenceFloor |
0.5 | 更少改指路 | 更敢改指路 |
cotProseFloor |
0.5 | 更少翻译(更省) | 更敢翻译 |
cotSameLanguageFloor |
0.8 | 更少判「已是目标语言」→ 更多翻译 | 更敢跳过 |
License
MIT