dsh-decision-layer 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.en.md +8 -8
  2. package/README.md +10 -10
  3. package/lib/client.js +39 -14
  4. package/lib/host/backend.d.ts +15 -0
  5. package/lib/host/backend.js +149 -0
  6. package/lib/host/config-store.d.ts +12 -0
  7. package/lib/host/config-store.js +254 -0
  8. package/lib/host/danger-gate.d.ts +45 -0
  9. package/lib/host/danger-gate.js +196 -0
  10. package/lib/host/excerpt.d.ts +25 -0
  11. package/lib/host/excerpt.js +62 -0
  12. package/lib/host/index.d.ts +6 -0
  13. package/lib/host/index.js +290 -0
  14. package/lib/host/log-analyze.d.ts +121 -0
  15. package/lib/host/log-analyze.js +254 -0
  16. package/lib/host/log-read.d.ts +4 -0
  17. package/lib/host/log-read.js +34 -0
  18. package/lib/host/log-store.d.ts +15 -0
  19. package/lib/host/log-store.js +98 -0
  20. package/lib/host/loop-guard.d.ts +10 -0
  21. package/lib/host/loop-guard.js +52 -0
  22. package/lib/host/manager-api.d.ts +20 -0
  23. package/lib/host/manager-api.js +174 -0
  24. package/lib/host/narrowing.d.ts +48 -0
  25. package/lib/host/narrowing.js +213 -0
  26. package/lib/host/quality-trend.d.ts +14 -0
  27. package/lib/host/quality-trend.js +22 -0
  28. package/lib/host/self-check.d.ts +27 -0
  29. package/lib/host/self-check.js +104 -0
  30. package/lib/host/session-state.d.ts +19 -0
  31. package/lib/host/session-state.js +288 -0
  32. package/lib/host/state-store.d.ts +14 -0
  33. package/lib/host/state-store.js +109 -0
  34. package/lib/host/steer-once.d.ts +13 -0
  35. package/lib/host/steer-once.js +25 -0
  36. package/lib/host/task-completion.d.ts +29 -0
  37. package/lib/host/task-completion.js +148 -0
  38. package/lib/host/types.d.ts +343 -0
  39. package/lib/host/types.js +11 -0
  40. package/package.json +17 -10
  41. package/service/.gitkeep +0 -0
  42. package/service/backend.mjs +0 -127
  43. package/service/config-store.mjs +0 -311
  44. package/service/danger-gate.mjs +0 -182
  45. package/service/dsh.mjs +0 -288
  46. package/service/log-analyze.mjs +0 -257
  47. package/service/log-read.mjs +0 -27
  48. package/service/log-store.mjs +0 -82
  49. package/service/loop-guard.mjs +0 -51
  50. package/service/manager-api.mjs +0 -137
  51. package/service/narrowing.mjs +0 -226
  52. package/service/quality-trend.mjs +0 -29
  53. package/service/self-check.mjs +0 -131
  54. package/service/session-state.mjs +0 -260
  55. package/service/state-store.mjs +0 -100
  56. package/service/steer-once.mjs +0 -25
  57. package/service/task-completion.mjs +0 -163
package/README.en.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  `dsh-decision-layer` adds structured, explicitly scoped decisions to the DeepSeek Harness (DSH) agent loop, backed by a pluggable adjudication backend. Dangerous-call gating, output self-check, tool narrowing, and loop prevention hook into the agent loop as automatic interventions.
6
6
 
7
- > **0.7.1 is a preview release.** The plugin works, but how much real value the adjudication interventions add is still being explored — every threshold is an unvalidated heuristic default. Treat it as something to observe, not a proven guarantee of impact. See [ROADMAP.md](<ROADMAP.md>) for scope and progress.
7
+ > **0.7.2 is a preview release.** The plugin works, but how much real value the adjudication interventions add is still being explored — every threshold is an unvalidated heuristic default. Treat it as something to observe, not a proven guarantee of impact. See [ROADMAP.md](<ROADMAP.md>) for scope and progress.
8
8
 
9
9
  ## Installation
10
10
 
@@ -22,13 +22,13 @@ Or from a GitHub Release: install directly from the version's tgz asset URL.
22
22
  dsh plugin --profile web add https://github.com/xbzbing/dsh-decision-layer/releases/download/v<version>/dsh-decision-layer-<version>.tgz
23
23
  ```
24
24
 
25
- > The repository no longer commits the built `lib/client.js` ([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)), so installing `github:xbzbing/dsh-decision-layer` without a ref does **not** work — that tree has no `lib/`, and while the host half still loads, the panel never appears. Use the Release tarball on the GitHub side.
25
+ > The repository does not commit build output (`lib/`) ([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)), so installing `github:xbzbing/dsh-decision-layer` without a ref does **not** work — that tree has neither `lib/host/` nor `lib/client.js`, so the plugin cannot load. Use the Release tarball on the GitHub side.
26
26
 
27
27
  Or build locally and install the `file:` path:
28
28
 
29
29
  ```sh
30
30
  npm ci --ignore-scripts
31
- npm run build:client
31
+ npm run build
32
32
  dsh plugin --profile web add file:/absolute/path/dsh-decision-layer
