Zum Hauptinhalt springen
Z

dsh-startlux-decision

zuosus/dsh-startlux-decision

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

Installation

dsh plugin --profile web add github:zuosus/dsh-startlux-decision

README

dsh-decision-engine

website

给 DeepSeek Harness 的 System-1 决策守卫:在每次工具调用前问模型四个问题 (风险、可逆性、任务相关性、注入嫌疑),据此放行、询问或拦截。默认 shadow: 只记录判断,绝不打断你干活。

A System-1 decision guard for DeepSeek Harness: before every tool call it asks the model four questions (risk, reversibility, task relevance, injection suspicion) and allows, asks, or blocks accordingly. Defaults to shadow: judgments are only recorded and never interrupt your work.

它支持所有 system-one 风格 API:内置多个预设,也可以填任意 custom base URL(自建 llama.cpp / vLLM、 企业网关、第三方中转)。请求路径、鉴权方式与响应结构与 system-one 协议保持一致, 换 provider 不影响各决策点的解析逻辑。

It works with any system-one style API: built-in presets for multiple presets, or any custom base URL (self-hosted llama.cpp / vLLM, corporate gateways, third-party relays). Request path, auth and response shape follow the system-one protocol, so switching providers never changes how answers are parsed at any decision point.

本项目原名 dsh-startlux-decision(更早叫 dsh-startlux-guard)。 0.4.0 起更名为 dsh-decision-engine 并加入 provider 抽象。

0.5.0 起配置文件名从 startlux.yml 改为 config.yml——本插件已不是 只服务 startlux 一家,名字里再挂某个服务商只会误导。旧文件会被自动改名 迁移,升级不需要重新填 key;迁移失败时仍会回退读取旧文件名。

This project was formerly named dsh-startlux-decision (before that, dsh-startlux-guard). Since 0.4.0 it is dsh-decision-engine, with a provider abstraction added.

Since 0.5.0 the config file is config.yml, renamed from startlux.yml — the plugin no longer serves a single vendor, and keeping one provider's name in the file would only mislead. Old files are automatically renamed in place; upgrading does not require re-entering keys, and if the rename fails the old filename is still read as a fallback.

安装 / Install

dsh plugin add dsh-decision-engine

或手动装进 profile:

Or add it to a profile manually:

cd ~/.dsh/profiles/desktop
pnpm add dsh-decision-engine

然后在 profile 的 package.json → dsh.profile.bundles 里加入 "dsh-decision-engine",重启 DSH。

Then add "dsh-decision-engine" to package.json → dsh.profile.bundles of the profile and restart DSH.

本地开发 / Local development

git clone https://github.com/zuosus/dsh-decision-engine.git
cd dsh-decision-engine
# 复制到 profile 的 node_modules,或 pnpm link
# copy into the profile's node_modules, or use pnpm link

仓库里带了迁移脚本,不用手改(默认只预览,--write 才写入并自动备份):

The repo ships a migration script, so nothing has to be edited by hand (preview-only by default; --write applies the change and keeps a backup):

node scripts/migrate-profile.mjs           # 预览 / preview
node scripts/migrate-profile.mjs --write   # 写入 + 备份 .bak / apply + .bak backup
cd ~/.dsh/profiles/desktop && pnpm install

然后重启 DSH。配置不用重填:插件在启动时会做一次自动迁移, 把旧目录里的配置与遥测拷到新目录——

Then restart DSH. No re-configuration is needed: on startup the plugin runs a one-time automatic migration, copying config and telemetry from old directories to the new one —

路径
数据目录(迁移目标)~/.dsh/dsh-decision-engine/
Path
Data directory (migration target)~/.dsh/dsh-decision-engine/

迁移规则:

Migration rules:

  • 只搬一次:新目录已有 config.yml 就不再动,绝不覆盖你后来改的值。

  • 配置改名:新目录里的旧名 startlux.yml 会被 rename 成 config.yml (rename 失败则拷贝);跨目录迁移是拷贝,源文件保留,出问题可对照。

  • 只搬两个文件:配置文件与 telemetry.jsonl。目录里其它东西 (笔记、备份)一概不碰。

  • 权限 600:配置里可能有明文 key,迁移后仍保持只有你可读。

  • Migrated only once: if a config.yml already exists in the new directory, nothing is touched — values you edited later are never overwritten.

  • Config rename: an old startlux.yml inside the new directory is renamed to config.yml (copied if rename fails); cross-directory migration copies, so the source file is kept for comparison if anything goes wrong.

  • Only two files move: the config file and telemetry.jsonl. Everything else in the directory (notes, backups) is left alone.

  • Permission 600: the config may contain a plaintext key, so after migration it stays readable only by you.

数据目录解析规则(跨平台)/ Data directory resolution (cross-platform)

数据目录是 $DSH_HOME/dsh-decision-engine/,~/.dsh 只是 DSH_HOME 未设置时的默认值。解析顺序与宿主 @deepseek-ai/dsh-home-paths 完全一致:

The data directory is $DSH_HOME/dsh-decision-engine/; ~/.dsh is only the default when DSH_HOME is unset. The resolution order matches the host's @deepseek-ai/dsh-home-paths exactly:

优先级来源
1显式配置的路径(本插件暂无此入口)
2环境变量 $DSH_HOME
3操作系统家目录下的 ~/.dsh
PrioritySource
1Explicitly configured path (this plugin has no such entry point yet)
2Environment variable $DSH_HOME
3~/.dsh under the OS home directory

细节(都由 lib/home-paths.js 统一处理):

Details (all handled in one place by lib/home-paths.js):

  • 空白值等于未设置:DSH_HOME="" 或纯空白会回退 ~/.dsh,绝不会把数据 目录解析到当前工作目录。

  • 支持波浪号:DSH_HOME=~/myhome 会展开;~\(Windows 形式)同样支持。

  • 相对路径会被规范化成绝对路径,否则落盘位置会随进程工作目录漂移。

  • Blank counts as unset: DSH_HOME="" or whitespace-only falls back to ~/.dsh; the data directory can never resolve into the current working directory.

  • Tilde is supported: DSH_HOME=~/myhome expands, and the ~\ (Windows) form too.

  • Relative paths are normalized to absolute, otherwise the on-disk location would drift with the process's working directory.

Windows 说明:~/.dsh 里的 .dsh 只是目录名,不是路径分隔符—— Windows 允许点开头的目录。真正与平台相关的是分隔符与家目录位置,二者分别由 node:path 与 node:os 处理,所以 Windows 上实际路径是 C:\Users\<你的用户名>\.dsh\dsh-decision-engine\config.yml。

Note for Windows: the .dsh in ~/.dsh is just a directory name, not a path separator — Windows allows directories that begin with a dot. What actually differs by platform are the separator and the home-directory location, handled by node:path and node:os respectively, so the real path on Windows is C:\Users\<your-name>\.dsh\dsh-decision-engine\config.yml.

配置 / Configuration

插件配置页 / The plugin config page

插件 → 已安装 → dsh-decision-engine → 点进该插件,配置区(「包含的组件」 上方)会出现 Decision Engine 卡片,布局与内置 dsh-rewind-plugin 一致:

