dsh-decision-layer 0.7.3 → 0.7.4
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.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/docs/en/DETAIL.md +1 -1
- package/docs/zh/DETAIL.md +1 -1
- package/lib/client.js +74 -17
- package/lib/host/decision-context.js +20 -1
- package/lib/host/index.js +1 -1
- package/lib/host/log-analyze.d.ts +2 -1
- package/lib/host/log-analyze.js +3 -2
- package/lib/host/manager-api.d.ts +1 -0
- package/lib/host/manager-api.js +5 -2
- package/lib/host/self-check.js +13 -1
- package/package.json +1 -1
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.
|
|
7
|
+
> **0.7.4 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
|
|
|
@@ -38,8 +38,8 @@ Restart `dsh web` and refresh after installing or updating: the host and client
|
|
|
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
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,
|
|
42
|
-
- **Tool narrowing** — at turn start and again when a new user request arrives inside the same turn, drops optional tools by task relevance, intersecting the host-allowed set and never expanding it; each re-judgement releases the previous restriction first, the restriction is lifted when the turn stops, and notices such as time or background-job updates never trigger one. 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 and step numbers plus an expandable task excerpt, so you can assess the dropped tools against the task; older logs are not backfilled.
|
|
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, and low rows carry a "low score" badge; the decision-analysis tab can show low scores only, so the scored turn is one click away in a long session (low-confidence low scores stay marked as noise to ignore).
|
|
42
|
+
- **Tool narrowing** — at turn start and again when a new user request arrives inside the same turn, drops optional tools by task relevance, intersecting the host-allowed set and never expanding it; each re-judgement releases the previous restriction first, the restriction is lifted when the turn stops, and notices such as time or background-job updates never trigger one. 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 and step numbers plus an expandable task excerpt, so you can assess the dropped tools against the task; older logs are not backfilled. The host fixes the tool set before the decision point for a step fires, so a drop reaches the model from the next step on; narrowing covers the tool channel only — UI fences in the reply body and skill instructions are unaffected.
|
|
43
43
|
- **Task-completion review** — when the user spells out multiple delivery conditions, each is judged satisfied / unsatisfied / insufficient evidence. Conditions come only from the turn's latest request, so a retired list cannot be revived, and this turn's tool evidence is primary (an earlier turn's success does not count; self-reported completion is not evidence). 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.
|
|
@@ -51,7 +51,7 @@ Restart `dsh web` and refresh after installing or updating: the host and client
|
|
|
51
51
|
Once a key is configured, the plugin sends adjudication requests to the remote endpoint `https://api.typesafe.ai` (model `jev-latest`) by default. The payload carries real context:
|
|
52
52
|
|
|
53
53
|
- Gate: the tool name plus redacted command/path summaries (up to 2048 characters each); no conversation content.
|
|
54
|
-
- Self-check: this turn's request, the recent user asks and AI replies it needs for context (background only, each with its own budget, credentials masked), and the turn's final reply (
|
|
54
|
+
- Self-check: this turn's request, the recent user asks and AI replies it needs for context (background only, each with its own budget, credentials masked), and the turn's final reply (opening and closing within a combined ~24K-character budget; the middle of an over-long reply is marked as omitted so the conclusion is not lost).
|
|
55
55
|
- Narrowing: the current request, the necessary recent background, and the latest execution progress (tool name, ok/error, bounded command/path and shell-result excerpts) plus candidate tool names/descriptions.
|
|
56
56
|
- Task-completion review: delivery conditions extracted from this turn's latest user request (up to 20; a turn's earlier requests are not merged) and that request with its necessary background (credentials masked), this turn's tool evidence (tool name, ok/error, bounded command/path, plus a bounded shell-result excerpt with long unlabeled blobs masked again), and the first ~8K characters of the reply. Raw results from file reads, searches and network tools are never sent.
|
|
57
57
|
- Manual verdicts: `decision_evaluate` and the `decision-layer` skill send whatever the agent submits, unchanged.
|
|
@@ -64,4 +64,4 @@ Two kinds of data are also stored locally for 30 days: decision logs in `~/.conf
|
|
|
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. 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.
|
|
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.4 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.
|
|
7
|
+
> **0.7.4 是体验版。** 插件功能已可用,但裁决介入能带来多少实际价值仍在探索中——各决策点的阈值都是未经验证的启发式默认值,建议先以观察心态体验,别当成经过验证的效果保证。功能与后续进度以 [ROADMAP.md](<ROADMAP.md>) 为准。
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
@@ -38,8 +38,8 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
|
|
|
38
38
|
|
|
39
39
|
- **手动裁决**:`decision_evaluate` 工具与 `decision-layer` skill 可显式提交 `noul` / `score` / `choice` 问题,`decision_check_connection` 做连通测试;手动调用不计入自动裁决指标,也不改变 agent 行为。
|
|
40
40
|
- **危险门控**:破坏性工具调用执行前用脱敏摘要请后端裁决,只有高置信「拒绝」才拦截;放行、询问、低置信、后端失败或响应不合法一律不介入,按宿主原路径执行。危险规则与拦截置信度门槛可配。门控日志带脱敏命令 / 路径与本轮任务节选,便于回看拦截是否合理。
|
|
41
|
-
- **产出自检与质量趋势**:回合结束给最终回复打分(0–2),默认只记录;可开启低分补完,同一回合最多补一次。连续多次高置信低分时,dock
|
|
42
|
-
-
|
|
41
|
+
- **产出自检与质量趋势**:回合结束给最终回复打分(0–2),默认只记录;可开启低分补完,同一回合最多补一次。连续多次高置信低分时,dock 触发器图标会从灰变橙、变红作为提醒。自检日志带回合号与同一套任务节选,低分行另有「低评分」标记;决策分析页可一键「只看低评分」,长会话里也能直接定位到被打分的回合(低置信低分仍标为噪声,可忽略)。
|
|
42
|
+
- **工具收窄**:回合开始时、以及同一回合内你提出新要求时,按任务相关性摘掉可选工具,只在宿主已允许的集合内取交集、不扩权;每次重新判断前先释放上一轮的旧限制,回合结束即解除,时间提示、后台通知等非用户要求不会触发重判。核心工具与保留前缀命中的工具始终保留,可选工具过多时自动降为仅观察、保留过少时视为判定不可信并跳过,对小上下文后端自动分批。默认真正生效,也可切到仅观察。收窄日志附带回合号、步骤号和可展开的任务节选,便于对照被摘除的工具评估判断是否合理;旧日志不会补录场景。宿主在本步决策点触发前就已确定工具集,所以摘除从下一步起才进入模型请求;收窄只作用于工具通道——回复正文里的 UI 围栏、技能说明等非工具能力不受影响。
|
|
43
43
|
- **任务完成核对**:用户明确列出多个交付条件时,逐项判定「已满足 / 未满足 / 证据不足」,条件只取本回合最近一次要求(避免旧清单被恢复),以本回合工具证据为准(上一回合的成功不算数,回复自述不算证据);默认只观察,可配补完。
|
|
44
44
|
- **防循环**:同一调用连续重复超过阈值即拒绝,确定性本地逻辑,不依赖后端;不受各决策点开关影响,但会话总开关关闭时同样暂停。
|
|
45
45
|
- **会话面板与决策分析页**:面板展示自动裁决指标、会话总开关和最近日志;「决策分析」tab 按会话展示过程画像、趋势回测与判断准确度,并支持逐条标注和开关本会话的自动介入。
|
|
@@ -51,7 +51,7 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
|
|
|
51
51
|
配置 Key 后,插件默认把裁决请求发往远程端点 `https://api.typesafe.ai`(模型 `jev-latest`)。裁决会携带具体上下文:
|
|
52
52
|
|
|
53
53
|
- 门控:工具名与脱敏后的命令 / 路径摘要(各最多 2048 字符),不含对话内容。
|
|
54
|
-
- 产出自检:本轮请求、最近必要的用户要求与 AI
|
|
54
|
+
- 产出自检:本轮请求、最近必要的用户要求与 AI 回复(作背景,各有限额度,已遮蔽凭据),以及本轮最终回复(开头与结尾合计约 24K 字符;超长回复的中间部分会标注省略,避免只看开头漏掉结论)。
|
|
55
55
|
- 工具收窄:当前有效请求、必要前文与最近执行进展(工具名、成败、有限的命令 / 路径与 shell 结果摘录)与候选工具的名称 / 描述。
|
|
56
56
|
- 完成核对:从本回合最近一次用户要求中提取的交付条件(最多 20 条,不合并同一回合的多次要求)、本回合最近一次要求与必要前文(作背景,已遮蔽凭据)、本回合工具证据(工具名、成败、有限的命令 / 路径,shell 类工具另有有限结果摘录,并对无标签的长随机串再遮蔽一次)、回复前约 8K 字符。文件读取、搜索与网络工具的结果原文不外发。
|
|
57
57
|
- 手动裁决:`decision_evaluate` 与 `decision-layer` skill 会把 agent 提交的内容原样发送。
|
|
@@ -72,4 +72,4 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
|
|
|
72
72
|
|
|
73
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.
|
|
75
|
+
架构决策记录见 [docs/adr/](<docs/adr/README.md>):每条规则为什么存在、当时放弃了哪些备选。0.7.4 为体验版:介入能力已可用,但实际价值与阈值仍在探索,欢迎反馈决策日志观察到的误判与体感。
|
package/docs/en/DETAIL.md
CHANGED
|
@@ -7,7 +7,7 @@ A structured set of explicitly scoped decisions for the DeepSeek Harness agent l
|
|
|
7
7
|
- **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, while backend failures, low confidence, and invalid responses fall back to the host's original path.
|
|
8
8
|
- **Output self-check and quality trend** — scores the final reply at turn end and can append one re-prompt when the score is low and the model is confident; several consecutive confident low scores raise an alert on the trigger button.
|
|
9
9
|
- **Task-completion review** — when the user lists multiple delivery conditions, each is judged satisfied / unsatisfied / insufficient evidence. Conditions come only from the turn's latest request, with this turn's tool evidence primary (an earlier turn's success does not count) and self-reported completion not counting.
|
|
10
|
-
- **Tool narrowing** — at turn start, and again when a new user request arrives inside the same turn, judges each optional tool's relevance to the task and drops the ones not needed; each re-judgement releases the previous restriction first, and notices such as time or background-job updates trigger no re-judgement. It intersects the host-allowed set and never expands it.
|
|
10
|
+
- **Tool narrowing** — at turn start, and again when a new user request arrives inside the same turn, judges each optional tool's relevance to the task and drops the ones not needed; each re-judgement releases the previous restriction first, and notices such as time or background-job updates trigger no re-judgement. It intersects the host-allowed set and never expands it. The host resolves the tool set before the decision point for a step fires, so a drop reaches the model from the next step on; narrowing covers the tool channel only — UI fences in the reply body and skill instructions are unaffected.
|
|
11
11
|
- **Loop guard** — denies the same call repeated consecutively past a threshold; deterministic and backend-free.
|
|
12
12
|
- **Decision panel and analysis view** — shows automatic-decision metrics and the decision log, and lets you rate each decision correct / wrong / unsure; the analysis view's header can enable or disable automatic interventions for the current session.
|
|
13
13
|
|
package/docs/zh/DETAIL.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- **危险调用门控**:破坏性工具调用执行前,用脱敏摘要请后端裁决;只有高置信「拒绝」才拦截,后端失败、低置信或响应不合法时一律按宿主原路径执行。
|
|
8
8
|
- **产出自检与质量趋势**:回合结束给最终回复打分,低分且模型有把握时可自动补一轮;连续多次高置信低分会在触发按钮上给出告警。
|
|
9
9
|
- **任务完成核对**:用户明确列出多个交付条件时,逐项判定「已满足 / 未满足 / 证据不足」,条件只取本回合最近一次要求,以本回合工具证据为准(上一回合的成功不算数),回复自述不算证据。
|
|
10
|
-
-
|
|
10
|
+
- **工具收窄**:回合开始时、以及同一回合内出现新的用户要求时,判断各可选工具与任务的相关性并摘掉用不上的;每次重新判断前先释放旧限制,时间提示与后台通知不触发重判。只在宿主已允许的工具集内取交集,绝不扩权。宿主在本步决策点触发前已解析工具集,所以摘除从下一步起才进入模型请求;收窄只覆盖工具通道,回复正文里的 UI 围栏、技能说明等非工具能力不受影响。
|
|
11
11
|
- **防循环**:同一调用连续重复超过阈值即拒绝,确定性本地逻辑,不依赖后端。
|
|
12
12
|
- **决策面板与分析页**:展示自动裁决指标与决策日志,并可逐条标注「判对 / 判错 / 说不准」;分析页顶部可直接开关本会话的自动介入。
|
|
13
13
|
|
package/lib/client.js
CHANGED
|
@@ -62,7 +62,7 @@ var zh = {
|
|
|
62
62
|
narrowKeptCount: "\u4FDD\u7559",
|
|
63
63
|
narrowMore: " \u7B49 {count} \u4E2A",
|
|
64
64
|
narrowObserve: "\u4EC5\u89C2\u5BDF",
|
|
65
|
-
narrowEnforce: "\u5DF2\u751F\u6548",
|
|
65
|
+
narrowEnforce: "\u5DF2\u751F\u6548\uFF08\u4E0B\u4E00\u6B65\u8D77\uFF09",
|
|
66
66
|
narrowNotApplied: "\u672A\u751F\u6548",
|
|
67
67
|
checkPassRate: "\u901A\u8FC7\u7387",
|
|
68
68
|
narrowApplyRate: "\u6536\u7A84\u7387",
|
|
@@ -92,8 +92,8 @@ var zh = {
|
|
|
92
92
|
logPathLabel: "\u8DEF\u5F84\uFF1A",
|
|
93
93
|
logSubjectLabel: "\u8BF7\u6C42\uFF1A",
|
|
94
94
|
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",
|
|
95
|
-
logCheckHint: "\u6BCF\u56DE\u5408\u7ED3\u675F\u540E\uFF0C\u628A\u8FD9\u8F6E AI \u7684\u6700\u7EC8\u56DE\u590D\u8FDE\u540C\u672C\u8F6E\u8BF7\u6C42\u4E0E\u5FC5\u8981\u524D\u6587\uFF08\u6700\u8FD1\u4E00\u6761\u7528\u6237\u8981\u6C42\u4E0E AI \u56DE\u590D\uFF0C\u4F5C\u80CC\u666F\uFF0C\u5747\u5DF2\u906E\u853D\u51ED\u636E\uFF09\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",
|
|
96
|
-
logNarrowHint: "\u6BCF\u56DE\u5408\u5F00\u59CB\u65F6\uFF0C\u4EE5\u53CA\u540C\u4E00\u56DE\u5408\u5185\u4F60\u63D0\u51FA\u65B0\u8981\u6C42\u65F6\uFF0C\u628A\u5F53\u524D\u8BF7\u6C42\u3001\u5FC5\u8981\u524D\u6587\u4E0E\u6700\u8FD1\u6267\u884C\u8FDB\u5C55\u8FDE\u540C\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\
|
|
95
|
+
logCheckHint: "\u6BCF\u56DE\u5408\u7ED3\u675F\u540E\uFF0C\u628A\u8FD9\u8F6E AI \u7684\u6700\u7EC8\u56DE\u590D\u8FDE\u540C\u672C\u8F6E\u8BF7\u6C42\u4E0E\u5FC5\u8981\u524D\u6587\uFF08\u6700\u8FD1\u4E00\u6761\u7528\u6237\u8981\u6C42\u4E0E AI \u56DE\u590D\uFF0C\u4F5C\u80CC\u666F\uFF0C\u5747\u5DF2\u906E\u853D\u51ED\u636E\uFF09\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\u4F4E\u5206\u884C\u5E26\u300C\u4F4E\u8BC4\u5206\u300D\u6807\u8BB0\uFF0C\u53EF\u5728\u51B3\u7B56\u5206\u6790\u9875\u53EA\u770B\u4F4E\u8BC4\u5206\u3002",
|
|
96
|
+
logNarrowHint: "\u6BCF\u56DE\u5408\u5F00\u59CB\u65F6\uFF0C\u4EE5\u53CA\u540C\u4E00\u56DE\u5408\u5185\u4F60\u63D0\u51FA\u65B0\u8981\u6C42\u65F6\uFF0C\u628A\u5F53\u524D\u8BF7\u6C42\u3001\u5FC5\u8981\u524D\u6587\u4E0E\u6700\u8FD1\u6267\u884C\u8FDB\u5C55\u8FDE\u540C\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\u9650\u5236\u5DE5\u5177\uFF08\u5BBF\u4E3B\u5148\u89E3\u6790\u672C\u6B65\u5DE5\u5177\u96C6\uFF0C\u56E0\u6B64\u4ECE\u4E0B\u4E00\u6B65\u8D77\u751F\u6548\uFF09\u3002\u65F6\u95F4\u63D0\u793A\u3001\u540E\u53F0\u901A\u77E5\u7B49\u975E\u7528\u6237\u8981\u6C42\u4E0D\u4F1A\u89E6\u53D1\u91CD\u65B0\u5224\u65AD\u3002",
|
|
97
97
|
actionDeny: "\u5DF2\u62D2\u7EDD",
|
|
98
98
|
actionAsk: "\u8F6C\u4EA4\u5BA1\u6279",
|
|
99
99
|
actionPass: "\u672A\u5E72\u9884",
|
|
@@ -101,6 +101,10 @@ var zh = {
|
|
|
101
101
|
checkResultLow: "\u5F97\u5206\u504F\u4F4E",
|
|
102
102
|
checkResultOk: "\u901A\u8FC7",
|
|
103
103
|
checkResultLowNoisy: "\u5F97\u5206\u504F\u4F4E\uFF08\u4F4E\u7F6E\u4FE1\uFF09",
|
|
104
|
+
logLowBadge: "\u4F4E\u8BC4\u5206",
|
|
105
|
+
logLowOnly: "\u53EA\u770B\u4F4E\u8BC4\u5206",
|
|
106
|
+
logLowOnlyHint: "\u4EC5\u663E\u793A\u81EA\u68C0\u5F97\u5206\u504F\u4F4E\u7684\u8BB0\u5F55\uFF08\u542B\u4F4E\u7F6E\u4FE1\u566A\u58F0\uFF09\uFF0C\u7528\u4E8E\u5FEB\u901F\u5B9A\u4F4D\u662F\u54EA\u4E00\u56DE\u5408\u3002",
|
|
107
|
+
logLowEmpty: "\u672C\u4F1A\u8BDD\u6CA1\u6709\u4F4E\u8BC4\u5206\u8BB0\u5F55\u3002",
|
|
104
108
|
checkNoiseTag: "\u566A\u58F0\uFF0C\u53EF\u5FFD\u7565",
|
|
105
109
|
checkNoiseHint: "\u6A21\u578B\u5BF9\u8FD9\u6B21\u6253\u5206\u51E0\u4E4E\u6CA1\u6709\u628A\u63E1\uFF08\u7F6E\u4FE1\u5EA6\u6781\u4F4E\uFF09\uFF0C\u8FD9\u6761\u4F4E\u5206\u4E0D\u8BA1\u5165\u8D28\u91CF\u8D8B\u52BF\u3001\u4E5F\u4E0D\u89E6\u53D1\u4EFB\u4F55\u8865\u5B8C\uFF0C\u89C6\u4F5C\u566A\u58F0\u5FFD\u7565\u5373\u53EF\u3002\u771F\u6B63\u503C\u5F97\u5173\u6CE8\u7684\u662F\u8FDE\u7EED\u591A\u6B21\u9AD8\u7F6E\u4FE1\u7684\u4F4E\u5206\u3002",
|
|
106
110
|
reasonUnavailable: "\u540E\u7AEF\u4E0D\u53EF\u8FBE",
|
|
@@ -118,7 +122,7 @@ var zh = {
|
|
|
118
122
|
selfCheckHint: "\u6BCF\u56DE\u5408\u7ED3\u675F\u7ED9\u6700\u7EC8\u56DE\u590D\u6253\u5206\uFF080\u20132\uFF0C\u6309\u5404\u6863\u4F4D\u6982\u7387\u52A0\u6743\uFF0C\u53EF\u80FD\u662F\u5C0F\u6570\u5982 1.4\uFF09\u3002\u5173\u95ED\uFF1A\u53EA\u628A\u4F4E\u5206\u8BB0\u8FDB\u65E5\u5FD7\u548C\u7EDF\u8BA1\uFF0C\u4E0D\u6253\u65AD\u5BF9\u8BDD\u3002\u5F00\u542F\uFF1A\u5F97\u5206\u504F\u4F4E\uFF08\u22641\uFF09\u4E14\u6A21\u578B\u6709\u628A\u63E1\u65F6\uFF0C\u81EA\u52A8\u4EE5\u4F60\u7684\u540D\u4E49\u8FFD\u52A0\u4E00\u6761\u63D0\u793A\uFF0C\u8BA9 AI \u590D\u6838\u6709\u65E0\u9057\u6F0F\u6216\u77DB\u76FE\u5E76\u8865\u5B8C\uFF1B\u7F6E\u4FE1\u5EA6\u504F\u4F4E\u7684\u6253\u5206\u53EA\u8BB0\u5F55\u3001\u4E0D\u6253\u65AD\uFF0C\u540C\u4E00\u56DE\u5408\u6700\u591A\u8865\u4E00\u6B21\u3002",
|
|
119
123
|
narrow: "\u5DE5\u5177\u6536\u7A84",
|
|
120
124
|
narrowObserveLabel: "\u4EC5\u89C2\u5BDF\uFF08\u4E0D\u771F\u6B63\u9650\u5236\u5DE5\u5177\uFF09",
|
|
121
|
-
narrowHint: "\u6BCF\u56DE\u5408\u5F00\u59CB\u65F6\u3001\u4EE5\u53CA\u540C\u4E00\u56DE\u5408\u5185\u4F60\u63D0\u51FA\u65B0\u8981\u6C42\u65F6\uFF0C\u7528\u88C1\u51B3\u6A21\u578B\u5224\u65AD\u5F53\u524D\u53EF\u9009\u5DE5\u5177\u4E0E\u4EFB\u52A1\u7684\u76F8\u5173\u6027\uFF0C\u6458\u6389\u7528\u4E0D\u4E0A\u7684\uFF1B\u8BFB\u5199\u3001\u7F16\u8F91\u3001Shell\u3001\u68C0\u7D22\u3001\u5F85\u529E\u7B49\u6838\u5FC3\u5DE5\u5177\u59CB\u7EC8\u4FDD\u7559\u3001\u4E0D\u53C2\u4E0E\u5224\u5B9A\u3002\u9ED8\u8BA4\u771F\u6B63\u751F\u6548\uFF1A\
|
|
125
|
+
narrowHint: "\u6BCF\u56DE\u5408\u5F00\u59CB\u65F6\u3001\u4EE5\u53CA\u540C\u4E00\u56DE\u5408\u5185\u4F60\u63D0\u51FA\u65B0\u8981\u6C42\u65F6\uFF0C\u7528\u88C1\u51B3\u6A21\u578B\u5224\u65AD\u5F53\u524D\u53EF\u9009\u5DE5\u5177\u4E0E\u4EFB\u52A1\u7684\u76F8\u5173\u6027\uFF0C\u6458\u6389\u7528\u4E0D\u4E0A\u7684\uFF1B\u8BFB\u5199\u3001\u7F16\u8F91\u3001Shell\u3001\u68C0\u7D22\u3001\u5F85\u529E\u7B49\u6838\u5FC3\u5DE5\u5177\u59CB\u7EC8\u4FDD\u7559\u3001\u4E0D\u53C2\u4E0E\u5224\u5B9A\u3002\u9ED8\u8BA4\u771F\u6B63\u751F\u6548\uFF1A\u4ECE\u4E0B\u4E00\u6B65\u8D77\u53EA\u4FDD\u7559\u5224\u4E3A\u76F8\u5173\u7684\u5DE5\u5177\uFF0C\u4E14\u53EA\u5728\u5BBF\u4E3B\u5DF2\u5141\u8BB8\u7684\u8303\u56F4\u5185\u53D6\u4EA4\u96C6\u3001\u4E0D\u6269\u6743\uFF1B\u5BBF\u4E3B\u5728\u672C\u6B65\u51B3\u7B56\u70B9\u89E6\u53D1\u524D\u5DF2\u786E\u5B9A\u5DE5\u5177\u96C6\uFF0C\u56E0\u6B64\u672C\u6B65\u81EA\u8EAB\u7684\u8C03\u7528\u4ECD\u6309\u539F\u96C6\u5408\u653E\u884C\u3002\u6536\u7A84\u53EA\u4F5C\u7528\u4E8E\u5DE5\u5177\u901A\u9053\uFF1A\u56DE\u590D\u6B63\u6587\u91CC\u7684 UI \u56F4\u680F\u3001\u6280\u80FD\u8BF4\u660E\u7B49\u975E\u5DE5\u5177\u80FD\u529B\u4E0D\u53D7\u5F71\u54CD\u3002\u5F00\u542F\u300C\u4EC5\u89C2\u5BDF\u300D\u540E\u53EA\u628A\u53EF\u6458\u6389\u7684\u5DE5\u5177\u8BB0\u8FDB\u65E5\u5FD7\u548C\u7EDF\u8BA1\uFF0C\u4E0D\u6539\u52A8\u5DE5\u5177\u96C6\u3002\u4E09\u91CD\u4FDD\u62A4\uFF1A\u6838\u5FC3\u5DE5\u5177\u59CB\u7EC8\u4FDD\u7559\uFF1B\u53EF\u9009\u5DE5\u5177\u8D85\u8FC7 20 \u4E2A\u65F6\u81EA\u52A8\u53EA\u89C2\u5BDF\u4E0D\u9650\u5236\uFF1B\u4FDD\u7559\u4E0B\u6765\u4E0D\u8DB3 5 \u4E2A\u65F6\u89C6\u4E3A\u5224\u5B9A\u4E0D\u53EF\u4FE1\u3001\u8DF3\u8FC7\u3002",
|
|
122
126
|
narrowKeepLabel: "\u59CB\u7EC8\u4FDD\u7559\u7684\u5DE5\u5177\u524D\u7F00",
|
|
123
127
|
narrowKeepHint: "\u540D\u5B57\u4EE5\u8FD9\u4E9B\u524D\u7F00\u5F00\u5934\u7684\u5DE5\u5177\u6C38\u8FDC\u4FDD\u7559\u3001\u4E0D\u53C2\u4E0E\u76F8\u5173\u6027\u5224\u5B9A\uFF0C\u9002\u5408\u8BB0\u5FC6/\u68C0\u7D22\u8FD9\u7C7B\u300C\u5E38\u9A7B\u3001\u6309\u9700\u89E6\u53D1\u300D\u7684\u5DE5\u5177\uFF08\u9ED8\u8BA4\u53EA\u6709 mcp__openviking\uFF09\u3002\u7528\u82F1\u6587\u9017\u53F7\u6216\u6362\u884C\u5206\u9694\u591A\u4E2A\u524D\u7F00\uFF1B\u7559\u7A7A\u5219\u4E0D\u989D\u5916\u4FDD\u7559\u3002",
|
|
124
128
|
narrowMaxLabel: "\u5019\u9009\u5DE5\u5177\u4E0A\u9650",
|
|
@@ -260,7 +264,7 @@ var en = {
|
|
|
260
264
|
narrowKeptCount: "kept",
|
|
261
265
|
narrowMore: " and {count} in total",
|
|
262
266
|
narrowObserve: "observe",
|
|
263
|
-
narrowEnforce: "enforced",
|
|
267
|
+
narrowEnforce: "enforced (from next step)",
|
|
264
268
|
narrowNotApplied: "not applied",
|
|
265
269
|
checkPassRate: "Pass rate",
|
|
266
270
|
narrowApplyRate: "Narrow rate",
|
|
@@ -290,8 +294,8 @@ var en = {
|
|
|
290
294
|
logPathLabel: "path: ",
|
|
291
295
|
logSubjectLabel: "request: ",
|
|
292
296
|
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.",
|
|
293
|
-
logCheckHint: "After each turn, the AI\u2019s final reply for that turn, together with the request and the necessary recent background (the last user ask and AI reply, credentials masked), 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).",
|
|
294
|
-
logNarrowHint: "At the start of each turn, and again when a new user request arrives inside it, the current request, the necessary background and the latest execution progress are sent with each available tool 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
|
|
297
|
+
logCheckHint: "After each turn, the AI\u2019s final reply for that turn, together with the request and the necessary recent background (the last user ask and AI reply, credentials masked), 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). Low rows carry a \u201Clow score\u201D badge and can be filtered in the decision analysis tab.",
|
|
298
|
+
logNarrowHint: "At the start of each turn, and again when a new user request arrives inside it, the current request, the necessary background and the latest execution progress are sent with each available tool 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, starting from the next step because the host resolves the tool set for the current step before the hook runs.",
|
|
295
299
|
actionDeny: "denied",
|
|
296
300
|
actionAsk: "sent to approval",
|
|
297
301
|
actionPass: "no intervention",
|
|
@@ -299,6 +303,10 @@ var en = {
|
|
|
299
303
|
checkResultLow: "low score",
|
|
300
304
|
checkResultOk: "passed",
|
|
301
305
|
checkResultLowNoisy: "low score (low confidence)",
|
|
306
|
+
logLowBadge: "low score",
|
|
307
|
+
logLowOnly: "Low scores only",
|
|
308
|
+
logLowOnlyHint: "Show only low self-check scores (including low-confidence noise), to find which turn was scored low.",
|
|
309
|
+
logLowEmpty: "No low self-check scores in this session.",
|
|
302
310
|
checkNoiseTag: "noise, ignore",
|
|
303
311
|
checkNoiseHint: "The model had almost no confidence in this score (near-zero confidence). This low score does not count toward the quality trend and triggers no re-prompt \u2014 treat it as noise. What matters is several consecutive high-confidence low scores.",
|
|
304
312
|
reasonUnavailable: "backend unreachable",
|
|
@@ -316,7 +324,7 @@ var en = {
|
|
|
316
324
|
selfCheckHint: "Each turn ends by scoring the final reply (0\u20132, a probability-weighted mean across the levels, so it can be fractional like 1.4). Off: low scores are only logged, the conversation is not interrupted. On: when the score is low (\u22641) and the model is confident enough, one prompt is appended on your behalf asking the AI to review for gaps or contradictions and finish the work; low-confidence scores are only logged, never interrupt, and at most one prompt per turn.",
|
|
317
325
|
narrow: "Tool narrowing",
|
|
318
326
|
narrowObserveLabel: "Observe only (do not restrict tools)",
|
|
319
|
-
narrowHint: "At the start of each turn, and again when a new user request arrives inside it, the adjudication model judges how relevant each optional tool is to the task and drops the ones not needed; core tools (read, write, edit, shell, search, todos) are always kept and never judged. Enforced by default: only the relevant tools are kept
|
|
327
|
+
narrowHint: "At the start of each turn, and again when a new user request arrives inside it, the adjudication model judges how relevant each optional tool is to the task and drops the ones not needed; core tools (read, write, edit, shell, search, todos) are always kept and never judged. Enforced by default: from the next step on, only the relevant tools are kept, intersected with the host-allowed set and never expanded; the host fixes the tool set for the current step before the decision point fires, so that step still runs with the original set. Narrowing covers the tool channel only: UI fences in the reply body and skill instructions are unaffected. Turn on Observe only to just log the tools that could be dropped, without changing the tool set. Three safeguards: core tools always kept; more than 20 optional tools falls back to observe only; fewer than 5 tools remaining is treated as untrustworthy and skipped.",
|
|
320
328
|
narrowKeepLabel: "Always-kept tool prefixes",
|
|
321
329
|
narrowKeepHint: "Tools whose names start with any of these prefixes are always kept and never judged for relevance \u2014 good for resident, on-demand tools like memory/retrieval (defaults to mcp__openviking only). Separate prefixes with commas or newlines; leave empty to keep nothing extra.",
|
|
322
330
|
narrowMaxLabel: "Candidate tool cap",
|
|
@@ -556,9 +564,10 @@ function renderCheck({ t }, entry) {
|
|
|
556
564
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { title: t("logCheckHint"), children: t("logCheck") }),
|
|
557
565
|
turnTag(t, entry)
|
|
558
566
|
] });
|
|
559
|
-
const
|
|
567
|
+
const subjectText = entry.subject ?? entry.task;
|
|
568
|
+
const subject = subjectText ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-subject", title: `${t("logSubjectLabel")}${subjectText}`, children: [
|
|
560
569
|
"\u300C",
|
|
561
|
-
|
|
570
|
+
subjectText.length > 60 ? `${subjectText.slice(0, 60)}\u2026` : subjectText,
|
|
562
571
|
"\u300D"
|
|
563
572
|
] }) : null;
|
|
564
573
|
if (entry.outcome === "error") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
@@ -573,8 +582,10 @@ function renderCheck({ t }, entry) {
|
|
|
573
582
|
entry.reason ? ` \xB7 ${failDetail(t, entry)}` : ""
|
|
574
583
|
] })
|
|
575
584
|
] });
|
|
576
|
-
const
|
|
577
|
-
const
|
|
585
|
+
const low = entry.outcome === "low";
|
|
586
|
+
const noisy = low && entry.lowConfidence === true;
|
|
587
|
+
const badge = low ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `decision-log-badge ${noisy ? "decision-log-badge-noise" : "decision-log-badge-low"}`, children: t("logLowBadge") }) : null;
|
|
588
|
+
const result = low ? noisy ? t("checkResultLowNoisy") : t("checkResultLow") : t("checkResultOk");
|
|
578
589
|
const verdict = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: noisy ? "decision-log-muted-verdict" : "decision-log-verdict", children: result });
|
|
579
590
|
const score = entry.score !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", title: t("logScoreHint"), children: [
|
|
580
591
|
" \xB7 ",
|
|
@@ -600,6 +611,7 @@ function renderCheck({ t }, entry) {
|
|
|
600
611
|
subject
|
|
601
612
|
] }) : null,
|
|
602
613
|
" \xB7 ",
|
|
614
|
+
badge,
|
|
603
615
|
verdict,
|
|
604
616
|
score,
|
|
605
617
|
conf,
|
|
@@ -739,7 +751,7 @@ function RatingButtons({ id, t, ratings, onRate }) {
|
|
|
739
751
|
}
|
|
740
752
|
function DecisionLogList({ entries, ratings, onRate, t, newestFirst = true }) {
|
|
741
753
|
const rows = newestFirst ? entries.slice().reverse() : entries.slice();
|
|
742
|
-
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: [
|
|
754
|
+
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)}${entry.kind === "check" && entry.outcome === "low" ? " decision-log-low" : ""}`, children: [
|
|
743
755
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-log-time", children: time(entry.at) }),
|
|
744
756
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "decision-log-body", children: [
|
|
745
757
|
renderBody({ t }, entry),
|
|
@@ -1114,6 +1126,7 @@ function AnalysisView({ sessionId, t }) {
|
|
|
1114
1126
|
const [analysis, setAnalysis] = (0, import_react3.useState)(null);
|
|
1115
1127
|
const [rows, setRows] = (0, import_react3.useState)(null);
|
|
1116
1128
|
const [logPage, setLogPage] = (0, import_react3.useState)(1);
|
|
1129
|
+
const [lowOnly, setLowOnly] = (0, import_react3.useState)(false);
|
|
1117
1130
|
const [loaded, setLoaded] = (0, import_react3.useState)(false);
|
|
1118
1131
|
const [message, setMessage] = (0, import_react3.useState)("");
|
|
1119
1132
|
const [enabled, setEnabled] = (0, import_react3.useState)(null);
|
|
@@ -1135,6 +1148,7 @@ function AnalysisView({ sessionId, t }) {
|
|
|
1135
1148
|
active.current = sessionId;
|
|
1136
1149
|
setBusy(false);
|
|
1137
1150
|
setMessage("");
|
|
1151
|
+
setLowOnly(false);
|
|
1138
1152
|
loadSession();
|
|
1139
1153
|
}, [sessionId]);
|
|
1140
1154
|
(0, import_react3.useEffect)(() => onSessionEnabled((change) => {
|
|
@@ -1187,17 +1201,18 @@ function AnalysisView({ sessionId, t }) {
|
|
|
1187
1201
|
};
|
|
1188
1202
|
(0, import_react3.useEffect)(() => {
|
|
1189
1203
|
let alive = true;
|
|
1190
|
-
void request(`logrows?sessionId=${encodeURIComponent(sessionId)}&page=${logPage}&pageSize=${LOG_PAGE_SIZE}`).then((value) => {
|
|
1204
|
+
void request(`logrows?sessionId=${encodeURIComponent(sessionId)}&page=${logPage}&pageSize=${LOG_PAGE_SIZE}${lowOnly ? "&lowOnly=1" : ""}`).then((value) => {
|
|
1191
1205
|
if (alive) setRows(value);
|
|
1192
1206
|
}).catch(() => {
|
|
1193
1207
|
});
|
|
1194
1208
|
return () => {
|
|
1195
1209
|
alive = false;
|
|
1196
1210
|
};
|
|
1197
|
-
}, [sessionId, logPage]);
|
|
1211
|
+
}, [sessionId, logPage, lowOnly]);
|
|
1198
1212
|
const profile = analysis?.profile;
|
|
1199
1213
|
const backtest = analysis?.trendBacktest;
|
|
1200
1214
|
const annotations = analysis?.annotations;
|
|
1215
|
+
const lowCount = profile?.check.low ?? 0;
|
|
1201
1216
|
const meanScore = profile && profile.check.scoreCount > 0 ? Math.round(profile.check.scoreSum / profile.check.scoreCount * 10) / 10 : null;
|
|
1202
1217
|
const completeTotal = profile ? profile.complete.satisfied + profile.complete.unsatisfied + profile.complete.insufficient : 0;
|
|
1203
1218
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "decision-analysis", role: "region", "aria-label": t("analysisTitle"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "decision-analysis-inner", children: [
|
|
@@ -1278,8 +1293,28 @@ function AnalysisView({ sessionId, t }) {
|
|
|
1278
1293
|
annotations.rated === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "decision-muted", children: t("analysisAccuracyEmpty") }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "decision-muted decision-rate-summary", children: t("rateSummary", { rated: annotations.rated, good: annotations.good, bad: annotations.bad, unsure: annotations.unsure }) })
|
|
1279
1294
|
] }),
|
|
1280
1295
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("section", { className: "decision-analysis-section", children: [
|
|
1281
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.
|
|
1282
|
-
|
|
1296
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "decision-analysis-log-head", children: [
|
|
1297
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { children: t("log") }),
|
|
1298
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1299
|
+
"button",
|
|
1300
|
+
{
|
|
1301
|
+
type: "button",
|
|
1302
|
+
className: "decision-log-filter",
|
|
1303
|
+
"aria-pressed": lowOnly,
|
|
1304
|
+
disabled: !analysis || lowCount === 0,
|
|
1305
|
+
title: t("logLowOnlyHint"),
|
|
1306
|
+
onClick: () => {
|
|
1307
|
+
setLowOnly((value) => !value);
|
|
1308
|
+
setLogPage(1);
|
|
1309
|
+
},
|
|
1310
|
+
children: [
|
|
1311
|
+
t("logLowOnly"),
|
|
1312
|
+
lowCount > 0 ? ` \xB7 ${lowCount}` : ""
|
|
1313
|
+
]
|
|
1314
|
+
}
|
|
1315
|
+
)
|
|
1316
|
+
] }),
|
|
1317
|
+
!rows || rows.total === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "decision-empty", role: "status", children: lowOnly ? t("logLowEmpty") : t("analysisLogTruncated") }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1283
1318
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DecisionLogList, { entries: rows.entries, ratings, onRate: (id, rating) => void annotate(id, rating), t, newestFirst: false }),
|
|
1284
1319
|
rows.pages > 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("nav", { className: "decision-pager", "aria-label": t("log"), children: [
|
|
1285
1320
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", disabled: rows.page <= 1, onClick: () => setLogPage((page) => Math.max(1, page - 1)), children: t("pagerPrev") }),
|
|
@@ -2155,6 +2190,14 @@ var css = `
|
|
|
2155
2190
|
real quality dip. */
|
|
2156
2191
|
.decision-log-muted-verdict{font-weight:600;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
2157
2192
|
.decision-log-muted .decision-log-time{opacity:.75}
|
|
2193
|
+
/* Every low self-check row gets an amber accent and a badge, even when the score
|
|
2194
|
+
is low-confidence noise. The noise row keeps its muted verdict and dashed
|
|
2195
|
+
accent, so it is findable without reading as a real quality dip. */
|
|
2196
|
+
.decision-log-item.decision-log-low{border-left-color:#c98a1e}
|
|
2197
|
+
.decision-log-item.decision-log-low.decision-log-muted{border-left-style:dashed}
|
|
2198
|
+
.decision-log-badge{display:inline-block;margin-right:.35rem;padding:0 .38rem;border:1px solid transparent;border-radius:999px;font-size:.68rem;font-weight:600;line-height:1.6;vertical-align:.05em}
|
|
2199
|
+
.decision-log-badge-low{color:#8a5a12;background:rgba(201,138,30,.16);border-color:rgba(201,138,30,.4)}
|
|
2200
|
+
.decision-log-badge-noise{color:var(--dsw-alias-label-secondary,#59636e);background:var(--dsw-alias-bg-layer-1,#f2f4f6);border-color:var(--dsw-alias-border-l2,#dce1e6)}
|
|
2158
2201
|
|
|
2159
2202
|
.decision-status{display:flex;align-items:center;gap:.5rem;margin-top:.7rem;font-size:.82rem}
|
|
2160
2203
|
.decision-status-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto;background:#9aa5b1}
|
|
@@ -2265,6 +2308,20 @@ var css = `
|
|
|
2265
2308
|
.decision-analysis-switch:focus-visible{outline:2px solid var(--decision-analysis-accent);outline-offset:2px}
|
|
2266
2309
|
.decision-analysis-section{border-top:1px solid var(--dsw-alias-border-l2,#eceff1);margin-top:1.1rem;padding-top:1rem}
|
|
2267
2310
|
.decision-analysis-section>h3{font-size:.92rem;margin:0 0 .6rem}
|
|
2311
|
+
/* Section header with the low-score-only filter on the right. Keeping the
|
|
2312
|
+
filter next to the log heading makes the affordance discoverable exactly
|
|
2313
|
+
where a long list is being scanned. */
|
|
2314
|
+
.decision-analysis-log-head{display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin-bottom:.6rem}
|
|
2315
|
+
.decision-analysis-log-head>h3{font-size:.92rem;margin:0}
|
|
2316
|
+
.decision-log-filter{flex:0 0 auto;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#ccd3da);border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary,#59636e);padding:.24rem .7rem;font:inherit;font-size:.76rem;line-height:1.3;white-space:nowrap;cursor:pointer;transition:color .15s,border-color .15s,background .15s}
|
|
2317
|
+
.decision-log-filter:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#eef1f3);color:var(--dsw-alias-label-primary,#202124)}
|
|
2318
|
+
.decision-log-filter[aria-pressed="true"]{border-color:#c98a1e;color:#8a5a12;background:rgba(201,138,30,.12)}
|
|
2319
|
+
.decision-log-filter:disabled{opacity:.5;cursor:default}
|
|
2320
|
+
.decision-log-filter:focus-visible{outline:2px solid var(--decision-analysis-accent);outline-offset:2px}
|
|
2321
|
+
@media (prefers-color-scheme:dark){
|
|
2322
|
+
.decision-log-badge-low{color:#e0a83e;background:rgba(224,168,62,.14);border-color:rgba(224,168,62,.4)}
|
|
2323
|
+
.decision-log-filter[aria-pressed="true"]{border-color:#e0a83e;color:#e0a83e;background:rgba(224,168,62,.14)}
|
|
2324
|
+
}
|
|
2268
2325
|
.decision-analysis-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:.7rem}
|
|
2269
2326
|
.decision-analysis-trend{max-width:26rem}
|
|
2270
2327
|
.decision-analysis-note{margin:.5rem 0 0}
|
|
@@ -16,6 +16,23 @@ function decisionText(value, maxBytes = 1536) {
|
|
|
16
16
|
const clean = redactCredentials(stripInjected(value.slice(0, 32 * 1024))).trim();
|
|
17
17
|
return clip(clean, maxBytes);
|
|
18
18
|
}
|
|
19
|
+
function clipTail(text, maxBytes) {
|
|
20
|
+
let size = 0;
|
|
21
|
+
let result = "";
|
|
22
|
+
const chars = Array.from(text);
|
|
23
|
+
for (let index = chars.length - 1; index >= 0; index--) {
|
|
24
|
+
const char = chars[index];
|
|
25
|
+
size += Buffer.byteLength(char);
|
|
26
|
+
if (size > maxBytes) break;
|
|
27
|
+
result = char + result;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function decisionTail(value, maxBytes = 512) {
|
|
32
|
+
if (typeof value !== "string" || maxBytes <= 0) return "";
|
|
33
|
+
const clean = redactCredentials(stripInjected(value.slice(0, 32 * 1024))).trim();
|
|
34
|
+
return clipTail(clean, maxBytes);
|
|
35
|
+
}
|
|
19
36
|
const OPAQUE = /[A-Za-z0-9_+-]{24,}/g;
|
|
20
37
|
const resultExcerpt = (value, maxBytes) => clip(decisionText(value, 32 * 1024).replace(OPAQUE, "[redacted]"), maxBytes);
|
|
21
38
|
const messagesOf = (value) => Array.isArray(value) ? value.filter(isObject) : [];
|
|
@@ -36,7 +53,9 @@ function backgroundOf(messages, isHuman) {
|
|
|
36
53
|
const message = messages[index];
|
|
37
54
|
if (!message) return void 0;
|
|
38
55
|
const role = isHuman(message) ? "user" : "assistant";
|
|
39
|
-
|
|
56
|
+
const raw = textOf(message);
|
|
57
|
+
const excerpt = role === "user" ? decisionText(raw, 512) : decisionTail(raw, 512);
|
|
58
|
+
return { role, text: excerpt };
|
|
40
59
|
}).filter((item) => Boolean(item?.text));
|
|
41
60
|
}
|
|
42
61
|
function evidenceOf(messages) {
|
package/lib/host/index.js
CHANGED
|
@@ -270,7 +270,7 @@ async function apply(ctx, options = {}) {
|
|
|
270
270
|
port,
|
|
271
271
|
trendConfig: (await resolveConfig({ path })).checkSettings
|
|
272
272
|
});
|
|
273
|
-
const listRows = (sessionId, { page, pageSize } = {}) => listSessionDecisions(sessionId, { port, page, pageSize });
|
|
273
|
+
const listRows = (sessionId, { page, pageSize, lowOnly } = {}) => listSessionDecisions(sessionId, { port, page, pageSize, lowOnly });
|
|
274
274
|
const unregister = createManagerRoutes({ path, sessions, backend, logStore, analyze, listRows, bindHost: web.webServer.host }).map((route) => web.webServer.register(route));
|
|
275
275
|
return () => {
|
|
276
276
|
unregister.forEach((dispose) => dispose());
|
|
@@ -106,11 +106,12 @@ export declare function reconstructSessionSeed(sessionId: unknown, { dir, port,
|
|
|
106
106
|
maxLog?: number;
|
|
107
107
|
readStream?: ReadStream;
|
|
108
108
|
}): Promise<SessionSeed | undefined>;
|
|
109
|
-
export declare function listSessionDecisions(sessionId: unknown, { dir, port, page, pageSize, readStream }?: {
|
|
109
|
+
export declare function listSessionDecisions(sessionId: unknown, { dir, port, page, pageSize, lowOnly, readStream }?: {
|
|
110
110
|
dir?: string;
|
|
111
111
|
port?: number;
|
|
112
112
|
page?: number;
|
|
113
113
|
pageSize?: number;
|
|
114
|
+
lowOnly?: boolean;
|
|
114
115
|
readStream?: ReadStream;
|
|
115
116
|
}): Promise<{
|
|
116
117
|
entries: LogRow[];
|
package/lib/host/log-analyze.js
CHANGED
|
@@ -220,7 +220,7 @@ async function reconstructSessionSeed(sessionId, { dir, port, trendConfig, maxLo
|
|
|
220
220
|
if (decisions.size === 0) return void 0;
|
|
221
221
|
return buildSeed(decisions, { maxLog, trendConfig });
|
|
222
222
|
}
|
|
223
|
-
async function listSessionDecisions(sessionId, { dir, port, page = 1, pageSize = 100, readStream = createReadableStream } = {}) {
|
|
223
|
+
async function listSessionDecisions(sessionId, { dir, port, page = 1, pageSize = 100, lowOnly = false, readStream = createReadableStream } = {}) {
|
|
224
224
|
if (typeof sessionId !== "string" || !sessionId.trim()) throw new Error("Invalid session id");
|
|
225
225
|
const size = Number.isSafeInteger(pageSize) && pageSize > 0 && pageSize <= 500 ? pageSize : 100;
|
|
226
226
|
const records = [];
|
|
@@ -229,7 +229,8 @@ async function listSessionDecisions(sessionId, { dir, port, page = 1, pageSize =
|
|
|
229
229
|
if (record.sessionId === sessionId) records.push(record);
|
|
230
230
|
}
|
|
231
231
|
const { decisions } = foldSession(records);
|
|
232
|
-
const
|
|
232
|
+
const all = [...decisions.values()].sort((a, b) => atOf(b) - atOf(a));
|
|
233
|
+
const rows = lowOnly ? all.filter((record) => record.kind === "check" && record.outcome === "low") : all;
|
|
233
234
|
const total = rows.length;
|
|
234
235
|
const pages = Math.max(1, Math.ceil(total / size));
|
|
235
236
|
const current = Math.min(Math.max(1, Number.isSafeInteger(page) ? page : 1), pages);
|
package/lib/host/manager-api.js
CHANGED
|
@@ -111,7 +111,9 @@ function createManagerRoutes({ path, sessions, backend, logStore, analyze, listR
|
|
|
111
111
|
}
|
|
112
112
|
}),
|
|
113
113
|
// One newest-first page of this session's full persisted decision rows, for
|
|
114
|
-
// the analysis tab's paginated log.
|
|
114
|
+
// the analysis tab's paginated log. `lowOnly=1` filters to low self-check
|
|
115
|
+
// scores before paging, so a single low score in a long session is one click
|
|
116
|
+
// away instead of a manual scan. Rows carry the same panel fields as /log,
|
|
115
117
|
// including the gate's already-redacted and length-bounded command/path, so
|
|
116
118
|
// this exposes nothing the recent-rows route does not already.
|
|
117
119
|
route("logrows", {
|
|
@@ -121,8 +123,9 @@ function createManagerRoutes({ path, sessions, backend, logStore, analyze, listR
|
|
|
121
123
|
const params = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
122
124
|
const page = Number.parseInt(params.get("page") ?? "1", 10);
|
|
123
125
|
const pageSize = Number.parseInt(params.get("pageSize") ?? "100", 10);
|
|
126
|
+
const lowOnly = params.get("lowOnly") === "1";
|
|
124
127
|
try {
|
|
125
|
-
return await listRows(id, { page: Number.isNaN(page) ? 1 : page, pageSize: Number.isNaN(pageSize) ? 100 : pageSize });
|
|
128
|
+
return await listRows(id, { page: Number.isNaN(page) ? 1 : page, pageSize: Number.isNaN(pageSize) ? 100 : pageSize, ...lowOnly ? { lowOnly } : {} });
|
|
126
129
|
} catch (error) {
|
|
127
130
|
if (error?.code === "BAD_REQUEST") throw error;
|
|
128
131
|
throw codedError("log rows failed", "INTERNAL");
|
package/lib/host/self-check.js
CHANGED
|
@@ -2,12 +2,19 @@ import { taskExcerpt } from "./excerpt.js";
|
|
|
2
2
|
import { createSteerOnce } from "./steer-once.js";
|
|
3
3
|
import { errorReason, isProbability } from "./types.js";
|
|
4
4
|
const MAX_OUTPUT = 24 * 1024;
|
|
5
|
+
const ANSWER_TAIL = 8 * 1024;
|
|
6
|
+
const OMITTED = "\n\n\u2026\uFF08\u4E2D\u95F4\u5185\u5BB9\u5DF2\u7701\u7565\uFF09\u2026\n\n";
|
|
5
7
|
const DEFAULT_RUBRIC = Object.freeze([
|
|
6
8
|
"\u660E\u663E\u7B54\u975E\u6240\u95EE\uFF0C\u6216\u5B58\u5728\u4E25\u91CD\u9057\u6F0F / \u81EA\u76F8\u77DB\u76FE",
|
|
7
9
|
"\u57FA\u672C\u7B54\u5230\u95EE\u9898\uFF0C\u4F46\u6709\u53EF\u5BDF\u89C9\u7684\u9057\u6F0F\u6216\u4E0D\u4E25\u8C28",
|
|
8
10
|
"\u5B8C\u6574\u3001\u51C6\u786E\u3001\u65E0\u81EA\u76F8\u77DB\u76FE\u5730\u56DE\u5E94\u4E86\u8BF7\u6C42"
|
|
9
11
|
]);
|
|
10
12
|
const STEER_TEXT = "\u672C\u56DE\u5408\u81EA\u68C0\u5F97\u5206\u504F\u4F4E\uFF1A\u8BF7\u590D\u6838\u662F\u5426\u7B54\u5230\u4E86\u7528\u6237\u7684\u95EE\u9898\u3001\u6709\u65E0\u9057\u6F0F\u6216\u81EA\u76F8\u77DB\u76FE\uFF0C\u5E76\u5728\u9700\u8981\u65F6\u8865\u5B8C\u3002";
|
|
13
|
+
function answerExcerpt(output) {
|
|
14
|
+
const text = String(output);
|
|
15
|
+
if (text.length <= MAX_OUTPUT) return text;
|
|
16
|
+
return `${text.slice(0, MAX_OUTPUT - ANSWER_TAIL)}${OMITTED}${text.slice(-ANSWER_TAIL)}`;
|
|
17
|
+
}
|
|
11
18
|
function boundedRubric(rubric) {
|
|
12
19
|
if (!Array.isArray(rubric)) return DEFAULT_RUBRIC;
|
|
13
20
|
const levels = rubric.filter((item) => typeof item === "string" && Boolean(item.trim()) && item.length <= 256).slice(0, 10);
|
|
@@ -20,7 +27,12 @@ function scoreQuestion(output, rubric = DEFAULT_RUBRIC, context) {
|
|
|
20
27
|
// against nothing and cannot detect an off-topic reply.
|
|
21
28
|
...context?.request ? { request: context.request } : {},
|
|
22
29
|
...context?.background?.length ? { background: context.background } : {},
|
|
23
|
-
answer:
|
|
30
|
+
answer: answerExcerpt(output),
|
|
31
|
+
// Short follow-ups ("继续", "补上", "按这个标准重新打分") have no standalone
|
|
32
|
+
// meaning; the task they continue is in background. Saying so explicitly
|
|
33
|
+
// stops the model from treating the follow-up as a new, empty request and
|
|
34
|
+
// from borrowing an unrelated earlier task as this turn's goal.
|
|
35
|
+
...context ? { note: "request \u662F\u672C\u56DE\u5408\u7528\u6237\u8BF7\u6C42\uFF1B\u82E5\u5B83\u53EA\u662F\u300C\u7EE7\u7EED\u300D\u300C\u8865\u4E0A\u300D\u8FD9\u7C7B\u77ED\u8FFD\u95EE\uFF0C\u8BF7\u7ED3\u5408 background \u4E2D\u627F\u63A5\u7684\u4EFB\u52A1\u5224\u65AD\uFF0C\u4E0D\u8981\u628A background \u91CC\u7684\u65E7\u4EFB\u52A1\u5F53\u6210\u65B0\u7684\u8981\u6C42\u3002background \u53EA\u662F\u80CC\u666F\uFF0C\u4E0D\u662F\u672C\u8F6E\u7684\u65B0\u8981\u6C42\u3002answer \u662F\u672C\u56DE\u5408\u6700\u7EC8\u56DE\u590D\u3002" } : {}
|
|
24
36
|
},
|
|
25
37
|
questions: { quality: {
|
|
26
38
|
type: "score",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-decision-layer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "A structured decision layer for the DeepSeek Harness agent loop: a danger-call gate, output self-check, tool narrowing, and loop guard backed by a pluggable adjudication model.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|