33
33
  ```
34
34
 
@@ -37,9 +37,9 @@ Restart `dsh web` and refresh after installing or updating: the host and client
37
37
  ## Features
38
38
 
39
39
  - **Manual verdicts** — `decision_evaluate` and the `decision-layer` skill submit `noul` / `score` / `choice` questions; `decision_check_connection` tests connectivity. Manual calls do not count as automatic decisions and do not change agent behavior.
40
- - **Dangerous-call gate** — before a destructive tool call, a redacted summary asks the backend for a verdict; only a high-confidence deny blocks the call. Allow, ask, low confidence, backend failure, and invalid responses do not intervene and run the host's original path. Rules and the block confidence floor are configurable.
41
- - **Output self-check and quality trend** — scores the final reply at turn end (0–2), observe-only by default; an opt-in low-score re-prompt fires at most once per turn. Several consecutive high-confidence low scores tint the dock trigger grey → amber → red as a reminder.
42
- - **Tool narrowing** — at turn start, drops optional tools by task relevance, intersecting the host-allowed set and never expanding it; core tools and keep-prefix matches are always kept, over-cap falls back to observe-only, too-few-kept is treated as untrustworthy and skipped, and small-context backends are batched automatically. Enforced by default; observe-only is available.
40
+ - **Dangerous-call gate** — before a destructive tool call, a redacted summary asks the backend for a verdict; only a high-confidence deny blocks the call. Allow, ask, low confidence, backend failure, and invalid responses do not intervene and run the host's original path. Rules and the block confidence floor are configurable. Gate rows carry the redacted command/path plus this turn's task excerpt, so a block can be reviewed in context.
41
+ - **Output self-check and quality trend** — scores the final reply at turn end (0–2), observe-only by default; an opt-in low-score re-prompt fires at most once per turn. Several consecutive high-confidence low scores tint the dock trigger grey → amber → red as a reminder. Self-check rows carry the turn number and the same task excerpt, so you can tell which turn was scored.
42
+ - **Tool narrowing** — at turn start, drops optional tools by task relevance, intersecting the host-allowed set and never expanding it; core tools and keep-prefix matches are always kept, over-cap falls back to observe-only, too-few-kept is treated as untrustworthy and skipped, and small-context backends are batched automatically. Enforced by default; observe-only is available. Narrowing logs include the turn number and an expandable task excerpt, so you can assess the dropped tools against the task; older logs are not backfilled.
43
43
  - **Task-completion review** — when the user spells out multiple delivery conditions, each is judged satisfied / unsatisfied / insufficient evidence, with tool evidence primary. Observe-only by default, with an opt-in re-prompt.
44
44
  - **Loop guard** — denies the same call repeated consecutively past a threshold; deterministic, local, and backend-free. The per-feature switches do not affect it, but the session master switch pauses it too.
45
45
  - **Session panel and decision-analysis view** — the panel shows automatic-decision metrics, the session master switch, and the recent log; the "Decision analysis" tab shows a per-session process profile, trend backtest, and decision accuracy, with per-row ratings and a session intervention switch.
@@ -58,10 +58,10 @@ Once a key is configured, the plugin sends adjudication requests to the remote e
58
58
 
59
59
  User-message text may include context the host injects, such as workspace instructions (AGENTS.md), runtime details, and memory snippets. This content leaves your machine, and the remote provider may log, retain, or otherwise use it depending on its terms; redaction and truncation reduce risk but are not anonymization.
60
60
 
61
- Two kinds of data are also stored locally for 30 days: decision logs in `~/.config/dsh-decision-layer/logs/` and session snapshots in `~/.config/dsh-decision-layer/state/`. They include redacted command/path summaries and short request subjects; delete them yourself if needed.
61
+ Two kinds of data are also stored locally for 30 days: decision logs in `~/.config/dsh-decision-layer/logs/` and session snapshots in `~/.config/dsh-decision-layer/state/`. They include redacted command/path summaries; the gate, self-check and narrowing rows carry a task excerpt (up to 512 characters, with injected memory/system-reminder blocks removed and common credential forms masked), the self-check and narrowing rows also carry the turn number, and completion rows hold only counts. The excerpts are written to the local logs/snapshots and shown in the local session panel and decision-analysis view only — outbound adjudication content is the list above; delete them yourself if needed.
62
62
 
63
63
  **If your session content is sensitive, prefer a locally deployed model** so adjudication data never leaves the machine. `tev1` is served natively by ollama at `/v1/systemone`; see [Running with a local tev1](<docs/en/deploy-tev1-with-plugin.md>). Any other local model works as long as it exposes a compatible `/v1/systemone` endpoint, pointed to from the config page.
64
64
 
65
65
  ## Safety boundary and development
66
66
 
67
- Automatic intervention only subtracts: the gate only denies, narrowing only removes tools, and neither grants new permissions. The gate only blocks on a high-confidence deny; on backend failure, low confidence, or an invalid response it does not intervene and runs the host's original path. Self-check, narrowing, and loop-guard errors skip intervention. The redacted gate summary sent to the backend carries only tool name and command/path with secret redaction; the plugin never reads file content, but the command itself (for example a heredoc or text written with echo) is included as-is. Node.js `>=20.11`; `npm test` runs the tests, `npm run build:client` builds `lib/client.js` (this is also where strict `tsc` runs), and `npm run check` runs both. **Build output is not committed** ([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)): `lib/` is gitignored and the source is the only source of truth. Releases run through `.github/workflows/release.yml`, which builds, gates, `npm pack`s and then asserts with `scripts/assert-pack.mjs` that the tarball really carries `lib/client.js` and leaks no sources — only then does it publish to npm or open a Release. It runs when a version bump lands on main and publishes through npm Trusted Publishing (OIDC), so no long-lived token is involved. Architecture decisions live in [docs/adr/](<docs/adr/README.md>): why each rule exists and what was rejected. 0.7.1 is a preview: the interventions work, but their real value and thresholds are still being explored — feedback on misjudgments and feel observed in the decision log is welcome.
67
+ Automatic intervention only subtracts: the gate only denies, narrowing only removes tools, and neither grants new permissions. The gate only blocks on a high-confidence deny; on backend failure, low confidence, or an invalid response it does not intervene and runs the host's original path. Self-check, narrowing, and loop-guard errors skip intervention. The redacted gate summary sent to the backend carries only tool name and command/path with secret redaction; the plugin never reads file content, but the command itself (for example a heredoc or text written with echo) is included as-is. Both halves are TypeScript under strict mode: `npm run build` type-checks with `tsc` and emits `lib/host/*.js` plus `lib/client.js` ([ADR-0015](<docs/adr/0015-typed-host-build.md>)); `npm test` builds first and then runs the tests, and `npm run check` matches CI (build, tests, and the packed-tarball check). Node.js `>=20.11` at runtime. **Build output is not committed** ([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)): `lib/` is gitignored and the source is the only source of truth. Releases run through `.github/workflows/release.yml`, which builds, gates, `npm pack`s and then asserts with `scripts/assert-pack.mjs` that the tarball really carries the host entry and `lib/client.js`, and leaks no sources — only then does it publish to npm or open a Release. It runs when a version bump lands on main and publishes through npm Trusted Publishing (OIDC), so no long-lived token is involved. Architecture decisions live in [docs/adr/](<docs/adr/README.md>): why each rule exists and what was rejected. 0.7.2 is a preview: the interventions work, but their real value and thresholds are still being explored — feedback on misjudgments and feel observed in the decision log is welcome.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  DeepSeek Harness(DSH)的结构化裁决插件。它把有明确判断标准的问题交给同协议的裁决后端,供 agent 在需要时使用,并在 agent loop 上接入危险门控、产出自检、工具收窄和防循环等自动介入。
6
6
 
7
- > **0.7.1 是体验版。** 插件功能已可用,但裁决介入能带来多少实际价值仍在探索中——各决策点的阈值都是未经验证的启发式默认值,建议先以观察心态体验,别当成经过验证的效果保证。功能与后续进度以 [ROADMAP.md](<ROADMAP.md>) 为准。
7
+ > **0.7.2 是体验版。** 插件功能已可用,但裁决介入能带来多少实际价值仍在探索中——各决策点的阈值都是未经验证的启发式默认值,建议先以观察心态体验,别当成经过验证的效果保证。功能与后续进度以 [ROADMAP.md](<ROADMAP.md>) 为准。
8
8
 
9
9
  ## 安装
10
10
 
@@ -22,13 +22,13 @@ dsh plugin --profile web add dsh-decision-layer@latest
22
22
  dsh plugin --profile web add https://github.com/xbzbing/dsh-decision-layer/releases/download/v<版本>/dsh-decision-layer-<版本>.tgz
23
23
  ```
24
24
 
25
- > 仓库不再提交构建产物 `lib/client.js`([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)),因此**不带 ref 的 `github:xbzbing/dsh-decision-layer` 安装不可用**——那样拿到的是没有 `lib/` 的源码树,host 半仍能加载,但面板不会出现。GitHub 侧请用上面的 Release 附件。
25
+ > 仓库不再提交构建产物 `lib/`([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)),因此**不带 ref 的 `github:xbzbing/dsh-decision-layer` 安装不可用**——那样拿到的是没有 `lib/host/` 与 `lib/client.js` 的源码树,插件加载不了。GitHub 侧请用上面的 Release 附件。
26
26
 
27
27
  也可从源码本地构建后用 `file:` 安装:
28
28
 
29
29
  ```sh
30
30
  npm ci --ignore-scripts
31
- npm run build:client
31
+ npm run build
32
32
  dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
33
33
  ```
34
34
 
@@ -37,9 +37,9 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
37
37
  ## 功能
38
38
 
39
39
  - **手动裁决**:`decision_evaluate` 工具与 `decision-layer` skill 可显式提交 `noul` / `score` / `choice` 问题,`decision_check_connection` 做连通测试;手动调用不计入自动裁决指标,也不改变 agent 行为。
40
- - **危险门控**:破坏性工具调用执行前用脱敏摘要请后端裁决,只有高置信「拒绝」才拦截;放行、询问、低置信、后端失败或响应不合法一律不介入,按宿主原路径执行。危险规则与拦截置信度门槛可配。
41
- - **产出自检与质量趋势**:回合结束给最终回复打分(0–2),默认只记录;可开启低分补完,同一回合最多补一次。连续多次高置信低分时,dock 触发器图标会从灰变橙、变红作为提醒。
42
- - **工具收窄**:回合开始时按任务相关性摘掉可选工具,只在宿主已允许的集合内取交集、不扩权;核心工具与保留前缀命中的工具始终保留,可选工具过多时自动降为仅观察、保留过少时视为判定不可信并跳过,对小上下文后端自动分批。默认真正生效,也可切到仅观察。
40
+ - **危险门控**:破坏性工具调用执行前用脱敏摘要请后端裁决,只有高置信「拒绝」才拦截;放行、询问、低置信、后端失败或响应不合法一律不介入,按宿主原路径执行。危险规则与拦截置信度门槛可配。门控日志带脱敏命令 / 路径与本轮任务节选,便于回看拦截是否合理。
41
+ - **产出自检与质量趋势**:回合结束给最终回复打分(0–2),默认只记录;可开启低分补完,同一回合最多补一次。连续多次高置信低分时,dock 触发器图标会从灰变橙、变红作为提醒。自检日志带回合号与同一套任务节选,便于对照是哪一轮被打分。
42
+ - **工具收窄**:回合开始时按任务相关性摘掉可选工具,只在宿主已允许的集合内取交集、不扩权;核心工具与保留前缀命中的工具始终保留,可选工具过多时自动降为仅观察、保留过少时视为判定不可信并跳过,对小上下文后端自动分批。默认真正生效,也可切到仅观察。收窄日志附带回合号和可展开的任务节选,便于对照被摘除的工具评估判断是否合理;旧日志不会补录场景。
43
43
  - **任务完成核对**:用户明确列出多个交付条件时,逐项判定「已满足 / 未满足 / 证据不足」,以工具证据为准;默认只观察,可配补完。
44
44
  - **防循环**:同一调用连续重复超过阈值即拒绝,确定性本地逻辑,不依赖后端;不受各决策点开关影响,但会话总开关关闭时同样暂停。
45
45
  - **会话面板与决策分析页**:面板展示自动裁决指标、会话总开关和最近日志;「决策分析」tab 按会话展示过程画像、趋势回测与判断准确度,并支持逐条标注和开关本会话的自动介入。
@@ -58,7 +58,7 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
58
58
 
59
59
  user 消息的文本里可能含有宿主注入的上下文,例如工作区指令(AGENTS.md)、运行时信息和记忆片段。这些内容会离开本机,远程服务提供方可能记录、留存或用于其他用途,具体取决于其服务条款;脱敏和截断只降低风险,不等于匿名化。
60
60
 
61
- 本地还会落盘两类数据,各保留 30 天:决策日志在 `~/.config/dsh-decision-layer/logs/`,会话快照在 `~/.config/dsh-decision-layer/state/`,其中包含脱敏后的命令 / 路径和请求主题摘要,可按需自行清理。
61
+ 本地还会落盘两类数据,各保留 30 天:决策日志在 `~/.config/dsh-decision-layer/logs/`,会话快照在 `~/.config/dsh-decision-layer/state/`,其中包含脱敏后的命令 / 路径;门控、自检与收窄带任务节选(最多 512 字符,过滤记忆 / 系统提醒注入块并遮蔽常见凭据写法),自检与收窄另带回合号,完成核对目前只有条件计数。任务节选只写入本地日志 / 快照并展示在本地会话面板和决策分析页,不随裁决请求外发(外发内容以上一节逐项列出为准),可按需自行清理。
62
62
 
63
63
  **会话内容敏感时,推荐改用本地部署的模型**,让裁决数据不出本机。`tev1` 可经 ollama 原生的 `/v1/systemone` 端点直接接入,部署步骤见 [用本地 tev1 配合插件(部署指南)](<docs/zh/deploy-tev1-with-plugin.md>);其他本地模型只要提供兼容 `/v1/systemone` 的端点,同样可以在配置页把服务地址指向本机。
64
64
 
@@ -68,8 +68,8 @@ user 消息的文本里可能含有宿主注入的上下文,例如工作区指
68
68
 
69
69
  ## 开发
70
70
 
71
- 服务端使用 Node.js `>=20.11`(`.mjs`,无需构建);客户端用 TypeScript + esbuild 构建。`npm test` 跑本地测试,`npm run build:client` 生成 `lib/client.js`(`tsc` 严格模式就在这一步),`npm run check` 跑完两者。
71
+ 服务端与客户端都是 TypeScript(严格模式):`npm run build` 用 `tsc` 检查类型、esbuild 产出 `lib/host/*.js` 与 `lib/client.js`([ADR-0015](<docs/adr/0015-typed-host-build.md>)),`npm test` 先构建再跑本地测试,`npm run check` 与 CI 等价(构建 + 测试 + `npm pack` 断言)。运行期需要 Node.js `>=20.11`。
72
72
 
73
- **构建产物不入库**([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)):`lib/` 已被 `.gitignore` 覆盖,源码是唯一事实来源。发布由 GitHub Actions 的 `release.yml` 完成——构建、跑门禁、`npm pack`,并用 `scripts/assert-pack.mjs` 断言产物里确实有 `lib/client.js` 且没有源码泄漏,之后才发 npm / 建 Release。该 workflow 在版本号变更推入 main 时自动执行;npm 侧使用 Trusted Publishing(OIDC),不依赖长期 token。
73
+ **构建产物不入库**([ADR-0012](<docs/adr/0012-distribution-via-github-actions.md>)):`lib/` 已被 `.gitignore` 覆盖,源码是唯一事实来源。发布由 GitHub Actions 的 `release.yml` 完成——构建、跑门禁、`npm pack`,并用 `scripts/assert-pack.mjs` 断言产物里确实有 host 入口与 `lib/client.js`、且没有源码泄漏,之后才发 npm / 建 Release。该 workflow 在版本号变更推入 main 时自动执行;npm 侧使用 Trusted Publishing(OIDC),不依赖长期 token。
74
74
 
75
- 架构决策记录见 [docs/adr/](<docs/adr/README.md>):每条规则为什么存在、当时放弃了哪些备选。0.7.1 为体验版:介入能力已可用,但实际价值与阈值仍在探索,欢迎反馈决策日志观察到的误判与体感。
75
+ 架构决策记录见 [docs/adr/](<docs/adr/README.md>):每条规则为什么存在、当时放弃了哪些备选。0.7.2 为体验版:介入能力已可用,但实际价值与阈值仍在探索,欢迎反馈决策日志观察到的误判与体感。
package/lib/client.js CHANGED
@@ -1,8 +1,6 @@
1
- window.__ModuleLoader__.load({
2
- id: 'dsh-decision-layer',
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
1
+ window.__ModuleLoader__.load({ id: 'dsh-decision-layer', factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ "use strict";
6
4
  var __defProp = Object.defineProperty;
7
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -82,6 +80,8 @@ var zh = {
82
80
  logCheck: "\u81EA\u68C0",
83
81
  logNarrow: "\u6536\u7A84",
84
82
  logComplete: "\u5B8C\u6210\u6838\u5BF9",
83
+ logTurn: "\u56DE\u5408 {turn}",
84
+ logTask: "\u573A\u666F\uFF08\u4EFB\u52A1\u8282\u9009\uFF09\uFF1A",
85
85
  logSuggested: "\u6A21\u578B\u5EFA\u8BAE",
86
86
  logEvalFailed: "\u8BC4\u4F30\u5931\u8D25",
87
87
  logScore: "\u5F97\u5206",
@@ -91,7 +91,7 @@ var zh = {
91
91
  logPathLabel: "\u8DEF\u5F84\uFF1A",
92
92
  logSubjectLabel: "\u8BF7\u6C42\uFF1A",
93
93
  logScoreHint: "\u5F97\u5206\u4E3A\u5404\u6863\u4F4D\u6309\u6982\u7387\u52A0\u6743\u7684\u7ED3\u679C\uFF0C\u53EF\u80FD\u662F\u5C0F\u6570\uFF08\u5982 1.4\uFF09\uFF0C\u6EE1\u5206\u4E3A\u9876\u6863\u53F7 2\u3002",
94
- logCheckHint: "\u6BCF\u56DE\u5408\u7ED3\u675F\u540E\uFF0C\u628A\u8FD9\u8F6E AI \u7684\u6700\u7EC8\u56DE\u590D\u4EA4\u7ED9\u88C1\u51B3\u6A21\u578B\uFF0C\u6309\u300C\u4E25\u91CD\u9057\u6F0F/\u81EA\u76F8\u77DB\u76FE \u2192 \u6709\u5C0F\u9057\u6F0F \u2192 \u5B8C\u6574\u51C6\u786E\u300D\u4E09\u6863\u6253\u4E00\u4E2A 0\u20132 \u7684\u8D28\u91CF\u5206\uFF0C\u7528\u6765\u53D1\u73B0\u9057\u6F0F\u6216\u77DB\u76FE\u3002",
94
+ logCheckHint: "\u6BCF\u56DE\u5408\u7ED3\u675F\u540E\uFF0C\u628A\u8FD9\u8F6E AI \u7684\u6700\u7EC8\u56DE\u590D\u4EA4\u7ED9\u88C1\u51B3\u6A21\u578B\uFF0C\u6309\u300C\u4E25\u91CD\u9057\u6F0F/\u81EA\u76F8\u77DB\u76FE \u2192 \u6709\u5C0F\u9057\u6F0F \u2192 \u5B8C\u6574\u51C6\u786E\u300D\u4E09\u6863\u6253\u4E00\u4E2A 0\u20132 \u7684\u8D28\u91CF\u5206\uFF0C\u7528\u6765\u53D1\u73B0\u9057\u6F0F\u6216\u77DB\u76FE\uFF1B\u81EA\u68C0\u884C\uFF08\u542B\u540E\u7AEF\u5931\u8D25\u884C\uFF09\u4F1A\u5E26\u672C\u56DE\u5408\u7684\u4EFB\u52A1\u8282\u9009\uFF08\u6700\u591A 512 \u5B57\uFF0C\u5DF2\u906E\u853D\u51ED\u636E\uFF09\u3002",
95
95
  logNarrowHint: "\u6BCF\u56DE\u5408\u5F00\u59CB\u65F6\uFF0C\u628A\u8FD9\u8F6E\u4EFB\u52A1\u4E0E\u5F53\u524D\u53EF\u7528\u5DE5\u5177\u9010\u4E2A\u4EA4\u7ED9\u88C1\u51B3\u6A21\u578B\u5224\u65AD\u76F8\u5173\u6027\uFF08noul\uFF09\uFF0C\u6458\u6389\u660E\u663E\u7528\u4E0D\u4E0A\u7684\u5DE5\u5177\uFF0C\u53EA\u5728\u5F53\u524D\u5BBF\u4E3B\u5DF2\u5141\u8BB8\u7684\u96C6\u5408\u5185\u53D6\u4EA4\u96C6\u3001\u7EDD\u4E0D\u6269\u6743\uFF1B\u300C\u4EC5\u89C2\u5BDF\u300D\u53EA\u8BB0\u5F55\u5EFA\u8BAE\uFF0C\u300C\u5DF2\u751F\u6548\u300D\u624D\u771F\u6B63\u5BF9\u672C\u56DE\u5408\u9650\u5236\u5DE5\u5177\u3002",
96
96
  actionDeny: "\u5DF2\u62D2\u7EDD",
97
97
  actionAsk: "\u8F6C\u4EA4\u5BA1\u6279",
@@ -277,6 +277,8 @@ var en = {
277
277
  logCheck: "Self-check",
278
278
  logNarrow: "Narrow",
279
279
  logComplete: "Completion",
280
+ logTurn: "Turn {turn}",
281
+ logTask: "Context (task excerpt): ",
280
282
  logSuggested: "model suggested",
281
283
  logEvalFailed: "evaluation failed",
282
284
  logScore: "score",
@@ -286,7 +288,7 @@ var en = {
286
288
  logPathLabel: "path: ",
287
289
  logSubjectLabel: "request: ",
288
290
  logScoreHint: "The score is a probability-weighted mean across the levels, so it can be fractional (e.g. 1.4); the top score is the highest level number, 2.",
289
- logCheckHint: "After each turn, the AI\u2019s final reply for that turn is sent to the adjudication model, which grades its quality on three levels (severe omission/contradiction \u2192 minor gaps \u2192 complete and accurate) as a 0\u20132 score, to catch omissions or contradictions.",
291
+ logCheckHint: "After each turn, the AI\u2019s final reply for that turn is sent to the adjudication model, which grades its quality on three levels (severe omission/contradiction \u2192 minor gaps \u2192 complete and accurate) as a 0\u20132 score, to catch omissions or contradictions; every self-check row (including a failed one) carries this turn\u2019s task excerpt (up to 512 characters, credentials masked).",
290
292
  logNarrowHint: "At the start of each turn, the current task and each available tool are sent to the adjudication model (a noul per tool) to judge relevance, dropping tools clearly not needed. It only intersects the host-allowed set and never expands it; observe merely logs the suggestion, enforced actually restricts the tools for this turn.",
291
293
  actionDeny: "denied",
292
294
  actionAsk: "sent to approval",
@@ -474,6 +476,12 @@ function gateArgs({ t }, entry) {
474
476
  const preview = value.length > 160 ? `${value.slice(0, 160)}\u2026` : value;
475
477
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "decision-log-cmd", title: `${label}${value}`, children: preview });
476
478
  }
479
+ function turnTag(t, entry) {
480
+ return typeof entry.turn === "number" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
481
+ " \xB7 ",
482
+ t("logTurn", { turn: entry.turn })
483
+ ] }) : null;
484
+ }
477
485
  function renderGate({ t }, entry) {
478
486
  const tool = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "decision-log-tool", children: entry.tool ?? "\u2014" });
479
487
  const args = gateArgs({ t }, entry);
@@ -541,7 +549,10 @@ function renderGate({ t }, entry) {
541
549
  ] });
542
550
  }
543
551
  function renderCheck({ t }, entry) {
544
- const label = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { title: t("logCheckHint"), children: t("logCheck") });
552
+ const label = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
553
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { title: t("logCheckHint"), children: t("logCheck") }),
554
+ turnTag(t, entry)
555
+ ] });
545
556
  const subject = entry.subject ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-subject", title: `${t("logSubjectLabel")}${entry.subject}`, children: [
546
557
  "\u300C",
547
558
  entry.subject.length > 60 ? `${entry.subject.slice(0, 60)}\u2026` : entry.subject,
@@ -593,7 +604,10 @@ function renderCheck({ t }, entry) {
593
604
  ] });
594
605
  }
595
606
  function renderNarrow({ t }, entry) {
596
- const label = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { title: t("logNarrowHint"), children: t("logNarrow") });
607
+ const label = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
608
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { title: t("logNarrowHint"), children: t("logNarrow") }),
609
+ turnTag(t, entry)
610
+ ] });
597
611
  if (entry.outcome === "error") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
598
612
  label,
599
613
  " ",
@@ -721,7 +735,16 @@ function DecisionLogList({ entries, ratings, onRate, t, newestFirst = true }) {
721
735
  const rows = newestFirst ? entries.slice().reverse() : entries.slice();
722
736
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className: "decision-log", children: rows.map((entry, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: `decision-log-item decision-log-${outcomeTagOf(entry)}`, children: [
723
737
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-log-time", children: time(entry.at) }),
724
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-log-body", children: renderBody({ t }, entry) }),
738
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "decision-log-body", children: [
739
+ renderBody({ t }, entry),
740
+ entry.task ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "decision-log-context", children: [
741
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
742
+ t("logTask"),
743
+ entry.task.length > 60 ? `${entry.task.slice(0, 60)}\u2026` : entry.task
744
+ ] }),
745
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: entry.task })
746
+ ] }) : null
747
+ ] }),
725
748
  entry.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RatingButtons, { id: entry.id, t, ratings, onRate })
726
749
  ] }, entry.id ?? `${entry.at}-${index}`)) });
727
750
  }
@@ -2112,6 +2135,10 @@ var css = `
2112
2135
  .decision-log-tool{font-size:.76rem;padding:.05rem .35rem;border-radius:5px;background:var(--dsw-alias-bg-layer-1,#f2f4f6);color:var(--dsw-alias-label-primary,#202124)}
2113
2136
  .decision-log-cmd{font-size:.74rem;padding:.05rem .35rem;border-radius:5px;background:var(--dsw-alias-bg-layer-1,#f2f4f6);color:var(--dsw-alias-label-secondary,#59636e);word-break:break-all}
2114
2137
  .decision-log-subject{font-size:.74rem;color:var(--dsw-alias-label-secondary,#59636e);font-style:italic}
2138
+ .decision-log-context{margin-top:.25rem;font-size:.74rem;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
2139
+ .decision-log-context summary{cursor:pointer}
2140
+ .decision-log-context summary:focus-visible{outline:2px solid var(--dsw-alias-label-secondary);outline-offset:2px}
2141
+ .decision-log-context p{margin:.25rem 0 0}
2115
2142
  .decision-log-verdict{font-weight:600}
2116
2143
  .decision-log-deny .decision-log-verdict{color:#c0392b}
2117
2144
  .decision-log-ok .decision-log-verdict{color:#1f8a70}
@@ -2297,7 +2324,5 @@ function apply(ctx) {
2297
2324
  (props) => props.view === "summary" ? props.t("summary") : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ConfigForm, { t: props.t })
2298
2325
  ));
2299
2326
  }
2300
-
2301
- return module.exports;
2302
- }
2303
- });
2327
+ return module.exports;
2328
+ } });
@@ -0,0 +1,15 @@
1
+ import type { BackendConfig, EvaluateInput, EvaluateResult } from './types.js';
2
+ export type BackendError = Error & {
3
+ reason: string;
4
+ };
5
+ export declare function validateRequest(input: unknown): EvaluateInput;
6
+ export declare function createBackend({ config, fetcher, timeoutMs, retryDelayMs }: {
7
+ config: () => Promise<BackendConfig>;
8
+ fetcher?: typeof fetch;
9
+ timeoutMs?: number;
10
+ retryDelayMs?: number;
11
+ }): {
12
+ evaluate(input: EvaluateInput, { signal }?: {
13
+ signal?: AbortSignal;
14
+ }): Promise<EvaluateResult>;
15
+ };
@@ -0,0 +1,149 @@
1
+ import { setTimeout as wait } from "node:timers/promises";
2
+ import { DEFAULT_MODEL } from "./config-store.js";
3
+ import { isObject, isProbability } from "./types.js";
4
+ const REQUEST_LIMIT = 256 * 1024;
5
+ const RESPONSE_LIMIT = 1024 * 1024;
6
+ const isContent = (value) => typeof value === "string" ? value.trim().length > 0 : isObject(value) || Array.isArray(value);
7
+ function backendError(reason, message) {
8
+ const error = new Error(message);
9
+ error.reason = reason;
10
+ return error;
11
+ }
12
+ function validateRequest(input) {
13
+ if (!isObject(input) || !isContent(input.state) || !isObject(input.questions)) throw new Error("Invalid state or questions");
14
+ const questions = Object.entries(input.questions);
15
+ if (questions.length < 1 || questions.length > 64) throw new Error("Invalid question count");
16
+ for (const [id, question] of questions) {
17
+ if (!id.trim() || id.length > 160 || !isObject(question) || !isContent(question.instructions)) throw new Error("Invalid question");
18
+ if (question.type === "choice") {
19
+ const criteria = question.criteria;
20
+ if (!isObject(criteria) || Object.keys(criteria).length < 2 || Object.keys(criteria).length > 255 || Object.entries(criteria).some(([key, value]) => !key.trim() || value !== null && !isContent(value))) throw new Error("Invalid choice criteria");
21
+ } else if (question.type === "score") {
22
+ const criteria = question.criteria;
23
+ if (!Array.isArray(criteria) || criteria.length < 2 || criteria.length > 10 || !criteria.every(isContent)) throw new Error("Invalid score criteria");
24
+ } else if (question.type !== "noul" || question.criteria !== void 0 && (!isObject(question.criteria) || Object.keys(question.criteria).some((key) => !["true", "false"].includes(key)))) {
25
+ throw new Error("Invalid question type or criteria");
26
+ }
27
+ }
28
+ let encoded;
29
+ try {
30
+ encoded = JSON.stringify(input, (_key, value) => {
31
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint" || typeof value === "number" && !Number.isFinite(value)) throw new Error("Invalid JSON");
32
+ return value;
33
+ });
34
+ } catch {
35
+ throw new Error("Invalid JSON payload");
36
+ }
37
+ if (Buffer.byteLength(encoded) > REQUEST_LIMIT) throw new Error("Request too large");
38
+ return { state: input.state, questions: input.questions };
39
+ }
40
+ function validateAnswer(value, question) {
41
+ if (!isObject(value) || value.type !== question.type) throw backendError("invalid-response", "Invalid response answer");
42
+ if (question.type === "noul") {
43
+ if (!isProbability(value.noul)) throw backendError("invalid-response", "Invalid response probability");
44
+ return { type: "noul", noul: value.noul };
45
+ }
46
+ const keys = question.type === "choice" ? Object.keys(question.criteria) : question.criteria.map((_, i) => String(i));
47
+ const probabilities = value.probabilities;
48
+ if (!isObject(probabilities) || !isProbability(value.confidence) || Object.keys(probabilities).length !== keys.length || keys.some((key) => !isProbability(probabilities[key])) || Math.abs(keys.reduce((sum, key) => sum + Number(probabilities[key]), 0) - 1) > 0.02) throw backendError("invalid-response", "Invalid response probabilities");
49
+ const common = {
50
+ confidence: value.confidence,
51
+ probabilities: Object.fromEntries(keys.map((key) => [key, probabilities[key]]))
52
+ };
53
+ if (question.type === "choice") {
54
+ if (typeof value.choice !== "string" || !keys.includes(value.choice)) throw backendError("invalid-response", "Invalid response choice");
55
+ return { type: "choice", ...common, choice: value.choice };
56
+ }
57
+ if (typeof value.score !== "number" || !Number.isFinite(value.score) || value.score < 0 || value.score > keys.length - 1) throw backendError("invalid-response", "Invalid response score");
58
+ return { type: "score", ...common, score: value.score };
59
+ }
60
+ function validateResponse(value, questions) {
61
+ if (!isObject(value) || !isObject(value.answers) || typeof value.model !== "string") throw backendError("invalid-response", "Invalid response");
62
+ const raw = value.answers;
63
+ const answers = /* @__PURE__ */ Object.create(null);
64
+ for (const [id, question] of Object.entries(questions)) answers[id] = validateAnswer(raw[id], question);
65
+ const usage = value.usage;
66
+ return { model: value.model, answers, usage: isObject(usage) ? {
67
+ input_tokens: Number.isSafeInteger(usage.input_tokens) ? usage.input_tokens : 0,
68
+ output_tokens: Number.isSafeInteger(usage.output_tokens) ? usage.output_tokens : 0
69
+ } : void 0 };
70
+ }
71
+ async function boundedJson(response) {
72
+ const reader = response.body?.getReader();
73
+ if (!reader) throw backendError("invalid-response", "Invalid response body");
74
+ const chunks = [];
75
+ let size = 0;
76
+ try {
77
+ while (true) {
78
+ const { done, value } = await reader.read();
79
+ if (done) break;
80
+ size += value.byteLength;
81
+ if (size > RESPONSE_LIMIT) throw backendError("invalid-response", "Invalid response size");
82
+ chunks.push(value);
83
+ }
84
+ } finally {
85
+ await reader.cancel().catch(() => {
86
+ });
87
+ }
88
+ try {
89
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
90
+ } catch {
91
+ throw backendError("invalid-response", "Invalid response JSON");
92
+ }
93
+ }
94
+ function createBackend({ config, fetcher = globalThis.fetch, timeoutMs = 2e4, retryDelayMs = 200 }) {
95
+ if (typeof config !== "function" || typeof fetcher !== "function" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || !Number.isSafeInteger(retryDelayMs) || retryDelayMs < 0 || retryDelayMs > 5e3) {
96
+ throw new Error("Invalid backend dependencies");
97
+ }
98
+ return {
99
+ async evaluate(input, { signal } = {}) {
100
+ const { state, questions } = validateRequest(input);
101
+ const settings = await config();
102
+ const key = settings.apiKey?.trim();
103
+ if (!key || /[\r\n]/.test(key)) throw backendError("config", "API key is required");
104
+ let url;
105
+ try {
106
+ url = new URL(settings.url);
107
+ } catch {
108
+ throw backendError("config", "Invalid backend URL");
109
+ }
110
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw backendError("config", "Invalid backend URL");
111
+ if (url.protocol === "http:" && settings.httpApprovedUrl !== settings.url) throw backendError("config", "HTTP URL requires confirmation");
112
+ const model = settings.model || DEFAULT_MODEL;
113
+ if (typeof model !== "string" || !model.trim() || /[\r\n]/.test(model)) throw backendError("config", "Invalid model");
114
+ const endpoint = new URL(url.href);
115
+ endpoint.pathname = `${url.pathname.replace(/\/+$/, "")}/v1/systemone`;
116
+ if (endpoint.origin !== url.origin) throw backendError("config", "Invalid backend endpoint");
117
+ const timeout = AbortSignal.timeout(timeoutMs);
118
+ const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
119
+ for (let attempt = 0; attempt < 3; attempt++) {
120
+ let reply;
121
+ try {
122
+ reply = await fetcher(endpoint.href, {
123
+ method: "POST",
124
+ redirect: "error",
125
+ signal: combined,
126
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
127
+ body: JSON.stringify({ state, questions, model })
128
+ });
129
+ } catch {
130
+ throw backendError("unreachable", "Backend request failed or timed out");
131
+ }
132
+ if (reply.ok) return validateResponse(await boundedJson(reply), questions);
133
+ await reply.body?.cancel().catch(() => {
134
+ });
135
+ if (![429, 529].includes(reply.status) || attempt === 2) throw backendError("http-error", `Backend HTTP ${reply.status}`);
136
+ try {
137
+ await wait(retryDelayMs * (attempt + 1), void 0, { signal: combined });
138
+ } catch {
139
+ throw backendError("unreachable", "Backend request failed or timed out");
140
+ }
141
+ }
142
+ throw backendError("http-error", "Backend retries exhausted");
143
+ }
144
+ };
145
+ }
146
+ export {
147
+ createBackend,
148
+ validateRequest
149
+ };
@@ -0,0 +1,12 @@
1
+ import type { ConfigView, FeatureKey, Features, ResolvedConfig } from './types.js';
2
+ export declare const DEFAULT_URL = "https://api.typesafe.ai";
3
+ export declare const DEFAULT_MODEL = "jev-latest";
4
+ export declare function configPath(env?: Record<string, string | undefined>): string;
5
+ export declare const FEATURE_KEYS: readonly FeatureKey[];
6
+ export declare function featureEnabled(features: Features | undefined, key: FeatureKey): boolean;
7
+ export declare function loadConfig(path?: string): Promise<ConfigView>;
8
+ export declare function resolveConfig({ path, env }?: {
9
+ path?: string;
10
+ env?: Record<string, string | undefined>;
11
+ }): Promise<ResolvedConfig>;
12
+ export declare function saveConfig(input: Record<string, unknown>, path?: string): Promise<ConfigView>;