Go to Plugins → Installed → dsh-decision-engine → open the plugin; above the "included components" area a Decision Engine card appears, laid out like the built-in dsh-rewind-plugin:

  • 服务提供方下拉:system one / bamboonode / custom,下面一行显示当前生效端点

  • 两个开关:启用守卫、真正拦截

  • 思考模式下拉:never / auto / always(见下文「省 token 的手段」里的取舍)

  • 模型下拉(来自当前 provider 的 /v1/models,含「自动(服务端默认)」)

  • 一个 API Key 输入框

  • 选 custom 时展开四个字段:Base URL(必填)、端点风格、附加请求头、密钥变量名

  • 一个 测试端点 按钮(回显 provider、实际 POST 的完整地址、HTTP 码、请求 id 与响应体)

  • 决策模型参与点列表:每个决策点一行,右侧是 关闭 / 观察 / 生效 三态

  • 实时的缓存命中率与每个点的 token / 延迟统计

  • Provider dropdown: system one / bamboonode / custom, with the currently effective endpoint shown on the line below

  • Two switches: Enable guard, Actually block

  • Thinking mode dropdown: never / auto / always (see the trade-off in "Token-saving design" below)

  • Model dropdown (served live from the current provider's /v1/models, plus "auto (server default)")

  • An API key input

  • Choosing custom expands four fields: Base URL (required), Endpoint style, Extra headers, Key variable name

  • A Test endpoint button (echoes the provider, the exact full POST URL, HTTP code, request id and response body)

  • Decision point list: one row per point, each with an off / observe / enforce tri-state on the right

  • Live cache-hit rate and per-point token / latency stats

改完即保存,没有「保存」按钮:开关 / 下拉 / 分段控件点一下就落盘;文本框 (地址、请求头)停手约 0.7 秒或失焦后落盘;API Key 只在失焦或回车 时落盘(避免把打字途中的半截 key 写进凭据文件)。底部状态行实时显示 待保存… / 保存中… / 已保存 / 未保存:原因,失败时附「重试」。落盘即写入 config.yml,不需要重启:监听器常驻注册,provider 与模式在每次调用时现读; 缓存参数同样是取值函数而非启动期快照。

Edits save immediately — there is no Save button: switches / dropdowns / segmented controls persist on click; text fields (URL, headers) persist ~0.7s after you stop typing or on blur; the API key persists only on blur or Enter (so a half-typed key never lands in the credentials file). The status line at the bottom shows pending… / saving… / saved / unsaved: reason live, with a retry action on failure. Persisting writes config.yml and needs no restart: listeners are registered permanently, provider and mode are read fresh on every call, and cache parameters are getters too rather than startup snapshots.

一个例外:选 custom 但 Base URL 还没填或不合法时,配置改动会整体延后 (状态行给出原因),地址补全后自动落盘;此时决策点改动不受影响,照常保存。

One exception: while custom is selected but Base URL is empty or invalid, config changes are deferred as a whole (the status line says why) and persist automatically once the address is complete; decision-point changes are unaffected and keep saving normally.

Provider:接任意 system-one 风格服务 / Any system-one style service

请求形状对所有 provider 完全一致(这也是 system-one 协议本身):

The request shape is identical across all providers (this is the system-one protocol itself):

POST <base>/decisions        或 / or   POST <base>/systemone
authorization: Bearer <key>
content-type: application/json

{ "model": "...", "state": {...}, "questions": {...}, "think_mode": "never" }

响应只需含 answers(model / usage 可选)。因此 provider 只决定三件事: 往哪发、用谁的 key、默认哪个模型。

A response only needs answers (model / usage optional). So a provider decides exactly three things: where to send, whose key to use, which model by default.

五个选项 / Five options

provider默认地址端点密钥名默认模型
system onehttps://api.typesafe.ai/v1/v1/systemoneTYPESAFE_API_KEY服务端默认(jev-latest)
bamboonodehttps://api.bamboonode.dev/v1/v1/decisionsBAMBOO_API_KEY服务端默认(startlux-decision-27b-autothink)
siliconflow(硅基流动,国内站)https://api.siliconflow.cn/v1/v1/systemoneSILICONFLOW_API_KEYKev-4B(可改 SemIf)
siliconflow-intl(Silicon Flow,国际站)https://api.siliconflow.com/v1/v1/systemoneSILICONFLOW_INTL_API_KEYKev-4B(可改 SemIf)
custom用户填写(必填)按 端点风格 拼customApiKeyEnv(缺省 DECISION_ENGINE_CUSTOM_KEY)customModel
providerDefault base URLEndpointKey variableDefault model
system onehttps://api.typesafe.ai/v1/v1/systemoneTYPESAFE_API_KEYserver default (jev-latest)
bamboonodehttps://api.bamboonode.dev/v1/v1/decisionsBAMBOO_API_KEYserver default (startlux-decision-27b-autothink)
siliconflow (CN site)https://api.siliconflow.cn/v1/v1/systemoneSILICONFLOW_API_KEYKev-4B (switch to SemIf if desired)
siliconflow-intl (intl site)https://api.siliconflow.com/v1/v1/systemoneSILICONFLOW_INTL_API_KEYKev-4B (switch to SemIf if desired)
customuser-provided (required)assembled per endpoint stylecustomApiKeyEnv (defaults to DECISION_ENGINE_CUSTOM_KEY)customModel

硅基流动两个站点的接入文档: 国内站 api-docs.siliconflow.cn · 国际站 docs.siliconflow.com

SiliconFlow integration docs for both sites: CN site api-docs.siliconflow.cn · intl site docs.siliconflow.com

开箱即用:siliconflow / siliconflow-intl 只需要粘一个 API key——地址、 路径(/systemone)、默认模型(Kev-4B)都已预置。两个站点是两套账号, key 互不通用,所以各占一格。

Ready to use: siliconflow / siliconflow-intl need nothing but one pasted API key — the base URL, path (/systemone) and default model (Kev-4B) are all preconfigured. The two sites are separate accounts with non-interchangeable keys, which is why they get their own slots.

每个 provider 有各自的 key 名,各自保存各自的 key。 五者互不覆盖, 可以同时配好五套凭据;切换 provider 不会串用别人的 key。

Each provider has its own key variable and keeps its own key. None of the five overwrite one another; you can configure all five credential sets at once, and switching providers never leaks one provider's key to another.

不写 provider 时等于 bamboonode,老配置行为完全不变。

Omitting provider means bamboonode — old configs behave exactly as before.

模型字段按 provider 分开存,切换 provider 不会把 A 的模型名发给 B:

Model fields are stored separately per provider, so switching providers never sends provider A's model name to provider B:

provider配置键默认
bamboonodemodel与历史一致(留空即服务端默认)
system onesystemoneModel留空 = 服务端默认(jev-latest)
siliconflowsiliconflowModel留空 = 预设默认 Kev-4B
siliconflow-intlsiliconflowIntlModel留空 = 预设默认 Kev-4B
customcustomModel留空 = 服务端默认
providerConfig keyDefault
bamboonodemodelunchanged from history (blank = server default)
system onesystemoneModelblank = server default (jev-latest)
siliconflowsiliconflowModelblank = preset default Kev-4B
siliconflow-intlsiliconflowIntlModelblank = preset default Kev-4B
customcustomModelblank = server default

⚠️ 两个 SiliconFlow 端点把 model 列为必填(实测省略或空串都回 400 {"code":20013,"message":"Model field is required."}),所以它们的 「留空」不是「让服务端挑」,而是用预设默认 Kev-4B;想换模型填 SemIf。 官方目前只支持这两个决策模型,界面上的模型下拉也只列这两个—— /v1/models 会返回 98 个模型,但 chat 模型打到 /systemone 会 400 Model does not exist.,列出来就是「选了就失败」。

模型名大小写不敏感:kev-4b / Kev-4b / Kev-4B 实测都 200, 配置里三种写法等价。

⚠️ Both SiliconFlow endpoints list model as required (measured: omitting it or sending an empty string both return 400 {"code":20013,"message":"Model field is required."}), so "blank" there does not mean "let the server pick" — it falls back to the preset default Kev-4B; put SemIf in to switch. The platform currently serves only these two decision models, and the model dropdown lists only these two as well — /v1/models returns 98 models, but a chat model sent to /systemone gets 400 Model does not exist., so listing them would only offer "options that fail when picked".

Model names are case-insensitive: kev-4b / Kev-4b / Kev-4B all measured 200 — the three spellings are equivalent in config.

custom:填写方式与限制 / custom: fields and limits

在设置界面选 custom 后需要填:

After selecting custom in the settings UI, you fill in:

字段必填说明
Base URL是必须是 http:// 或 https:// 地址。不支持其它协议(ftp: 等会被拒绝),不带 scheme 也会被拒绝——自定义地址必须显式,避免猜错协议。不能含空格、换行或查询串。
端点风格否auto(默认)/ decisions / systemone。这是首选路径,不是唯一路径:首选返回 404 时插件会自动改用另一条再试一次(见下)。
附加请求头否格式 名称: 值,多项用分号分隔,如 X-Tenant: team-a; X-Trace: dsh。authorization / content-type / host 由插件固定,写了也会被忽略。
密钥变量名否填了就从该环境变量或 ~/.dsh/.credentials.yaml 的 refs: 段取 key;也可以直接在 API Key 输入框粘贴。
模型否留空 = 由服务端选默认;实测留空比发空串安全(发 "model": "" 会被 400)。
FieldRequiredDescription
Base URLyesMust be an http:// or https:// URL. Other schemes are rejected (ftp: etc.), and a URL without a scheme is rejected too — a custom address must be explicit so the protocol is never guessed. No spaces, newlines, or query strings.
Endpoint stylenoauto (default) / decisions / systemone. This is the preferred path, not the only one: if the preferred path returns 404, the plugin automatically retries once on the other path (see below).
Extra headersnoFormat name: value, semicolon-separated, e.g. X-Tenant: team-a; X-Trace: dsh. authorization / content-type / host are fixed by the plugin; supplying them is silently ignored.
Key variable namenoWhen set, the key is read from that environment variable or the refs: section of ~/.dsh/.credentials.yaml; you can also paste it straight into the API key input.
ModelnoBlank = server default. Measured: blank is safer than an empty string (sending "model": "" gets a 400).

地址拼接规则(末尾斜杠一律先去掉,因此 .../v1/ 与 .../v1 等价):

URL assembly rules (a trailing / is always stripped first, so .../v1/ and .../v1 are equivalent):

你填的 base URL端点风格实际 POST
https://x.devdecisionshttps://x.dev/v1/decisions
https://x.dev/v1decisionshttps://x.dev/v1/decisions
https://x.dev/v1/decisionsdecisionshttps://x.dev/v1/decisions(不重复)
https://x.dev/api/v1decisionshttps://x.dev/api/v1/decisions
https://x.dev/v1systemonehttps://x.dev/v1/systemone
https://x.dev/v1/systemonedecisionshttps://x.dev/v1/systemone(已带端点即原样)
Base URL you enterEndpoint styleActual POST
https://x.devdecisionshttps://x.dev/v1/decisions
https://x.dev/v1decisionshttps://x.dev/v1/decisions
https://x.dev/v1/decisionsdecisionshttps://x.dev/v1/decisions (no duplication)
https://x.dev/api/v1decisionshttps://x.dev/api/v1/decisions
https://x.dev/v1systemonehttps://x.dev/v1/systemone
https://x.dev/v1/systemonedecisionshttps://x.dev/v1/systemone (kept as-is once it already names an endpoint)

拼接是幂等的:对结果再拼一次不会变成 /v1/v1/decisions 或 //decisions。

Assembly is idempotent: assembling once more onto the result never produces /v1/v1/decisions or //decisions.

端点路径由服务商决定,不由模型决定 / The endpoint path belongs to the provider, not the model

这是本插件最容易踩空的一处,因此实测过(2026-10-25,真实 key 打真实端点):

This is the plugin's most trip-prone detail, so it was verified against real endpoints with real keys (2026-10-25):

服务商模型/v1/decisions/v1/systemone
SiliconFlowKev-4B404 Not Found200 ✅
bamboonodestartlux-decision-27b-autothink200 ✅200 ✅
ProviderModel/v1/decisions/v1/systemone
SiliconFlowKev-4B404 Not Found200 ✅
bamboonodestartlux-decision-27b-autothink200 ✅200 ✅

也就是说,Kev-4B 并不是「只能用 /systemone 的模型」——路径由服务商决定。 同一个 Kev-4B 换一个服务商,路径就得跟着换。

In other words, Kev-4B is not "a model that only speaks /systemone" — the path belongs to the provider. The same Kev-4B on a different provider needs a different path.

因此 端点风格 只是「首选」:

Which is why endpoint style is only the preference:

  1. 先打首选路径;

  2. 收到 404(= 路径不存在)时,自动改用另一条再试一次;

  3. 试通的那条会被记住(按 base URL 区分),下次直接打它,不再多一次往返;

  4. 两条都 404 时,遥测与「测试端点」会列出试过的两个地址并提示改端点风格。

  5. Send to the preferred path first;

  6. On 404 (= the path does not exist), automatically retry once on the other path;

  7. The path that works is remembered (keyed by base URL), so the next call goes straight to it without an extra round-trip;

  8. If both paths 404, telemetry and the "Test endpoint" button list both addresses that were tried and suggest changing the endpoint style.

只有 404 会触发换路径:400(模型/内容不合法)、401(key 不对)、5xx 都与路径无关, 换路径只是多烧一次请求。配置页的「生效端点」下方会显示「已验证可用:…」。

Only 404 triggers the path switch: 400 (invalid model/content), 401 (wrong key) and 5xx all have nothing to do with the path — switching would just burn another request. Below "effective endpoint" the config page shows "verified working: …".

旧版本没有回退:路径踩空就是 404,而守卫是 fail-open 的——静默失效。 现场证据:telemetry.jsonl 里 2026-10-25 13:16–13:41 有 8 次 http-404 降级。

Older versions had no fallback: a wrong path just 404s, and the guard is fail-open — it fails silently. Smoking gun: telemetry.jsonl recorded 8 http-404 degradations between 13:16 and 13:41 on 2026-10-25.

持久化与切换行为 / Persistence and switching behavior

  • 所有 provider 的配置并列存在 config.yml 里,互不覆盖: provider 记录当前选择,baseUrl/model 属于内置预设(bamboonode 的 baseUrl 保留老的覆盖语义),customBaseUrl/customModel/customApiKey/ customApiKeyEnv/customHeaders/customEndpointStyle 属于 custom。

  • 切换 provider 只改 provider 一个键:其它 provider 已保存的地址、模型、密钥 原样留着,切回来即恢复。

  • 保存后再次打开配置页按服务端返回的状态回显(不缓存、不推断),custom 的字段 除密钥明文外全部回显。

  • 密钥优先级:该 provider 的显式 key(systemoneApiKey / bamboonodeApiKey / siliconflowApiKey / siliconflowIntlApiKey / customApiKey)> 密钥存储位置上对应的键 > 环境变量。

  • 旧的单一 apiKey 字段只对 bamboonode 生效(历史兼容)。

  • All providers' settings live side by side in config.yml and never overwrite one another: provider records the current choice, baseUrl/model belong to the built-in presets (bamboonode's baseUrl keeps its legacy override semantics), and customBaseUrl/customModel/customApiKey/ customApiKeyEnv/customHeaders/customEndpointStyle belong to custom.

  • Switching providers changes exactly one key, provider: the other providers' saved URLs, models and keys stay untouched and come right back when you switch over again.

  • Reopening the config page after saving reflects the server's returned state verbatim (no caching, no guessing); all custom fields are echoed back except the key's plaintext.

  • Key precedence: the provider's explicit key (systemoneApiKey / bamboonodeApiKey / siliconflowApiKey / siliconflowIntlApiKey / customApiKey) > the matching key at the chosen storage location > environment variables.

  • The legacy single apiKey field applies only to bamboonode (backwards compatibility).

密钥保存位置 / Where keys are stored

密钥可以放在两个地方,默认 credentials:

Keys can live in two places; the default is credentials:

keyStorage写到哪里适合
credentials(默认)~/.dsh/.credentials.yaml 的 refs: 段与其他服务(DeepSeek / ModelScope …)统一托管,插件目录不留明文
config插件配置目录下的 config.yml希望配置自包含、能随目录整体备份或迁移;注意是明文
keyStorageWritten toGood for
credentials (default)the refs: section of ~/.dsh/.credentials.yamlmanaged alongside other services (DeepSeek / ModelScope …); no plaintext in the plugin directory
configconfig.yml in the plugin's config directorya self-contained config you can back up or move as one directory — note it is plaintext

在设置界面「密钥保存位置」下拉切换,切换即按新位置写入;已有的 key 不会被 自动搬家(避免"看不到的地方被改动"),需要时重新粘贴一次即可。

The "key storage location" dropdown in the UI switches where keys are written, effective immediately; existing keys are never silently relocated (nothing changes behind your back) — just paste once more if you need to move one.

写入 credentials 的行为约束:

Constraints when writing to credentials:

  • 只改 refs: 段,records:(DSH 的授权记录,含嵌套结构)与注释逐字保留;

  • 键名就是该 provider 自己的名字,所以五套 key 能同时存在、互不覆盖;

  • 传空值等于删除该键(清空即撤销,不留空壳行);

  • 文件权限固定 600;

  • 写入失败会整体报错,不会出现"配置说存了、实际没存"。

  • Only the refs: section is touched; records: (DSH's own authorization records, nested included) and comments are preserved verbatim;

  • the key name is that provider's own name, so all five key sets coexist without overwriting each other;

  • passing an empty value deletes the key (clearing means revoking — no empty shell line is left behind);

  • the file permission is pinned to 600;

  • a failed write raises as a whole — the config can never claim a key was saved when it wasn't.

密钥优先级:显式 key 字段 > 所选存储位置里的对应键 > 环境变量。

Key precedence: explicit key field > the matching key at the chosen storage location > environment variables.

错误与降级 / Errors and degradation

情形表现
base URL 非法 / 为空(custom)该次落盘直接被拒(HTTP 400 + 具体原因,如「必须以 http:// 或 https:// 开头」),不会写进 yml;自动保存会一直挂着「未保存」并允许重试
密钥缺失不发请求,no-key 降级(记录里带该 provider 的变量名)
鉴权失败(401/403)auth-failed 降级,提示检查该 provider 的 key 是否有效、是否与地址匹配
地址错(404/400)http-404 / http-400 降级,记录里带 provider、endpoint 与响应体
provider 配置不可用invalid-base-url 降级,不打到猜出来的地址上
网络失败 / 超时network-error / timeout 降级,随后按阈值进入冷却
SituationBehavior
Invalid or empty base URL (custom)That save attempt is rejected outright (HTTP 400 plus the concrete reason, e.g. "must start with http:// or https://") and never written into the yml; auto-save keeps showing "unsaved" and allows a retry
Key missingNo request is sent; no-key degradation (the record carries that provider's variable name)
Auth failure (401/403)auth-failed degradation, telling you to check whether that provider's key is valid and matches the URL
Wrong address (404/400)http-404 / http-400 degradation; the record carries provider, endpoint and the response body
Provider config unusableinvalid-base-url degradation; nothing is sent to a guessed address
Network failure / timeoutnetwork-error / timeout degradation, then cooldown per the thresholds

降级一律 fail-open:放行原行为,并写一条 telemetry.jsonl。 「测试端点」按钮会把完整诊断(provider、实际端点、HTTP 码、请求 id、响应体)直接显示出来。

Degradation is always fail-open: the original behavior passes through and one line is written to telemetry.jsonl. The Test endpoint button shows the full diagnosis (provider, actual endpoint, HTTP code, request id, response body) directly.

错误显示在消息尾部 / Errors shown at the tail of the message

降级如果只写进日志,用户在聊天窗口里什么都看不到——插件「像是没生效」。 所以本插件会把这一轮出的问题,直接显示在对应那条消息的末尾 (就像内置的 token 统计那样挂在消息尾部)。

The degradation is not only logged: it is shown at the tail of the message it belongs to, the same place the host's own token statistics sit.

┌─────────────────────────────────────────┐
│ 助手消息正文……                          │
├─────────────────────────────────────────┤
│ 决策  工具授权  鉴权被拒            bash │  ← 这一轮出的问题
└─────────────────────────────────────────┘

它怎么知道属于哪一轮? 靠会话事件。每一次降级,宿主往会话里写一条 plugin:decision-engine/error 记录(带 turn),客户端的 conversation Definition 把它折进「这一轮的错误」,再由 conversation.chat.turnTail 槽位渲染出来。

为什么不用轮询 HTTP 接口:只有会话事件带 turn——前端据此才知道该把错误 挂在哪条消息下面。轮询拿到的是「最近 N 条错误」,配上当前轮次就会把三天前 的故障挂到今天的新消息下面。

这一轮同一类错误只显示一行。 一轮里可能连续降级好几次(每个决策点各一次), 逐条渲染会在消息尾部堆出四五行几乎一样的红字,反而看不清「这一轮到底坏在哪」。

看不到错误的三种情况(宁可少显示,也不挂错消息):

情形为什么看不到
思维链翻译门槛出错翻译链路没有会话上下文,归不到具体轮次
读不到当前轮次宁可只进日志,也不把错误挂到别的消息下面
宿主没有该扩展位老版本 DSH 没有消息尾部这个位置可挂

错误永远不进模型可见的上下文。 它们是给人看的注解,不是给模型看的。

Errors in the message tail (English)

Each degradation is written into the session as a plugin:decision-engine/error record carrying its turn, folded by a conversation Definition into "the errors of this turn", and rendered by the conversation.chat.turnTail slot. That is why the error lands on the message it belongs to instead of on "whatever failed recently" — only a session event carries the turn number the UI needs.

One line per error kind per turn (a turn can degrade several times; showing all of them would bury the actual problem). Errors never enter the model's visible context: they are annotations for humans.

决策点(参与哪些环节)/ Decision points (which stages it joins)

这个插件的定位不是「工具调用风险闸门」,而是一个便宜的判断层:用一次 约 1.9k token 的小模型调用,去替代或阻止更贵的 LLM 开销。因此判断必须克制—— 只在「替代更贵的调用」或「阻止更大的开销」处介入,绝不为了「多一道检查」而介入。

This plugin is positioned not as a "tool-call risk gate" but as a cheap judgement layer: one ~1.9k-token small-model call replacing or preventing more expensive LLM work. So judgment must be restrained — intervene only where a call replaces something more expensive, or prevents a bigger expense — never just to add one more check.

八个决策点全部已实装,由你决定让决策模型进入哪些环节。

All eight decision points are fully implemented — you decide which stages the decision model joins.

#决策点挂载位置触发闸(为什么不是每次都问)生效方式省 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 工具且候选 ≥2deny + 指路省误加载的技能正文
8思维链翻译门槛包住 translator.translate()本地预筛 + 同文本缓存跳过并返回原文按条省翻译调用
#Decision pointHookTrigger gate (why not every call)What it enforcesToken-saving mechanism
1Tool authorizationtools/pre-executeRead-only tools / auto presets / excluded tools pass straight throughask / denyReplaces auto-review's full-context examination
2Model routingagent/requestFirst N steps of a turn only + skip when the route is unchanged + skip on new user inputReplaces provider/modelMechanical steps sink to a cheaper model
3Subtask delegationagent/pre-stepOnly the first step of a turn, and only with new user inputInjects one hintOne avoided spawn = one whole context saved
4Context compactionagent/pre-step → ctx.compactionOnly when measured tokens reach 70% of the trigger lineRuns deterministic pruning first so the summarization is skippedSaves one summarization call
5Turn continuationagent/turn-stoppingAt most one steer per turnagent.steer()Avoids a wasted full turn
6Retrieval necessitytools/pre-executeRetrieval tools only; zero overhead for everything elsedenySaves search and result tokens
7Skill selectiontools/pre-executeOnly the skill tool, and only with ≥2 candidatesdeny + redirectionSaves the body of a wrongly loaded skill
8COT translation gateWraps translator.translate()Local pre-filter + same-text cacheSkips and returns the originalSaves translation calls per message

三态的含义 / The three modes

模式行为
关闭连请求都不发。省 token,也省那约 2 秒延迟
观察照常判断并记录「本该怎样」,但不改变任何行为
生效按判断执行该点的处置
ModeBehavior
offNo request is sent at all. Saves tokens and the ~2s latency
observeJudges and records what would have happened, but changes no behavior
enforceExecutes that point's outcome per the judgment

新接入的决策点都应当先跑 观察,确认与人工判断一致后再切 生效。

Newly wired decision points should all run in observe first, and only switch to enforce once their judgments match human review.

阈值是实测校准的,不是拍脑袋的 / Thresholds are calibrated on measurements, not guesses

每个决策点的阈值都用真实端点跑过对照样本(「该判 A」与「该判 B」成对输入), 只有两类输入真的分得开才留着。校准过程发现并修掉了三个问题,值得记下来:

Every point's thresholds were run against paired samples on a real endpoint ("should judge A" vs "should judge B"), and a gate stays only if the two kinds of input really separate. Calibration surfaced and fixed three problems worth recording:

  1. choice 的 confidence 是被选中项的概率。三选项平分时天然只有 0.33, 所以分派点的门槛不能设 0.6,否则永远判不出 advise。现在按「显著高于均分」 取 0.5。

  2. question 的问法决定一切。原来问「这个信息是否已知道」,两类输入给出 0.30 / 0.12 —— 完全没有区分力。改问「recent 里是否逐字覆盖了这个事实, 只依据那段文本回答」之后变成 0.92 / 0.01。

  3. 判据顺序会改变结果。轮次延续若先看 closed,「明确未完成」那组 因为 closed=0.28 不达线就会掉进默认 stop——而它恰恰该续轮。 现在先看 unfinished。

  4. choice's confidence is the probability of the picked option. With three choices splitting evenly it is naturally 0.33, so the delegation floor can never be 0.6 — advise would be unreachable. It now requires "clearly above a uniform split": 0.5.

  5. The question's wording decides everything. Asking "is this information already known?" gave 0.30 / 0.12 on the two input kinds — zero discriminative power. Asking "does recent cover this fact verbatim — answer only from that text" flipped it to 0.92 / 0.01.

  6. Check order changes outcomes. If turn continuation tests closed first, the "clearly unfinished" group misses the line at closed=0.28 and falls through to the default stop — exactly the case that should continue. unfinished is checked first now.

校准后的实测对照(scripts/verify-endpoint.mjs + 成对样本):

Post-calibration measured pairs (scripts/verify-endpoint.mjs + paired samples):

决策点输入 A判定输入 B判定
模型路由机械步骤switch新架构设计keep
子任务分派三个包并行补测试advise变量改名inline
上下文压缩结论已落地prune对话承载状态summarize
轮次延续任务已完成stop明确还有三步continue
检索必要性上下文已覆盖skip完全没提过allow
思维链翻译英文→中文translate中文→中文skip
Decision pointInput AVerdictInput BVerdict
Model routingmechanical stepswitchnew architecture designkeep
Subtask delegationtests for three packages in paralleladviserenaming a variableinline
Context compactionconclusions already landedprunethe conversation carries statesummarize
Turn continuationtask completestopclearly three steps leftcontinue
Retrieval necessityalready covered in contextskipnever mentionedallow
COT translationEnglish → ChinesetranslateChinese → Chineseskip

为什么不是「所有 hook 都开」/ Why not "all hooks on"

决策模型每次约 1.9k token、p50 约 0.5 秒。agent/pre-step 与 agent/request 都是每步触发,两个都开且不加闸,等于每步多等 1 秒以上。所以热路径上的 决策点(注册表里标了 hotPath,配置页显示「热路径」角标)都带明确的触发闸, 不能无条件调用。

The decision model costs about 1.9k tokens and p50 ≈ 0.5s per call. agent/pre-step and agent/request both fire every step; running both without gates would add over a second of waiting per step. So hot-path points (marked hotPath in the registry, shown with a "hot path" badge in the config page) all carry explicit trigger gates and can never be called unconditionally.

护栏是双层缓存(见下节):命中缓存就不发请求,把热路径上的重复调用挡掉。 本插件不设每小时的 token 预算——一次决策约 1.9k token,命中缓存后几乎为零, 不值得用「超限即停止判断」的方式省,那会让守卫在最需要它的时刻失效。

The guardrail is the two-layer cache (next section): a cache hit sends no request, blocking repeated calls on the hot path. This plugin deliberately has no hourly token budget — one decision is ~1.9k tokens and near zero once cached; saving via "stop judging when over budget" would disable the guard exactly when it is needed most, so it isn't worth it.

模型选择 / Model selection

配置页的「模型」是下拉框,由服务商实时提供,另有「自动(服务端默认)」选项。

The "Model" field on the config page is a dropdown, served live by the provider, plus an "auto (server default)" option.

默认就是「自动」(config.yml 里 model: 留空):插件不发送该字段, 服务端用它当前的默认决策模型。实测省略 model 返回 200,响应里回填实际使用的模型名:

"Auto" is the default (model: left blank in config.yml): the plugin does not send the field and the server uses its current default decision model. Measured: omitting model returns 200 and the response names the model actually used:

{"answers":{...},"model":"startlux-decision-27b-autothink","usage":{...}}

这样即便服务商将来更换模型名,留空配置也不会因此 400。

This way, even if the provider renames its model in the future, a blank config cannot break with a 400.

注意:config.yml 里的 model: 必须写出来但留空(model: 后面不跟值)。 解析器把显式空值保留为 "";若整行删掉,键会缺失并回落到内置的硬编码模型名, 「自动」就不生效了。

Note: model: in config.yml must be present but empty (write the key, put no value after it). The parser keeps an explicit empty value as ""; delete the whole line and the key goes missing, falling back to the built-in hardcoded model name — "auto" would no longer apply.

列表为何要过滤:bamboonode 的 /v1/models 返回全部 20 个模型,其中多数是 chat 模型,拿去打 /v1/decisions 会 400 model ... is not served by this plugin。 因此对 bamboonode 按 supported_endpoint_types 只保留声明了 StartLux Decision / StartLux SystemOne 的模型。实测 typesafe/jev-1.14 虽声明 jev-decisions,仍被该端点拒绝,因此也排除。

Why the list is filtered: bamboonode's /v1/models returns all 20 models, most of them chat models, which would 400 with model ... is not served by this plugin against /v1/decisions. So for bamboonode the list keeps only models declaring StartLux Decision / StartLux SystemOne in supported_endpoint_types. Measured: typesafe/jev-1.14 declares jev-decisions yet is still rejected by that endpoint, so it is excluded too.

对 system one / custom:自建服务的 /v1/models 往往不带 supported_endpoint_types,继续按类型过滤会得到空列表(用户会以为服务没有可用 模型)。因此这两种 provider 改为「有类型就按类型过滤,没类型就全收」,下拉里也 仍可手填。

For system one / custom: self-hosted /v1/models often carries no supported_endpoint_types, and filtering by type would yield an empty list (users would assume the service has no usable model). So these two providers switch to "filter by type when types exist, accept everything when they don't", and the dropdown still allows a manually typed value.

对 siliconflow / siliconflow-intl:它们的 /v1/models 返回 98 个模型、 没有一条带 supported_endpoint_types,而决策端点只认 Kev-4B / SemIf (chat 模型打过去是 400 Model does not exist.)。类型过滤拿不到任何线索, 全收又等于「96 个选了就失败的选项」,因此按预设里的 knownModels 白名单筛, 下拉里只列这两个。

For siliconflow / siliconflow-intl: their /v1/models returns 98 models with not one carrying supported_endpoint_types, while the decision endpoint only accepts Kev-4B / SemIf (a chat model sent there gets 400 Model does not exist.). Type filtering yields no signal at all, and accepting everything would list "96 options that fail when picked" — so the preset's knownModels whitelist applies, and the dropdown lists only these two.

超时与自动回退 / Timeout and automatic fallback

每次请求都带硬超时(默认 8000ms),超时不是错误,而是降级放行:

Every request carries a hard timeout (default 8000ms); a timeout is not an error but a fail-open degradation:

AbortSignal.timeout(timeoutMs) → 超时 → degrade("timeout") → 钩子 next() 放行
AbortSignal.timeout(timeoutMs) → timeout → degrade("timeout") → hook next() passes through

三层保护,保证守卫永远不会拖住工具执行:

Three layers of protection ensure the guard never holds up a tool execution:

  1. 单次超时:timeoutMs(默认 8000ms,可在配置页调整,最小 1000ms)。 实测该端点 p50≈0.7s、冷启动 p95≈6.4s,8s 覆盖冷启动又不至于让用户干等。

  2. 连续失败冷却:连续 failureThreshold(默认 3)次失败后进入 cooldownMs(默认 60s)冷却,冷却期内直接放行、0ms 返回, 不再对着故障端点反复消耗。

  3. 调用方取消:若上层传入 AbortSignal 且被中止,立即降级返回 caller-aborted。

  4. Single-call timeout: timeoutMs (default 8000ms, adjustable on the config page, minimum 1000ms). Measured on that endpoint: p50 ≈ 0.7s, cold-start p95 ≈ 6.4s — 8s covers cold starts without making users wait.

  5. Consecutive-failure cooldown: after failureThreshold (default 3) consecutive failures the client enters cooldownMs (default 60s); inside cooldown calls pass through directly, returning in 0ms instead of repeatedly burning requests against a broken endpoint.

  6. Caller abort: if an upstream AbortSignal fires, the call degrades immediately with caller-aborted.

实测(指向不可路由地址,timeoutMs: 1500):

Measured (pointing at an unroutable address, timeoutMs: 1500):

第1次: 1504ms → 降级      第4次: 0ms → 降级   ← 冷却生效
第2次: 1502ms → 降级      第5次: 0ms → 降级
第3次: 1502ms → 降级(触发冷却)

Call 1: 1504ms → degraded     Call 4: 0ms → degraded   ← cooldown active
Call 2: 1502ms → degraded     Call 5: 0ms → degraded
Call 3: 1502ms → degraded (triggers the cooldown)

没有配置 key 时根本不发请求,直接 no-key 降级,零延迟。

With no key configured, no request is sent at all — straight no-key degradation, zero latency.

失败降级(fail-open)/ Fail-open degradation

插件的设计原则是任何不确定都退回原生行为,绝不因为守卫自身故障而卡住工具调用。 以下情形全部 next() 放行,并在 telemetry.jsonl 记一条 action=degraded:

The plugin's design rule is any uncertainty falls back to native behavior — a guard failure must never wedge a tool call. All cases below pass through via next(), and one action=degraded line is recorded in telemetry.jsonl:

情形detail
插件未启用(enabled: false)不进入判断,直接放行
没有 API keyno-key
命中失败冷却cooldown
端点 4xx / 5xxhttp-404 / http-500 等(附 endpoint、body)
网络不可达network-error
超时timeout(附 timeoutMs)
返回非 JSON / 非对象invalid-json / invalid-response
缺 answers 字段missing-answers
provider 配置不可用(如 custom 的 base URL 非法)invalid-base-url(附 provider 与原因)
鉴权被拒(401/403)auth-failed(附 provider、status、该 provider 的密钥变量名)
答案形状不符(类型错、缺字段、choice 非法、概率越界/NaN)answer-shape-mismatch
守卫内部抛异常捕获后放行,仅 logger.warn
只读工具 / auto 权限预设 / 被排除工具不判断,直接放行
Situationdetail
Plugin disabled (enabled: false)no judgment, passes through directly
No API keyno-key
Failure cooldown hitcooldown
Endpoint 4xx / 5xxhttp-404 / http-500 … (with endpoint, body)
Network unreachablenetwork-error
Timeouttimeout (with timeoutMs)
Non-JSON / non-object responseinvalid-json / invalid-response
Missing answers fieldmissing-answers
Provider unusable (e.g. invalid custom base URL)invalid-base-url (with provider and reason)
Auth rejected (401/403)auth-failed (with provider, status, that provider's key variable name)
Answer shape mismatch (wrong type, missing field, illegal choice, probability out of range / NaN)answer-shape-mismatch
Exception thrown inside the guardcaught, then passed through, only logger.warn
Read-only tool / auto permission preset / excluded toolno judgment, passes through

默认 mode: shadow 时,即使判断出 deny 也只记录不执行。

In the default mode: shadow, even a judged deny is only recorded, never executed.

省 token 的设计 / Token-saving design

  • 双层缓存:

    • 精确:完整 state + 问题集一致即复用。这是唯一默认开启的层。

    • 同类复用:键是「工具名 + 命令族 + 风险标记」的归一化签名,刻意丢弃 参数内容——命中即意味着不同参数共享同一次裁决。因此这一层 默认关闭(fail-closed),只有调用方显式声明 softCacheable: true 才启用;工具授权(authorize)永久禁用:一次「低风险放行」若被 归一化复用,会扩散到参数完全不同的同类调用上(评审意见 2026-10)。

      历史教训:曾以为「只缓存低风险放行 + 风险标记完全一致」就安全, 但缓存层自己不判断风险——rm -rf /tmp/x 的合理放行会被 rm -rf /Users/chi/xx 原样继承,两者风险标记完全一致。安全决策的 缓存要么绑定完整调用上下文,要么不做。各决策点的裁决现在要么与 完整 state 一一对应,要么不复用。

  • thinkMode 默认 never:实测主要省延迟(约 1.9s vs 7.2s);输入 token 基本不变 (该端点 output_tokens 恒为 0,思考开销未计入计量)。三档可在配置页「思考模式」 下拉切换,或手改 config.yml:

    值行为适用
    never不思考。默认值热路径首选,不想拖慢每次工具调用
    auto仅在模型自己判断需要时思考折中
    always总是思考高风险场景、排查误判时临时使用;每次约多等 5s

    非法值(如 thinkMode: sometimes)会被归一成 never,不会被原样发给服务端: 配置来自自研 YAML 子集解析器,绕过了 Schemastery 校验,因此取值必须在这里兜。

  • 只读工具直接放行:read/grep/glob/todo_write 等不发请求。

  • state 截断:args 走头尾摘要,recent 只带最近 4 条、每条 240 字符。

  • 失败冷却:连续 3 次失败进入 60s 冷却,避免对着故障端点烧钱。

  • 双层缓存:精确命中即不发请求;「同类复用」软层默认关闭、授权点永久禁用。

  • 默认只观察:新决策点先跑 observe,确认与人工判断一致后再切 enforce。

  • Two-layer cache:

    • exact: reused when the full state + question set match — the only layer enabled by default.

    • soft reuse: the key is a normalized signature of "tool name + command family + risk flags" that deliberately drops argument content — a hit means different arguments share one verdict. So this layer ships disabled (fail-closed) and activates only when a caller explicitly declares softCacheable: true; tool authorization (authorize) is permanently excluded: a "low-risk allow" reused across a normalized signature would spread to calls with entirely different arguments (review note, 2026-10).

      Historical lesson: it once seemed safe to "cache only low-risk allows with identical risk flags" — but the cache layer itself does not judge risk. A justified allow for rm -rf /tmp/x would be inherited verbatim by rm -rf /Users/chi/xx, whose risk flags match exactly. A security verdict is either bound to the full call context or not cached at all. Every decision point's verdict now either maps to the complete state one-to-one, or is not reused.

  • thinkMode defaults to never: the measured effect is mainly on latency (~1.9s vs 7.2s); input tokens stay basically unchanged (this endpoint always reports output_tokens = 0, thinking overhead is unmetered). The three values switch via the thinking-mode dropdown on the config page or by editing config.yml:

    ValueBehaviorGood for
    neverNo thinking. The default.Hot paths — don't slow down every tool call
    autoThink only when the model itself decides it's neededMiddle ground
    alwaysAlways thinkHigh-risk scenarios or debugging misjudgments, temporarily; adds ~5s per call

    Illegal values (e.g. thinkMode: sometimes) are normalized to never and never forwarded verbatim: config comes from an in-house YAML-subset parser that bypasses Schemastery validation, so values must be caught here.

  • Read-only tools pass through: read/grep/glob/todo_write and friends send no request.

  • State truncation: args becomes a head/tail digest; recent carries only the last 4 entries at 240 characters each.

  • Failure cooldown: 3 consecutive failures enter a 60s cooldown, avoiding burned money on a broken endpoint.

  • Two-layer cache: an exact hit sends no request; the "soft reuse" layer is off by default and permanently disabled for authorization.

  • Observe by default: new decision points run in observe first, switching to enforce only once their judgments match human review.

统计与对比 / Statistics and comparison

插件设置页分成两个 tab:配置 与 统计与对比。统计单独成页(而不是挤在 配置表单末尾当一小块)——它有自己的整页宽度,因为要回答的是「钱花在哪」与 「比不开插件省了多少」,这两件事都需要一张十来列的对比表才看得清。

统计页回答两个问题:钱花在哪,以及 不开这个插件本该花多少。

页面结构:时间范围 → 汇总指标 → 横向对比(provider × model 的实际消耗) → 节省量对比(观察模式:基线 → 实际)→ 按决策点分账 → 口径脚注。

时间范围 / Time range

预设区间
当日今天 00:00 → 明天 00:00
本周本周一 00:00 → 下周一 00:00(周日整天都在里面)
本月本月 1 日 00:00 → 下月 1 日 00:00
本年本年 1 月 1 日 00:00 → 次年 1 月 1 日 00:00
自定义精确到分钟的起止时刻

区间一律左闭右开([start, end))。相邻区间(本月 → 本年)因此不会在 边界上重复计入同一条记录,而闭区间会。「自定义」的结束时刻按含该整分钟 处理:填 17:45 表示到 17:45:59.999 为止。

「本周」从周一开始(ISO 习惯),不是周日。

界面上会把预设解析成的真实边界写出来(「实际区间:10-26 08:00 → 11-02 08:00」),否则「本周」到底是哪几天,跨月跨年时用户无从核对。

维度与指标 / Dimensions and metrics

按 provider × model 独立统计。每一组给出:

  • input token 总量:累加周期内每次请求的实际输入 token。 优先采用 API 返回的 usage.input_tokens;服务方不报时用内置估算器 (见下)。output token 同样统计——省不省钱要看两头,只看输入会 高估收益。
  • 耗时总量与平均耗时:每次请求从发起至完整响应的端到端毫秒数。 缓存命中的那次耗时为 0(确实没发请求),它省下的时间单独计入 「省时间」。 平均耗时按「真实请求」算,不含缓存命中——命中那次根本没有这段 过程,算进分母会让这个数字随命中率升高而变好看,读的人会以为请求变快了, 而它其实说的是「我们有多久没发请求」。

节省量的基线口径 / What the savings are measured against

节省量只统计「观察」模式的记录。

这不是取舍,是口径问题:节省量是一个反事实(counterfactual)数字, 它回答的是「如果采用决策模型,这次能少花多少」。而反事实只有在两种路径 跑在同一份数据上时才成立——这正是观察模式的定义:

模式普通 LLM 路径节省量
观察(observe)真的跑了,同一批工作✅ 有实测意义
生效(enforce)没发生,决策已落地❌ 只能是事后估算

观察模式下决策模型只记录、不改变行为,于是同一批工作既有「普通 LLM 实际 怎么跑的」(真实数据),也有「决策模型说本该怎么跑」(记录下来的裁决)—— 两边比的是同一批活,结论才成立。

生效模式下普通 LLM 路径根本没有发生,「省了多少」只能是事后估算。把两种 模式的数字混在同一张表里,读者看到的是一个漂亮的节省量,却不知道它是实测 对比还是估算模型算出来的——这是统计里最难自查的一类偏差。因此界面上 明确标注:汇总卡片与节省量对比表的标题都带「(观察)」,页面顶部有 一条常驻提示,页脚还有一份按模式的分账(观察 / 生效 / 关闭 / 未知)。

「省了 0」与「没有可比对的数据」是两件事。 若所选区间里一条观察模式的记录都没有(savings.unavailable),界面会 明确写出「现在是节省量 0 是『没有可比对的数据』,不是『一点也没省』」, 并提示把相关决策点切到「观察」。

基线 = 不启用本插件的缓存与上下文压缩时,同样的工作本应消耗的 token 与时间。 节省量一律相对基线计算(比例的分母是基线,不是实际值)。

本插件只有两类可归因的优化:

  1. 结果缓存:缓存命中时一次请求根本没发出去 → 基线 = 原本那次完整请求 的开销,实际 = 0。缓存里存的就是上一次真实请求的结果,所以基线直接 取它——省下的正是「本来要再付一次」的那份。
  2. 上下文截断:state 被裁到 maxStateChars 之内 → 基线 = 未截断时的 input token。

刻意不把「思考模式」算进节省:think_mode 是用户自己的配置,不是插件的 优化;把它算进来会让节省量随配置摆动,数字失去意义。

没有可归因的优化时,节省量是 0(不是负数,也不是某个假想的数)。

历史记录:老遥测用 shadow / enforce 两值,其中 shadow 就是现在的 observe,回填时会正确映射——老遥测里那 205 条 shadow 记录正是唯一有 实测意义的观察样本,丢掉它们等于把历史里唯一可信的节省量一并抹掉。没有 mode 字段的老记录一律归为「未知模式」,不计入节省量:无法证明它是在 观察模式下产生的,就不能把它的「省下的量」当成实测展示。

升级时的一次性迁移:引入 mode 之前写下的 stats.jsonl 记录没有这个 字段。插件会在启动时按时间戳从老遥测里找回每条记录当时的模式并补上 (实测 2416 条全部对上、无一歧义),否则用户升级后会看到历史节省量凭空 变成 0。匹配不上的记录��持「未知模式」不计入——宁可少算,不可猜。原文件 在改写前会先备份为 stats.jsonl.mode-backup。

横向对比 / Side-by-side comparison

统计页给两张表:

  • 横向对比(provider × model):实际消耗——次数、input、output、总耗时、 均耗时、命中缓存。这张表统计全部模式的记录——它回答的是「钱花在哪」。
  • 节省量对比(观察模式:基线 → 实际):同一批组合,把基线与实际并排放开, 省下多少直接可读。这张表只统计观察模式的记录,且「实际」三列显示的 也是观察模式那部分的实际消耗——它们必须与旁边的「省」列同源,否则读者 会拿生效模式的消耗去减观察模式的节省,得出一个毫无意义的数。

两张表共用一份排序:点击表头换排序字段,再点换升降序。可排序的指标有 input / output / 耗时 / 均耗时 / 次数 / 省 input / 省 output / 省耗时 / 节省比例。

此外还有一张按决策点分账表:同一个 provider/model 上,哪个决策点在 花钱一目了然。

裁决统计(allow / ask / reject)/ Verdict statistics

用量统计回答「钱花在哪」,裁决统计回答另一个问题:「决策模型到底 放行了多少、惊动了用户多少次、拦下了多少次」。两个问题都要看,所以分成 两块,而不是合成一张表。

三格的含义按对待办的用户动作做了什么划分:

分类含义
放行 allow动作照常进行——不打断用户,也不拦下任何东西
询问 ask动作被升级到人工确认(用户会被打断)
拦截 reject动作被拦下,不执行

界面上给三格计数 + 一张按决策点分账的表(哪个点在拦、在问,一目了然)。

为什么不能只看裁决词:九个决策点各有自己的裁决词(delegate / ask / deny / skip / redirect / inline / switch …),而同一个词在不同 点上含义可能相反:

  • retrieval.skip 拦下了一次检索(→ 拦截)
  • cotTranslate.skip 只是没翻译一句话(→ 放行)

只按词统一映射,会把「不翻译」算成「拦截」——得到的「拦截次数」毫无意义。 因此映射表按 (决策点, 裁决词) 定义,集中在一处(lib/outcomes.js), 新增决策点或新增裁决词时在那里登记。

「未裁决」是第四格,但不是第四种裁决:降级(超时、鉴权失败、端点 404) 时插件根本没改变任何行为,把它算成「放行」会虚增放行次数,算成「拦截」 会虚增拦截次数。它单列一格。

未登记的裁决会被显式提示:新增裁决词忘了登记时,数字只会偏小—— 看着仍然合理,是最难自查的一类错。界面上会直接写出「有 N 条裁决未归类」 并指出是哪一个。

与用量统计是两套口径,互不换算:一次降级没有裁决却花了等待时间;一次 缓存命中的裁决照常计入却没花任何 token。

数据存在哪 / Where the data lives

统计写在 ~/.dsh/dsh-decision-engine/stats.jsonl(独立于 telemetry.jsonl),一行一条 JSON,字段:

{"ts":"…","provider":"bamboonode","model":"startlux-decision-27b-autothink",
 "point":"authorize","mode":"observe","action":"delegate","cached":false,
 "inputTokens":449,"outputTokens":77,"durationMs":4777,"baseInputTokens":449,
 "baseOutputTokens":77,"baseDurationMs":4777,
 "inputSource":"api","outputSource":"api"}

mode 记的是这个决策点此刻的三态模式(observe / enforce / off), 它决定这条记录是否计入节省量——只有 observe 计入(见上文「节省量的基线 口径」)。没有这个字段的老记录归为「未知模式」,不计入节省量。

action 记的是这次决策的裁决词(delegate / ask / deny / skip …), 按上文的映射表就能分出 allow / ask / reject。它在拿到裁决之后才写进 统计记录——裁决是 spec.decide() 的产物,而 decide() 要等决策模型的答案 回来才跑得出来,所以统计不能写在发请求的那一步。

为什么不用 telemetry.jsonl:它的字段覆盖不了统计所需——没有 provider (只有 model,同一模型在两个 provider 上跑就分不开)、没有 output token、 没有基线,且耗时口径与缓存命中的 0 混在一起。遥测是给人读的流水账, 统计是给机器读的结构化度量,两者各管各的。

写入是异步的:决策路径上的额外开销只有一次入队 + 一次 JSON 序列化, 不等磁盘。写盘失败只记一条警告,绝不影响主功能。

升级后立刻有历史可看:插件启动时会把旧 telemetry.jsonl 里的历史 回填进 stats.jsonl,只做一次(靠同目录的 stats.backfilled 标记记住)。

回填必须落盘,而不是每次查询现算:现算的话历史只在「统计文件为空」时才有 数据——而统计文件在插件跑起来后第一笔新请求就会被创建,于是用户刚升级就 发现「以前一个月的记录不见了」。落盘之后历史与新记录同处一个文件,随时间 范围一起被统计到。

老遥测没有 provider,但模型名往往能反推出服务商(startlux 系 → bamboonode, Kev/SemIf 系 → 硅基流动),降级记录还能靠 extra.endpoint 的主机名定位(只认 内置预设的地址,自建地址不猜);四条路都走不通的仍显示「未知(历史)」。老遥测 还没有 output token,所以回填的记录会在界面上明确标注「由旧遥测推算」,其 耗时基线取同组真实请求的均值——不会把推算值混充成实测值。

新写的遥测一律自带 provider,回填不再需要猜:统计的归因源头是遥测, 遥测不记 provider,回填就无据可依,只能记 unknown,界面上便是整块 「未知(历史)」——降级记录尤其如此(它们没拿到任何答案,本就没有 model, 连按模型名反推都做不到)。所以降级也要记 provider 与 model:失败发生在 哪个服务商上是事实,藏起来会让「按 provider 对比」看不见谁在超时。

老遥测在缓存命中时记的 inputTokens 是被复用的那次真实请求的计数 (classify 直接返回缓存对象),而那次命中其实一个 token 都没花。回填时 必须把它映射成「基线」而不是「实际消耗」——否则节省量会算出负数。

token 估算 / Token estimation

API 不返回 usage 时(自建 llama.cpp / vLLM 这类服务通常不返回),用内置 的零依赖估算器兜底:

tokens ≈ 90 + 中日韩字符数 + 其它字符数 / 3

系数来自实测标定(2026-10-27,真实 key 打真实端点,14 组不同长度与语种的 请求体),平均相对误差约 6%。固定开销 90 对应 JSON 脚手架与服务端的对话 模板,与用户数据无关。

不引第三方 tokenizer:tiktoken / js-tiktoken 都是几百 KB 的依赖, 而本插件的硬约束是零运行时依赖。真实数字始终优先取 API 返回值。

  • Statistics and comparison: the settings page is split into two tabs — 配置 (Config) and 统计与对比 (Statistics). The statistics tab gets its own full-width page rather than a small block at the bottom of the form: answering "where does the money go" and "how much would this have cost without the plugin" needs a comparison table with a dozen columns.

    Time ranges: today (00:00–24:00), this week (Mon–Sun), this month, this year, or a custom start/end precise to the minute. Ranges are half-open [start, end), so adjacent ranges never double-count a record at the boundary. The resolved boundaries are shown on the page ("实际区间: …"), because "this week" is ambiguous near a month or year boundary.

    Dimensions: provider × model, independently. Each group reports total input tokens, total output tokens, total duration, and average duration. The average is over real requests only — a cache hit never left, so counting it in the denominator would make the number look better as the hit rate rises (it would then be reporting "how long since we called", not "how long a call takes").

    Two tables: 横向对比 (actual consumption per provider × model) and 节省量对比 (baseline vs actual, side by side), plus a per-decision-point breakdown. Clicking a header re-sorts both.

    Savings count only records produced in observe mode. This is a calibration rule, not a preference: savings are a counterfactual ("what would this have cost if the decision model had been adopted"), and a counterfactual only holds when both paths ran on the same work. That is exactly what observe mode is — the decision model records but does not change behaviour, so the same batch of work has both the plain-LLM actual consumption and the decision model's "here is what should have happened". In enforce mode the plain-LLM path never happened, so "how much was saved" can only be estimated after the fact. Mixing the two would show a handsome savings figure whose provenance (measured comparison vs. estimate) the reader cannot tell — the hardest kind of statistical bias to spot and to explain. The UI therefore labels it: the summary cards and the savings table are titled "(观察)", a banner at the top of the page states the rule, and the footnote breaks the range down by mode (observe / enforce / off / unknown).

    Note the distinction the UI makes explicit: "saved 0" and "no comparable data" are different facts. When a range contains no observe record at all (savings.unavailable), the page says so rather than showing a zero that would read as "nothing was saved".

    The baseline is "what the same work would have cost without this plugin's caching and context truncation". Savings are always measured against the baseline (the ratio's denominator is the baseline, not the actual). Only two optimizations are attributable: a cache hit (the request never left — baseline = the full request it replaced) and context truncation (baseline = the untruncated input tokens). think_mode is deliberately excluded: it is the user's configuration, not the plugin's optimization, so including it would make the savings number swing with settings. With no attributable optimization the saving is exactly 0.

    Legacy records are handled rather than dropped: the old telemetry's shadow/enforce pair maps onto observe/enforce, so the 205 historic shadow rows — the only genuinely observed samples — keep contributing real savings. Records with no mode field at all become "unknown" and are excluded from savings: there is no way to prove they were produced in observe mode, and an unprovable number must not be presented as measured.

    A second block, 裁决统计 (verdict statistics), answers a different question: how many verdicts did the decision model actually produce, and what did they do to the user's work? Three classes, defined by what happened to the pending user action:

    ClassMeaning
    allowThe action proceeds — nobody is interrupted, nothing is blocked
    askThe action is escalated to a human confirmation
    rejectThe action is blocked and does not run

    It shows the three counts plus a per-decision-point breakdown (which point asks, which point blocks).

    The mapping cannot key on the verdict word alone. Each point has its own vocabulary (delegate / ask / deny / skip / redirect / inline / switch …) and the same word can mean opposite things on different points: retrieval.skip blocks a search (→ reject) while cotTranslate.skip merely skips translating a sentence (→ allow). Mapping by word alone would count "did not translate" as "blocked", making the rejection count meaningless. The mapping table is therefore keyed by (decision point, verdict word) and lives in one place (lib/outcomes.js); a new point or a new verdict word must be registered there.

    A fourth column, 未裁决 ("no verdict"), is not a fourth kind of verdict: when the plugin degrades (timeout, auth failure, endpoint 404) it changed nothing at all, so counting that as an allow inflates the allow count and counting it as a rejection inflates the rejection count. It gets its own column.

    Unregistered verdicts are surfaced explicitly. Forgetting to register a new verdict word only makes the numbers smaller — they still look plausible, which is the hardest kind of error to notice. The UI states "N verdicts unclassified" and names them.

    Verdict statistics and usage statistics are two separate measures and do not convert into each other: a degradation has no verdict but still cost waiting time, while a cache hit has a real verdict but cost no tokens.

    Data lives in ~/.dsh/dsh-decision-engine/stats.jsonl (separate from telemetry.jsonl, whose fields cannot satisfy these needs — it has no provider, no output tokens, and no baseline). Writes are asynchronous and never block the decision path; a failed write only logs a warning.

    The action field holds the verdict word (delegate / ask / deny / skip …), which is what the allow / ask / reject mapping reads. It is written after the verdict exists: a verdict is the product of spec.decide(), and decide() can only run once the model's answers come back — so the statistics record cannot be written at the moment the request goes out.

    At startup the plugin backfills old telemetry.jsonl history into the stats file, exactly once (a stats.backfilled marker remembers it). The backfill must be persisted rather than recomputed per query: recomputed, history would only exist while the stats file was empty — and the first new request creates it, so a user who just upgraded would watch their month of history vanish. Backfilled rows are explicitly labelled as derived, since the old records lack provider and output tokens.

    When the API reports no usage (self-hosted llama.cpp / vLLM usually don't), a zero-dependency estimator fills in: tokens ≈ 90 + CJK chars + other chars / 3, calibrated against 14 real requests (~6% mean error). Real API values always win.

验证链路 / Verification

自动化验收(三条,全部可重复跑)/ Automated acceptance (three scripts, all repeatable)

pnpm test              # 285 项单元测试:纯函数 + 适配器接线 + providers + 配置往返 + 设置界面渲染 + 缓存回归 + 统计与对比 + 裁决分账
                       # 285 unit tests: pure functions + adapter wiring + providers + config round-trip + settings UI rendering + cache regressions + verdict accounting
pnpm verify:wiring     # 接线检查:用真实 schemastery 加载插件 / wiring check: loads the plugin with the real schemastery
pnpm verify:endpoint   # 8 个决策点逐个打真实端点,校验题型与答案形状 / hits the real endpoint once per decision point, validating question shape and answer parsing
pnpm verify            # 前两条一起跑 / runs the first two together

五个测试文件各管一层,互不依赖宿主:

The five test files each own one layer, none depending on the host:

文件覆盖
test/spec.test.js每个决策点的问法 / 解析 / 处置与阈值边界
test/engine.test.js适配器接线、off/observe/enforce 语义、全部降级分支
test/providers.test.jsprovider 预设、地址拼接、URL 校验、鉴权与请求构造
test/config-file.test.jsconfig.yml 读写往返:保存后再打开的回显、切换不互相覆盖
test/client-ui.test.js设置界面渲染与交互:provider 下拉、custom 字段、错误提示、改动即保存(无保存按钮、改动自动 PUT /config、API Key 失焦提交)
FileCoverage
test/spec.test.jsEach point's question phrasing / parsing / enforcement and threshold boundaries
test/engine.test.jsAdapter wiring, off/observe/enforce semantics, every degradation branch
test/providers.test.jsProvider presets, URL assembly, URL validation, auth and request construction
test/config-file.test.jsconfig.yml read/write round-trip: echo after save-and-reopen, switching without cross-overwrite
test/client-ui.test.jsSettings UI rendering and interaction: provider dropdown, custom fields, error hints, edit-saves-immediately (no save button, edits auto-PUT /config, API key submits on blur)

test/providers.test.js 是 provider 改造的验收用例(不依赖宿主依赖, 裸仓库可直接跑),覆盖六项硬约束:

test/providers.test.js is the acceptance suite for the provider rework (it depends on nothing from the host and runs in a bare checkout), covering six hard constraints:

  1. 内置 system one 可用(默认地址 / /v1/systemone / 自己的密钥变量名)

  2. 内置 bamboonode 行为不变(默认端点、老 baseUrl 覆盖、老模型名)

  3. custom base URL 可用(含端点风格、请求头、密钥来源优先级)

  4. 非法 URL 被拒绝(缺 scheme、非 http(s)、含空格、带查询串、空值)

  5. 末尾斜杠与端点拼接正确、幂等,不出现 /v1/v1 或 //decisions

  6. 请求构造不再硬编码 bamboonode.dev(含源码级断言:lib/index.js 与 lib/client.js 里不得再出现该域名)

  7. Built-in system one works (default URL / /v1/systemone / its own key variable)

  8. Built-in bamboonode behavior is unchanged (default endpoint, legacy baseUrl override, legacy model name)

  9. custom base URL works (including endpoint style, extra headers, key source precedence)

  10. Illegal URLs are rejected (missing scheme, non-http(s), spaces, query strings, empty)

  11. Trailing-slash and endpoint assembly is correct and idempotent — never /v1/v1 or //decisions

  12. Request construction no longer hardcodes bamboonode.dev (with a source-level assertion: the domain must not reappear in lib/index.js or lib/client.js)

verify:wiring 需要 profile 的依赖可解析。仓库里没有 node_modules 时先建链接:

verify:wiring needs the profile's dependencies resolvable. If the repo has no node_modules, create the link first:

ln -s ~/.dsh/profiles/desktop/node_modules node_modules

它检查的是「宿主到底给不给这个服务」——包括缺 translator / sessionTitle 时 apply() 不抛、所有监听器都注册上、每个新点默认 off(升级不改变行为)。

What it checks is "does the host actually provide this service" — including that apply() does not throw when translator / sessionTitle are absent, that every listener gets registered, and that each new point defaults to off (upgrading changes no behavior).

verify:endpoint 要网络与对应 provider 的密钥(bamboonode 默认读 BAMBOO_API_KEY, 可用 DECISION_ENGINE_BASE_URL + DECISION_ENGINE_API_KEY 指向其它 system-one 服务),它逐点校验: 端点接受该点的问题集(choice 必须有 criteria、score 必须 2–10 档、 题型只认三种)、答案能被 parse() 解析、decide() 产出合法 kind。 这一步不能省:题型写错会在运行时 400 并静默降级,光看日志很难发现 「这个点其实一直没生效」。

verify:endpoint needs network access and the provider's key (bamboonode reads BAMBOO_API_KEY by default; DECISION_ENGINE_BASE_URL + DECISION_ENGINE_API_KEY can point it at other system-one services). Per point it validates: the endpoint accepts that point's question set (choice requires criteria, score requires a 2–10 scale, only three question types exist), answers parse via parse(), and decide() yields a legal kind. This step cannot be skipped: a wrong question type 400s at runtime and degrades silently — from the logs alone it is nearly impossible to notice "this point has never actually worked".

手工验证 / Manual verification

  1. 「测试端点」按钮 → 看到 HTTP 200 和 x-oneapi-request-id,说明配置与网络都对; 诊断文本第一行就是当前 provider,接着是实际 POST 的完整端点。

  2. 插件配置页 → 确认 服务提供方、生效端点、模型、密钥 已配置 ✅; 决策点列表里每个点都应有 关闭 / 观察 / 生效 三态(没有「即将支持」)。 若某个点显示 宿主缺服务,说明宿主没有它依赖的服务(如 translator), 该点无法开启——这不是 bug,是不适用的部署。

  3. 把某个点切到 观察,触发一次对应动作 → 看 ~/.dsh/dsh-decision-engine/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 里是原始判断分数
  4. 服务商后台 → 出现 type=2 · Action decision 记录。

  5. The Test endpoint button → seeing HTTP 200 and an x-oneapi-request-id means both config and network are right; the first line of the diagnosis names the current provider, then comes the exact full POST endpoint.

  6. The plugin config page → confirm Provider, effective endpoint, Model, key configured ✅; every point in the decision-point list should offer the off / observe / enforce tri-state (there is no "coming soon"). If a point shows host service missing, the host lacks a service it depends on (e.g. translator) and the point cannot be enabled — that is not a bug, just an inapplicable deployment.

  7. Switch one point to observe, trigger its action → read ~/.dsh/dsh-decision-engine/telemetry.jsonl (on first upgrade, history from the old directory is copied over automatically):

    • action=degraded, detail=no-key → the key was not found
    • detail=http-404 / http-400 → wrong URL or model (the record carries endpoint and body)
    • detail=answer-shape-mismatch → the endpoint's answer shape is off (recheck that point's question set)
    • point=route, action=switch → the point judged "this step should have sunk to a cheaper model"
    • each line's point field names the decision point; scores holds the raw judgment scores
  8. The provider's dashboard → a type=2 · Action decision record appears.

阈值 / Thresholds

工具授权 / Tool authorization

键默认含义
guardDenyConfidence0.85高风险 + 高置信 + 不可逆 → deny
guardIrreversibleDenyFloor0.5判定不可逆的门槛
guardIrreversibleCeiling0.2低风险但不可逆 → 升级询问
guardTaskRelevanceFloor0.5任务相关性否决线:低风险分支仅在 taskP 低于此值时才询问;设 0 停用该分支,设 1 恢复「只看不可逆」的旧行为
guardConfidenceMin0.6中风险置信下限,低于则放行
guardInjectionAskThreshold0.8注入嫌疑 → 升级询问
KeyDefaultMeaning
guardDenyConfidence0.85high risk + high confidence + irreversible → deny
guardIrreversibleDenyFloor0.5the bar for calling something irreversible
guardIrreversibleCeiling0.2low risk but irreversible → escalate to ask
guardTaskRelevanceFloor0.5task-relevance veto: the low-risk branch escalates only when taskP is below this; 0 disables the branch, 1 restores the old "irreversible-only" behavior
guardConfidenceMin0.6confidence floor for medium risk; below it passes
guardInjectionAskThreshold0.8injection suspicion → escalate to ask

其余八个决策点 / The other decision points

全部阈值都在 config.yml 里可覆盖,默认值按实测校准(见上文「阈值是实测 校准的」)。下表列出「改动它会怎样」:

Every threshold can be overridden in config.yml, and the defaults were calibrated on measurements (see "Thresholds are calibrated on measurements" above). The table lists what changing each one does:

键默认调高会怎样调低会怎样
routeCheapProvider / routeCheapModel空留空即不路由,点开着也只记录—
routeMaxStepsPerTurn3每轮判断更多步,更准但更慢更快但后续步骤沿用旧裁决
routeMaxScore0.4更少下沉(更保守)更多下沉(更省但可能降质)
routeMechanicalFloor0.6更难判为机械(更保守)更易下沉
routeRiskFloor0.3风险稍高就保留强模型(更保守)更敢下沉
delegateConfidenceFloor0.5更少建议分派更多建议分派
delegateBlockingFloor0.6更容易判「被阻塞」→ 更少分派更敢分派
delegateParallelFloor0.5组队需要更强并行证据更容易建议组队
compactContextWindow0—设成实际窗口才能按比例提前介入
compactThresholdRatio0.7更晚介入(贴近 compaction 自己的 0.8)更早介入
compactDistilledFloor0.6更少裁剪(更保守)更多裁剪
compactVerbatinFloor0.6更少因「需要原文」而改走摘要更保守(更容易摘要)
turnStopMaxSteers1一轮可续多次(注意空转风险)0 = 永不续轮
turnStopUnfinishedFloor0.6更少续轮更易续轮
turnStopValueFloor0.35更少续轮更易续轮
retrievalCoveredFloor0.5更少跳过检索(更安全)更敢跳过
retrievalWouldAddCeiling0.8更敢跳过(更省)更少跳过
skillPickConfidenceFloor0.5更少改指路更敢改指路
cotProseFloor0.5更少翻译(更省)更敢翻译
cotSameLanguageFloor0.8更少判「已是目标语言」→ 更多翻译更敢跳过
KeyDefaultRaising it…Lowering it…
routeCheapProvider / routeCheapModelemptyblank means no routing: even with the point on, it only records—
routeMaxStepsPerTurn3more steps judged per turn — better but slowerfaster, but later steps keep the old verdict
routeMaxScore0.4fewer sink decisions (more conservative)more sinks (cheaper, may lose quality)
routeMechanicalFloor0.6harder to judge a step mechanical (conservative)easier to sink
routeRiskFloor0.3keeps the strong model even at slightly higher risk (conservative)more willing to sink
delegateConfidenceFloor0.5fewer delegation advicesmore delegation advices
delegateBlockingFloor0.6more often judged "blocked" → fewer delegationsmore willing to delegate
delegateParallelFloor0.5teaming up requires stronger parallel evidenceeasier to suggest teaming up
compactContextWindow0—must be set to the real window for proportional early intervention
compactThresholdRatio0.7intervene later (close to compaction's own 0.8)intervene earlier
compactDistilledFloor0.6less pruning (more conservative)more pruning
compactVerbatinFloor0.6less often switching to summarization because "verbatim is needed"more conservative (more likely to summarize)
turnStopMaxSteers1a turn may continue multiple times (mind the spin risk)0 = never continue
turnStopUnfinishedFloor0.6fewer continuationsmore continuations
turnStopValueFloor0.35fewer continuationsmore continuations
retrievalCoveredFloor0.5fewer retrieval skips (safer)more willing to skip
retrievalWouldAddCeiling0.8more willing to skip (cheaper)fewer skips
skillPickConfidenceFloor0.5fewer redirectionsmore willing to redirect
cotProseFloor0.5less translation (cheaper)more willing to translate
cotSameLanguageFloor0.8less often judged "already in target language" → more translationmore willing to skip

License

MIT

Ähnliche Plugins