dsh-decision-layer 0.4.0 → 0.5.0
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 +6 -4
- package/README.md +7 -5
- package/lib/client.js +248 -33
- package/package.json +1 -1
- package/service/config-store.mjs +47 -4
- package/service/danger-gate.mjs +33 -16
- package/service/dsh.mjs +72 -1
- package/service/log-analyze.mjs +155 -0
- package/service/log-read.mjs +27 -0
- package/service/log-store.mjs +12 -0
- package/service/loop-guard.mjs +3 -4
- package/service/manager-api.mjs +26 -1
- package/service/quality-trend.mjs +29 -0
- package/service/self-check.mjs +12 -5
- package/service/session-state.mjs +52 -15
- package/service/task-completion.mjs +155 -0
package/README.en.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# dsh-decision-layer
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> English | [简体中文](README.md)
|
|
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
|
+
> **0.5.0 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
|
|
|
@@ -36,8 +36,10 @@ Restart `dsh web` and refresh after installing or updating: the host and client
|
|
|
36
36
|
|
|
37
37
|
- **Manual verdicts** — `decision_evaluate` and the `decision-layer` skill submit `noul`, `score`, or `choice` questions; `decision_check_connection` tests connectivity. Neither counts as an automatic decision or changes agent behavior.
|
|
38
38
|
- **Configurable backend** — defaults to `https://api.typesafe.ai` and `jev-latest`, with per-field URL/key/model overrides under the same protocol. No request without a key. Custom HTTP endpoints require address-bound plaintext-risk confirmation.
|
|
39
|
-
- **Dangerous-call gate** — before a destructive tool call, a redacted summary asks the backend for
|
|
39
|
+
- **Dangerous-call gate** — before a destructive tool call, a redacted summary asks the backend for a verdict, using a **two-tier model**: only a high-confidence deny is enforced monotonically through the guard to block that call; every other case (a high-confidence allow/ask, low confidence, backend failure, or an invalid result) does not intervene and runs the host's original path. The gate is a safety net that only blocks when the model is confident of danger — it never authorizes on the host's behalf or downgrades approval. Rules are configurable.
|
|
40
40
|
- **Output self-check** — scores the final output at turn end against a rubric (0–2, a probability-weighted mean across levels, so it can be fractional). Observe-only by default; an opt-in low-score re-prompt appends one prompt on your behalf when the score is low and the model is confident, at most once per turn.
|
|
41
|
+
- **Task-completion review** — when the user spells out multiple delivery conditions (numbered / bulleted / multi-command), each is judged satisfied / unsatisfied / insufficient-evidence via a choice before turn end, with tool evidence primary and self-reported replies not counting as evidence; extraction is deterministic and never infers implicit expectations. Observe-only by default, steer configurable, coexisting with self-check and tracked as separate metrics.
|
|
42
|
+
- **Output-quality trend alert** — a read-only reminder built on self-check data. After several consecutive high-confidence low scores, the trigger button's icon shifts from grey to amber to red, with a hover hint that the task may have grown harder or the model may be fluctuating and suggesting a prompt tweak or model swap. Purely a hint, never driving automatic intervention; the three thresholds (high-confidence bar / consecutive rounds / severe tier) are configurable (defaults 0.6 / 3 / 5), conservative, and pending real-data calibration.
|
|
41
43
|
- **Tool narrowing** — at turn start, a noul judges each optional tool's relevance to the task and drops the ones not needed (relevance probability below the drop threshold, default 0.3, configurable), intersecting the host-allowed set and never expanding it. Enforced by default; switch to observe-only to log without changing the tool set. Three safeguards: core tools (read/write/edit/shell/search/todos, plus agent meta-abilities such as asking the user, goals, skills, delivery, background jobs, and teamwork) are always kept and never judged; more optional tools than the candidate cap (default 20, configurable) falls back to observe only; fewer than 5 tools remaining is treated as untrustworthy and skipped. Tools matching a **keep-prefix** allowlist (defaults to `mcp__openviking` and `team_task_`) are also always kept and never judged — good for resident, on-demand tools like memory, retrieval, and teamwork.
|
|
42
44
|
- **Loop guard** — deterministic and backend-free; denies the same call repeated consecutively past a threshold. Always on and outside the per-feature switches.
|
|
43
45
|
- **Session panel** — a dock trigger below the composer opens the panel with automatic-decision metrics (attempts, fallbacks, model suggestions vs actual outcomes), the master switch, and the decision log, with an empty state before any automatic decision. The master switch pauses only automatic intervention.
|
|
@@ -46,4 +48,4 @@ Restart `dsh web` and refresh after installing or updating: the host and client
|
|
|
46
48
|
|
|
47
49
|
## Safety boundary and development
|
|
48
50
|
|
|
49
|
-
Automatic intervention only subtracts: the gate only denies, narrowing only removes tools, and neither grants new permissions. The gate defers to native DSH approval on backend failure rather than allowing; self-check, narrowing, and loop-guard errors skip intervention. The redacted summary sent to the backend carries only tool name and command/path with secret redaction — never file content. Node.js `>=20.11`; `npm test` and `npm run build:client` run local checks. Rebuild and commit `lib/client.js` with any client change. 0.
|
|
51
|
+
Automatic intervention only subtracts: the gate only denies, narrowing only removes tools, and neither grants new permissions. The gate defers to native DSH approval on backend failure rather than allowing; self-check, narrowing, and loop-guard errors skip intervention. The redacted summary sent to the backend carries only tool name and command/path with secret redaction — never file content. Node.js `>=20.11`; `npm test` and `npm run build:client` run local checks. Rebuild and commit `lib/client.js` with any client change. 0.5.0 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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# dsh-decision-layer
|
|
2
2
|
|
|
3
|
-
> English
|
|
3
|
+
> [English](README.en.md) | 简体中文
|
|
4
4
|
|
|
5
5
|
DeepSeek Harness(DSH)的结构化裁决插件。它把有明确判断标准的问题交给同协议的裁决后端,供 agent 在需要时使用,并在 agent loop 上接入危险门控、产出自检、工具收窄和防循环等自动介入。
|
|
6
6
|
|
|
7
|
-
> **0.
|
|
7
|
+
> **0.5.0 是体验版。** 插件功能已可用,但裁决介入能带来多少实际价值仍在探索中——各决策点的阈值都是未经验证的启发式默认值,建议先以观察心态体验,别当成经过验证的效果保证。功能与后续进度以 [ROADMAP.md](<ROADMAP.md>) 为准。
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
@@ -36,8 +36,10 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
|
|
|
36
36
|
|
|
37
37
|
- **手动裁决**:`decision_evaluate` 工具与 `decision-layer` skill 支持显式提交 `noul`、`score`、`choice` 问题,`decision_check_connection` 做连通测试。手动调用和连通测试不计入自动裁决指标,也不改变 agent 行为。
|
|
38
38
|
- **可配置后端**:默认 `https://api.typesafe.ai`、`jev-latest`,URL、Key、model 可逐字段覆盖;仅支持相同的结构化裁决协议。未配置 Key 时不发送请求。自定义 HTTP 地址需针对实际目标确认明文传输风险,地址变更须重新确认。
|
|
39
|
-
-
|
|
39
|
+
- **危险门控**:破坏性工具调用执行前用脱敏摘要请后端裁决,采用**两档模型**——只有模型高置信判定「拒绝」时才经监控守卫单调拦截该调用;其余情况(高置信放行 / 询问、低置信、后端失败或响应不合法)一律不介入,按宿主原路径执行。门控只做「确信危险才拦」的安全网,不替宿主授权、不降级审批。危险规则可增删配置。
|
|
40
40
|
- **产出自检**:回合结束用评分量表给最终产出打分(0–2,按各档位概率加权,可能是小数)。默认只提示、不打断;可开启「低分补完」,得分偏低且模型有把握时以你的名义追加一次补完提示,同一回合最多补一次。
|
|
41
|
+
- **任务完成核对**:当用户明确列出多个交付条件(编号 / 项目符号 / 多命令)时,回合结束前用单选逐项判定「已满足 / 未满足 / 证据不足」,工具证据优先、回复自述不算证据;确定性提取、不推断隐含期望。默认只观察、可配 steer,与产出自检并存、指标分列。
|
|
42
|
+
- **产出质量趋势告警**:建在自检数据上的只读提醒。连续多次高置信低分时,把输入框旁触发按钮的图标从灰变橙、变红,hover 提示可能是任务变难或模型波动、建议调整 prompt 或更换模型。纯提示、不驱动自动干预;高置信门槛 / 连续轮数 / 严重档三个阈值可配(默认 0.6 / 3 / 5),偏保守、待实测校准。
|
|
41
43
|
- **工具收窄**:回合开始时用 noul 逐个判断可选工具与本轮任务的相关性,摘掉用不上的(相关性概率低于摘除阈值,默认 0.3、可配),只在宿主已允许的集合内取交集、不扩权。默认真正生效,可切到「仅观察」只记录不改工具集。三重保护:核心工具(读写、编辑、Shell、检索、待办,以及问用户、目标、技能、交付、后台作业、协作等 agent 元能力)始终保留、不参与判定;可选工具超过候选上限(默认 20,可配)时自动降为仅观察;保留下来不足 5 个时视为判定不可信、跳过。**保留工具前缀**白名单里的工具(默认含 `mcp__openviking`、`team_task_`)也始终保留、不参与判定,适合记忆 / 检索、协作这类常驻、按需触发的工具。
|
|
42
44
|
- **防循环**:确定性判断,不走后端;同一调用连续重复超过阈值即拒绝,打断死循环。始终生效,不在决策点开关范围内。
|
|
43
45
|
- **会话面板**:输入框下方的 dock 触发器弹出面板,展示自动裁决指标(评估数、失败回退、模型建议与实际结果分列)、会话总开关和决策日志;无自动裁决时显示空态。总开关关闭只暂停自动介入,不影响手动裁决。
|
|
@@ -46,8 +48,8 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
|
|
|
46
48
|
|
|
47
49
|
## 安全边界
|
|
48
50
|
|
|
49
|
-
自动介入只减不增:门控只拒绝、收窄只删工具,都不给 agent
|
|
51
|
+
自动介入只减不增:门控只拒绝、收窄只删工具,都不给 agent 新权限。危险门控采用两档模型,只在模型高置信判定拒绝时拦截;后端失败、低置信或响应不合法时一律不介入、按原路径执行,不因裁决故障阻断,也不替宿主放行。自检、收窄与防循环异常则跳过介入。发送到后端的危险摘要只含工具名与命令 / 路径并对疑似密钥脱敏,不含文件内容。
|
|
50
52
|
|
|
51
53
|
## 开发
|
|
52
54
|
|
|
53
|
-
服务端使用 Node.js `>=20.11`(`.mjs`,无需构建);客户端用 TypeScript + esbuild 构建。`npm test` 跑本地测试,`npm run build:client` 生成 `lib/client.js`。改动客户端后需重建并连同 `lib/client.js` 一起提交,否则 GitHub 安装拿到的是旧 bundle。0.
|
|
55
|
+
服务端使用 Node.js `>=20.11`(`.mjs`,无需构建);客户端用 TypeScript + esbuild 构建。`npm test` 跑本地测试,`npm run build:client` 生成 `lib/client.js`。改动客户端后需重建并连同 `lib/client.js` 一起提交,否则 GitHub 安装拿到的是旧 bundle。0.5.0 为体验版:介入能力已可用,但实际价值与阈值仍在探索,欢迎反馈决策日志观察到的误判与体感。
|
package/lib/client.js
CHANGED
|
@@ -51,6 +51,7 @@ var dictionaries = {
|
|
|
51
51
|
cardGate: "\u5371\u9669\u95E8\u63A7",
|
|
52
52
|
cardCheck: "\u4EA7\u51FA\u81EA\u68C0",
|
|
53
53
|
cardNarrow: "\u5DE5\u5177\u6536\u7A84",
|
|
54
|
+
cardComplete: "\u5B8C\u6210\u6838\u5BF9",
|
|
54
55
|
cardTimes: " \u6B21",
|
|
55
56
|
allow: "\u653E\u884C",
|
|
56
57
|
ask: "\u8BE2\u95EE",
|
|
@@ -79,6 +80,7 @@ var dictionaries = {
|
|
|
79
80
|
logGate: "\u95E8\u63A7",
|
|
80
81
|
logCheck: "\u81EA\u68C0",
|
|
81
82
|
logNarrow: "\u6536\u7A84",
|
|
83
|
+
logComplete: "\u5B8C\u6210\u6838\u5BF9",
|
|
82
84
|
logSuggested: "\u6A21\u578B\u5EFA\u8BAE",
|
|
83
85
|
logEvalFailed: "\u8BC4\u4F30\u5931\u8D25",
|
|
84
86
|
logScore: "\u5F97\u5206",
|
|
@@ -89,13 +91,14 @@ var dictionaries = {
|
|
|
89
91
|
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",
|
|
90
92
|
actionDeny: "\u5DF2\u62D2\u7EDD",
|
|
91
93
|
actionAsk: "\u8F6C\u4EA4\u5BA1\u6279",
|
|
94
|
+
actionPass: "\u672A\u5E72\u9884",
|
|
95
|
+
gatePassThrough: "\u672A\u5E72\u9884\uFF08\u6309\u539F\u8DEF\u5F84\u6267\u884C\uFF09",
|
|
92
96
|
checkResultLow: "\u5F97\u5206\u504F\u4F4E",
|
|
93
97
|
checkResultOk: "\u901A\u8FC7",
|
|
94
98
|
reasonUnavailable: "\u540E\u7AEF\u4E0D\u53EF\u8FBE",
|
|
95
99
|
reasonHttpError: "\u540E\u7AEF\u8FD4\u56DE\u9519\u8BEF",
|
|
96
100
|
reasonInvalidResponse: "\u54CD\u5E94\u683C\u5F0F\u4E0D\u7B26",
|
|
97
101
|
reasonLowConfidence: "\u7F6E\u4FE1\u5EA6\u4E0D\u8DB3",
|
|
98
|
-
reasonCapacity: "\u4F1A\u8BDD\u72B6\u6001\u5DF2\u6EE1",
|
|
99
102
|
reasonConfig: "\u914D\u7F6E\u65E0\u6548",
|
|
100
103
|
reasonTooMany: "\u5019\u9009\u8FC7\u591A",
|
|
101
104
|
reasonTooManyCount: "\u5019\u9009 {count} \u4E2A\u504F\u591A",
|
|
@@ -123,6 +126,25 @@ var dictionaries = {
|
|
|
123
126
|
featureCheckHint: "\u5173\u95ED\u540E\u4E0D\u518D\u5728\u56DE\u5408\u7ED3\u675F\u65F6\u7ED9\u6700\u7EC8\u56DE\u590D\u6253\u5206\u3002",
|
|
124
127
|
featureNarrow: "\u5DE5\u5177\u6536\u7A84",
|
|
125
128
|
featureNarrowHint: "\u5173\u95ED\u540E\u4E0D\u518D\u5728\u56DE\u5408\u5F00\u59CB\u65F6\u8BC4\u4F30\u5DE5\u5177\u76F8\u5173\u6027\u3002\u9632\u5FAA\u73AF\u4E3A\u786E\u5B9A\u6027\u672C\u5730\u4FDD\u62A4\uFF0C\u59CB\u7EC8\u751F\u6548\uFF0C\u4E0D\u5728\u6B64\u5F00\u5173\u8303\u56F4\u3002",
|
|
129
|
+
featureComplete: "\u4EFB\u52A1\u5B8C\u6210\u6838\u5BF9",
|
|
130
|
+
featureCompleteHint: "\u5173\u95ED\u540E\u4E0D\u518D\u5728\u56DE\u5408\u7ED3\u675F\u65F6\u9010\u9879\u6838\u5BF9\u7528\u6237\u660E\u786E\u5217\u51FA\u7684\u4EA4\u4ED8\u6761\u4EF6\u3002",
|
|
131
|
+
trendWarnHint: "\u672C\u4F1A\u8BDD\u6700\u8FD1\u8FDE\u7EED\u51E0\u8F6E\u4EA7\u51FA\u8D28\u91CF\u504F\u4F4E\uFF08\u9AD8\u7F6E\u4FE1\uFF09\u3002\u53EF\u80FD\u662F\u4EFB\u52A1\u96BE\u5EA6\u4E0A\u5347\u6216\u6A21\u578B\u72B6\u6001\u6CE2\u52A8\uFF0C\u5EFA\u8BAE\u5C1D\u8BD5\u8C03\u6574 prompt \u6216\u66F4\u6362\u6A21\u578B\u3002",
|
|
132
|
+
trendSevereHint: "\u672C\u4F1A\u8BDD\u4EA7\u51FA\u8D28\u91CF\u6301\u7EED\u504F\u4F4E\uFF08\u9AD8\u7F6E\u4FE1\uFF09\u3002\u5EFA\u8BAE\u8C03\u6574 prompt \u6216\u66F4\u6362\u6A21\u578B\uFF0C\u6216\u91CD\u5F00\u4F1A\u8BDD\u3002",
|
|
133
|
+
rateGroup: "\u6807\u6CE8\u8FD9\u6B21\u5224\u65AD",
|
|
134
|
+
rateGood: "\u5224\u5BF9\u4E86",
|
|
135
|
+
rateBad: "\u5224\u9519\u4E86",
|
|
136
|
+
rateUnsure: "\u8BF4\u4E0D\u51C6",
|
|
137
|
+
rateSummary: "\u5DF2\u6807\u6CE8 {rated} \u6761\uFF08\u5224\u5BF9 {good} / \u5224\u9519 {bad} / \u8BF4\u4E0D\u51C6 {unsure}\uFF09",
|
|
138
|
+
annotateFailed: "\u6807\u6CE8\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002",
|
|
139
|
+
completeSteer: "\u6709\u672A\u6EE1\u8DB3\u6761\u4EF6\u65F6\u81EA\u52A8\u8865\u4E00\u8F6E",
|
|
140
|
+
completeHint: "\u56DE\u5408\u7ED3\u675F\u65F6\uFF0C\u5BF9\u7528\u6237\u660E\u786E\u5217\u51FA\u7684\u591A\u4E2A\u4EA4\u4ED8\u6761\u4EF6\uFF08\u7F16\u53F7 / \u9879\u76EE\u7B26\u53F7 / \u591A\u547D\u4EE4\uFF09\u9010\u9879\u5224\u5B9A\u300C\u5DF2\u6EE1\u8DB3 / \u672A\u6EE1\u8DB3 / \u8BC1\u636E\u4E0D\u8DB3\u300D\uFF0C\u4EE5\u5DE5\u5177\u8BC1\u636E\u4E3A\u51C6\u3001\u56DE\u590D\u81EA\u79F0\u5B8C\u6210\u4E0D\u7B97\u8BC1\u636E\u3002\u5173\u95ED\uFF1A\u53EA\u628A\u7ED3\u679C\u8BB0\u8FDB\u65E5\u5FD7\u548C\u7EDF\u8BA1\uFF0C\u4E0D\u6253\u65AD\u5BF9\u8BDD\u3002\u5F00\u542F\uFF1A\u786E\u6709\u6761\u4EF6\u88AB\u5224\u300C\u672A\u6EE1\u8DB3\u300D\u4E14\u6A21\u578B\u6709\u628A\u63E1\u65F6\uFF0C\u81EA\u52A8\u4EE5\u4F60\u7684\u540D\u4E49\u8FFD\u52A0\u4E00\u6761\u63D0\u793A\u8BA9 AI \u590D\u6838\u8865\u5B8C\uFF1B\u300C\u8BC1\u636E\u4E0D\u8DB3\u300D\u4E0D\u89E6\u53D1\u8865\u5B8C\uFF0C\u540C\u4E00\u56DE\u5408\u6700\u591A\u8865\u4E00\u6B21\u3002",
|
|
141
|
+
completeResultOk: "\u6761\u4EF6\u9F50\u5907",
|
|
142
|
+
completeResultUnsatisfied: "\u6709\u672A\u6EE1\u8DB3",
|
|
143
|
+
completeConditions: "\u6761\u4EF6",
|
|
144
|
+
completeSatisfied: "\u6EE1\u8DB3",
|
|
145
|
+
completeUnsatisfied: "\u672A\u6EE1\u8DB3",
|
|
146
|
+
completeInsufficient: "\u8BC1\u636E\u4E0D\u8DB3",
|
|
147
|
+
completeSteered: "\u5DF2\u8865\u4E00\u8F6E",
|
|
126
148
|
backend: "\u88C1\u51B3\u540E\u7AEF",
|
|
127
149
|
url: "\u670D\u52A1\u5730\u5740",
|
|
128
150
|
model: "\u6A21\u578B",
|
|
@@ -142,7 +164,6 @@ var dictionaries = {
|
|
|
142
164
|
unavailable: "\u8FDE\u63A5\u4E0D\u53EF\u7528",
|
|
143
165
|
error: "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002",
|
|
144
166
|
loading: "\u6B63\u5728\u8BFB\u53D6\u4F1A\u8BDD\u72B6\u6001",
|
|
145
|
-
capacity: "\u4F1A\u8BDD\u72B6\u6001\u5BB9\u91CF\u5DF2\u6EE1\uFF1A\u6B64\u4F1A\u8BDD\u81EA\u52A8\u4ECB\u5165\u5DF2\u6682\u505C\uFF1B\u64CD\u4F5C\u4ECD\u53D7\u5BBF\u4E3B\u6743\u9650\u7B56\u7565\u7EA6\u675F\u3002",
|
|
146
167
|
destination: "\u88C1\u51B3\u65F6\u4F1A\u5411\u4EE5\u4E0B\u540E\u7AEF\u53D1\u9001\u5FC5\u8981\u7684\u8BC4\u4F30\u4E0A\u4E0B\u6587\uFF1A",
|
|
147
168
|
httpWarning: "\u6B64\u5730\u5740\u4F7F\u7528\u672A\u52A0\u5BC6\u7684 HTTP\u3002\u786E\u8BA4\u540E\uFF0CAPI Key \u4E0E\u8BC4\u4F30\u4E0A\u4E0B\u6587\u5C06\u4EE5\u660E\u6587\u53D1\u9001\u81F3\u8BE5\u5730\u5740\u3002"
|
|
148
169
|
},
|
|
@@ -165,6 +186,7 @@ var dictionaries = {
|
|
|
165
186
|
cardGate: "Danger gate",
|
|
166
187
|
cardCheck: "Output self-check",
|
|
167
188
|
cardNarrow: "Tool narrowing",
|
|
189
|
+
cardComplete: "Task completion",
|
|
168
190
|
cardTimes: "",
|
|
169
191
|
allow: "Allow",
|
|
170
192
|
ask: "Ask",
|
|
@@ -193,6 +215,7 @@ var dictionaries = {
|
|
|
193
215
|
logGate: "Gate",
|
|
194
216
|
logCheck: "Self-check",
|
|
195
217
|
logNarrow: "Narrow",
|
|
218
|
+
logComplete: "Completion",
|
|
196
219
|
logSuggested: "model suggested",
|
|
197
220
|
logEvalFailed: "evaluation failed",
|
|
198
221
|
logScore: "score",
|
|
@@ -203,13 +226,14 @@ var dictionaries = {
|
|
|
203
226
|
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.",
|
|
204
227
|
actionDeny: "denied",
|
|
205
228
|
actionAsk: "sent to approval",
|
|
229
|
+
actionPass: "no intervention",
|
|
230
|
+
gatePassThrough: "no intervention (proceeded on its original path)",
|
|
206
231
|
checkResultLow: "low score",
|
|
207
232
|
checkResultOk: "passed",
|
|
208
233
|
reasonUnavailable: "backend unreachable",
|
|
209
234
|
reasonHttpError: "backend returned an error",
|
|
210
235
|
reasonInvalidResponse: "unexpected response shape",
|
|
211
236
|
reasonLowConfidence: "low confidence",
|
|
212
|
-
reasonCapacity: "session state full",
|
|
213
237
|
reasonConfig: "invalid configuration",
|
|
214
238
|
reasonTooMany: "too many candidates",
|
|
215
239
|
reasonTooManyCount: "{count} candidates is too many",
|
|
@@ -237,6 +261,25 @@ var dictionaries = {
|
|
|
237
261
|
featureCheckHint: "When off, the final reply is no longer scored at the end of a turn.",
|
|
238
262
|
featureNarrow: "Tool narrowing",
|
|
239
263
|
featureNarrowHint: "When off, tool relevance is no longer evaluated at the start of a turn. The loop guard is a deterministic local safeguard that is always on and not covered by this switch.",
|
|
264
|
+
featureComplete: "Task completion check",
|
|
265
|
+
featureCompleteHint: "When off, the explicit deliverable conditions the user listed are no longer checked at turn end.",
|
|
266
|
+
trendWarnHint: "Output quality has been low for a few consecutive turns this session (high confidence). It may be rising task difficulty or model drift \u2014 consider adjusting the prompt or switching models.",
|
|
267
|
+
trendSevereHint: "Output quality has stayed low this session (high confidence). Consider adjusting the prompt, switching models, or starting a new session.",
|
|
268
|
+
rateGroup: "Rate this decision",
|
|
269
|
+
rateGood: "Correct",
|
|
270
|
+
rateBad: "Wrong",
|
|
271
|
+
rateUnsure: "Unsure",
|
|
272
|
+
rateSummary: "Rated {rated} ({good} correct / {bad} wrong / {unsure} unsure)",
|
|
273
|
+
annotateFailed: "Could not save the rating. Please retry.",
|
|
274
|
+
completeSteer: "Auto re-prompt on an unmet condition",
|
|
275
|
+
completeHint: "At turn end, each explicit deliverable condition the user listed (numbered / bulleted / multi-command) is judged satisfied / unsatisfied / insufficient evidence, with tool evidence as the source of truth \u2014 a reply claiming completion is not evidence. Off: results are only logged, the conversation is not interrupted. On: when a condition is confidently judged unsatisfied, one prompt is appended on your behalf asking the AI to review and finish it; insufficient evidence never triggers a re-prompt, and at most one per turn.",
|
|
276
|
+
completeResultOk: "conditions met",
|
|
277
|
+
completeResultUnsatisfied: "unmet found",
|
|
278
|
+
completeConditions: "conditions",
|
|
279
|
+
completeSatisfied: "met",
|
|
280
|
+
completeUnsatisfied: "unmet",
|
|
281
|
+
completeInsufficient: "insufficient",
|
|
282
|
+
completeSteered: "re-prompted",
|
|
240
283
|
backend: "Decision backend",
|
|
241
284
|
url: "Server URL",
|
|
242
285
|
model: "Model",
|
|
@@ -256,7 +299,6 @@ var dictionaries = {
|
|
|
256
299
|
unavailable: "Connection unavailable",
|
|
257
300
|
error: "Request failed. Please retry.",
|
|
258
301
|
loading: "Loading session settings",
|
|
259
|
-
capacity: "Session capacity reached: automatic intervention is paused here; host permission rules still apply.",
|
|
260
302
|
destination: "Decisions send the necessary evaluation context to this backend:",
|
|
261
303
|
httpWarning: "This address uses unencrypted HTTP. Your API key and evaluation context will be sent in plaintext to this address."
|
|
262
304
|
}
|
|
@@ -290,19 +332,17 @@ function SessionPanel({ sessionId, t }) {
|
|
|
290
332
|
const trigger = (0, import_react.useRef)(null);
|
|
291
333
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
292
334
|
const [enabled, setEnabled] = (0, import_react.useState)(null);
|
|
293
|
-
const [capacityExceeded, setCapacityExceeded] = (0, import_react.useState)(false);
|
|
294
335
|
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
295
336
|
const [metrics, setMetrics] = (0, import_react.useState)(null);
|
|
296
337
|
const [log, setLog] = (0, import_react.useState)([]);
|
|
338
|
+
const [analysis, setAnalysis] = (0, import_react.useState)(null);
|
|
339
|
+
const [ratings, setRatings] = (0, import_react.useState)({});
|
|
297
340
|
const [status, setStatus] = (0, import_react.useState)("idle");
|
|
298
341
|
const [message, setMessage] = (0, import_react.useState)("");
|
|
299
342
|
(0, import_react.useEffect)(() => {
|
|
300
343
|
let live = true;
|
|
301
344
|
void request(`session?sessionId=${encodeURIComponent(sessionId)}`).then((value) => {
|
|
302
|
-
if (live)
|
|
303
|
-
setEnabled(value.enabled);
|
|
304
|
-
setCapacityExceeded(Boolean(value.capacityExceeded));
|
|
305
|
-
}
|
|
345
|
+
if (live) setEnabled(value.enabled);
|
|
306
346
|
}).catch(() => {
|
|
307
347
|
if (live) setMessage(t("error"));
|
|
308
348
|
});
|
|
@@ -338,11 +378,35 @@ function SessionPanel({ sessionId, t }) {
|
|
|
338
378
|
if (alive) setLog(value.entries);
|
|
339
379
|
}).catch(() => {
|
|
340
380
|
});
|
|
381
|
+
void request(`logs?sessionId=${encodeURIComponent(sessionId)}`).then((value) => {
|
|
382
|
+
if (alive) {
|
|
383
|
+
setAnalysis(value);
|
|
384
|
+
setRatings(value.ratings ?? {});
|
|
385
|
+
}
|
|
386
|
+
}).catch(() => {
|
|
387
|
+
});
|
|
341
388
|
return () => {
|
|
342
389
|
alive = false;
|
|
343
390
|
element.close();
|
|
344
391
|
};
|
|
345
392
|
}, [open, t, sessionId]);
|
|
393
|
+
const annotate = async (id, rating) => {
|
|
394
|
+
setRatings((prev) => ({ ...prev, [id]: rating }));
|
|
395
|
+
try {
|
|
396
|
+
await request("annotate", {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: { "content-type": "application/json" },
|
|
399
|
+
body: JSON.stringify({ sessionId, target: id, rating })
|
|
400
|
+
});
|
|
401
|
+
} catch {
|
|
402
|
+
setMessage(t("annotateFailed"));
|
|
403
|
+
setRatings((prev) => {
|
|
404
|
+
const next = { ...prev };
|
|
405
|
+
delete next[id];
|
|
406
|
+
return next;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
};
|
|
346
410
|
const updateEnabled = async (next) => {
|
|
347
411
|
setBusy(true);
|
|
348
412
|
try {
|
|
@@ -352,7 +416,6 @@ function SessionPanel({ sessionId, t }) {
|
|
|
352
416
|
body: JSON.stringify({ sessionId, enabled: next })
|
|
353
417
|
});
|
|
354
418
|
setEnabled(value.enabled);
|
|
355
|
-
setCapacityExceeded(Boolean(value.capacityExceeded));
|
|
356
419
|
} catch {
|
|
357
420
|
setMessage(t("error"));
|
|
358
421
|
} finally {
|
|
@@ -360,8 +423,8 @@ function SessionPanel({ sessionId, t }) {
|
|
|
360
423
|
}
|
|
361
424
|
};
|
|
362
425
|
const time = (at) => new Date(at).toLocaleTimeString();
|
|
363
|
-
const reasonText = (reason) => reason === "unavailable" || reason === "unreachable" ? t("reasonUnavailable") : reason === "http-error" ? t("reasonHttpError") : reason === "invalid-response" ? t("reasonInvalidResponse") : reason === "low-confidence" ? t("reasonLowConfidence") : reason === "
|
|
364
|
-
const actionText = (action) => action === "deny" ? t("actionDeny") : action === "ask" ? t("actionAsk") : action;
|
|
426
|
+
const reasonText = (reason) => reason === "unavailable" || reason === "unreachable" ? t("reasonUnavailable") : reason === "http-error" ? t("reasonHttpError") : reason === "invalid-response" ? t("reasonInvalidResponse") : reason === "low-confidence" ? t("reasonLowConfidence") : reason === "too-many-candidates" ? t("reasonTooMany") : reason === "low-keep" ? t("reasonLowKeep") : reason === "config" ? t("reasonConfig") : reason;
|
|
427
|
+
const actionText = (action) => action === "deny" ? t("actionDeny") : action === "ask" ? t("actionAsk") : action === "pass" ? t("actionPass") : action;
|
|
365
428
|
const failDetail = (entry) => {
|
|
366
429
|
const reason = reasonText(entry.reason);
|
|
367
430
|
if (entry.reason === "low-confidence" && typeof entry.confidence === "number") return `${reason}\uFF08${t("logConfidence")} ${entry.confidence}\uFF09`;
|
|
@@ -376,27 +439,49 @@ function SessionPanel({ sessionId, t }) {
|
|
|
376
439
|
tool,
|
|
377
440
|
" ",
|
|
378
441
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
379
|
-
t("
|
|
442
|
+
t("gatePassThrough"),
|
|
380
443
|
entry.reason ? ` \xB7 ${failDetail(entry)}` : ""
|
|
381
444
|
] })
|
|
382
445
|
] });
|
|
383
|
-
|
|
384
|
-
|
|
446
|
+
if (entry.outcome === "deny") {
|
|
447
|
+
const suggestion2 = entry.suggestion && entry.suggestion !== "deny" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
448
|
+
" \xB7 ",
|
|
449
|
+
t("logSuggested"),
|
|
450
|
+
" ",
|
|
451
|
+
actionText(entry.suggestion)
|
|
452
|
+
] }) : null;
|
|
453
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
454
|
+
t("logGate"),
|
|
455
|
+
" \xB7 ",
|
|
456
|
+
tool,
|
|
457
|
+
" ",
|
|
458
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-verdict", children: [
|
|
459
|
+
"\u2192 ",
|
|
460
|
+
t("actionDeny")
|
|
461
|
+
] }),
|
|
462
|
+
suggestion2
|
|
463
|
+
] });
|
|
464
|
+
}
|
|
465
|
+
const suggestion = entry.suggestion ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
385
466
|
" \xB7 ",
|
|
386
467
|
t("logSuggested"),
|
|
387
468
|
" ",
|
|
388
469
|
actionText(entry.suggestion)
|
|
389
470
|
] }) : null;
|
|
471
|
+
const lowConf = entry.reason === "low-confidence" && typeof entry.confidence === "number" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
472
|
+
" \xB7 ",
|
|
473
|
+
t("reasonLowConfidence"),
|
|
474
|
+
" ",
|
|
475
|
+
entry.confidence
|
|
476
|
+
] }) : null;
|
|
390
477
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
391
478
|
t("logGate"),
|
|
392
479
|
" \xB7 ",
|
|
393
480
|
tool,
|
|
394
481
|
" ",
|
|
395
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
] }),
|
|
399
|
-
suggestion
|
|
482
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-log-verdict", children: t("gatePassThrough") }),
|
|
483
|
+
suggestion,
|
|
484
|
+
lowConf
|
|
400
485
|
] });
|
|
401
486
|
};
|
|
402
487
|
const renderCheck = (entry) => {
|
|
@@ -485,18 +570,57 @@ function SessionPanel({ sessionId, t }) {
|
|
|
485
570
|
modeTag
|
|
486
571
|
] });
|
|
487
572
|
};
|
|
488
|
-
const
|
|
573
|
+
const renderComplete = (entry) => {
|
|
574
|
+
const label = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("logComplete") });
|
|
575
|
+
if (entry.outcome === "error") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
576
|
+
label,
|
|
577
|
+
" ",
|
|
578
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
579
|
+
t("logEvalFailed"),
|
|
580
|
+
entry.reason ? ` \xB7 ${failDetail(entry)}` : ""
|
|
581
|
+
] })
|
|
582
|
+
] });
|
|
583
|
+
const result = entry.outcome === "unsatisfied" ? t("completeResultUnsatisfied") : t("completeResultOk");
|
|
584
|
+
const parts = [];
|
|
585
|
+
if (typeof entry.satisfied === "number") parts.push(`${t("completeSatisfied")} ${entry.satisfied}`);
|
|
586
|
+
if (typeof entry.unsatisfied === "number" && entry.unsatisfied > 0) parts.push(`${t("completeUnsatisfied")} ${entry.unsatisfied}`);
|
|
587
|
+
if (typeof entry.insufficient === "number" && entry.insufficient > 0) parts.push(`${t("completeInsufficient")} ${entry.insufficient}`);
|
|
588
|
+
const tally = parts.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
589
|
+
" \xB7 ",
|
|
590
|
+
parts.join(" / ")
|
|
591
|
+
] }) : null;
|
|
592
|
+
const steer = entry.steered ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
593
|
+
" \xB7 ",
|
|
594
|
+
t("completeSteered")
|
|
595
|
+
] }) : null;
|
|
596
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
597
|
+
label,
|
|
598
|
+
" \xB7 ",
|
|
599
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-log-verdict", children: result }),
|
|
600
|
+
typeof entry.conditions === "number" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
601
|
+
" \xB7 ",
|
|
602
|
+
t("completeConditions"),
|
|
603
|
+
" ",
|
|
604
|
+
entry.conditions
|
|
605
|
+
] }) : null,
|
|
606
|
+
tally,
|
|
607
|
+
steer
|
|
608
|
+
] });
|
|
609
|
+
};
|
|
610
|
+
const outcomeTagOf = (entry) => entry.kind === "narrow" ? entry.outcome === "error" ? "error" : entry.outcome === "applied" ? "ok" : "ok" : entry.kind === "complete" ? entry.outcome === "error" ? "error" : entry.outcome === "unsatisfied" ? "warn" : "ok" : outcomeTag(entry);
|
|
489
611
|
const statusLabel = status === "ok" ? t("statusOk") : status === "down" ? t("statusDown") : status === "checking" ? t("statusChecking") : t("statusIdle");
|
|
490
612
|
const attempts = metrics?.attempts ?? 0;
|
|
613
|
+
const severity = metrics?.check?.trend?.severity ?? "normal";
|
|
614
|
+
const trendTitle = severity === "severe" ? t("trendSevereHint") : severity === "warn" ? t("trendWarnHint") : void 0;
|
|
491
615
|
const checkValid = metrics?.check ? metrics.check.attempts - metrics.check.failures : 0;
|
|
492
616
|
const checkPassRate = metrics?.check && checkValid > 0 ? Math.round((checkValid - metrics.check.low) / checkValid * 100) : null;
|
|
493
617
|
const narrowValid = metrics?.narrow ? metrics.narrow.attempts - metrics.narrow.failures : 0;
|
|
494
618
|
const narrowApplyRate = metrics?.narrow && narrowValid > 0 ? Math.round(metrics.narrow.applied / narrowValid * 100) : null;
|
|
495
619
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
496
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { ref: trigger, className:
|
|
497
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "decision-trigger-icon", width: "
|
|
498
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
499
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "
|
|
620
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { ref: trigger, className: `decision-trigger decision-trend-${severity}`, type: "button", "aria-haspopup": "dialog", "aria-expanded": open, "aria-busy": enabled === null, "aria-label": t("button"), title: trendTitle, onClick: () => setOpen(true), children: [
|
|
621
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "decision-trigger-icon", width: "15", height: "15", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [
|
|
622
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "8", cy: "8", r: "6.4", stroke: "currentColor", "stroke-width": "1.3" }),
|
|
623
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8 4.6 11.3 11H4.7L8 4.6Z", fill: "currentColor", stroke: "none" })
|
|
500
624
|
] }),
|
|
501
625
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-trigger-text", children: [
|
|
502
626
|
t("triggerDecisions"),
|
|
@@ -538,7 +662,6 @@ function SessionPanel({ sessionId, t }) {
|
|
|
538
662
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "decision-body", children: [
|
|
539
663
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
|
|
540
664
|
enabled === null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", children: t("loading") }),
|
|
541
|
-
capacityExceeded && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", children: t("capacity") }),
|
|
542
665
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "decision-toggle", children: [
|
|
543
666
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-toggle-text", children: t("enabled") }),
|
|
544
667
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-toggle-box", children: [
|
|
@@ -548,7 +671,7 @@ function SessionPanel({ sessionId, t }) {
|
|
|
548
671
|
className: "decision-toggle-input",
|
|
549
672
|
type: "checkbox",
|
|
550
673
|
checked: enabled === true,
|
|
551
|
-
disabled: busy || enabled === null
|
|
674
|
+
disabled: busy || enabled === null,
|
|
552
675
|
onChange: (event) => void updateEnabled(event.target.checked)
|
|
553
676
|
}
|
|
554
677
|
),
|
|
@@ -567,7 +690,7 @@ function SessionPanel({ sessionId, t }) {
|
|
|
567
690
|
] }),
|
|
568
691
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
|
|
569
692
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("metrics") }),
|
|
570
|
-
!metrics?.gate && !metrics?.check && !metrics?.narrow ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "decision-empty", role: "status", children: t("empty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "decision-cards", children: [
|
|
693
|
+
!metrics?.gate && !metrics?.check && !metrics?.narrow && !metrics?.complete ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "decision-empty", role: "status", children: t("empty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "decision-cards", children: [
|
|
571
694
|
metrics.gate && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("article", { className: "decision-card decision-card-wide", children: [
|
|
572
695
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { className: "decision-card-head", children: [
|
|
573
696
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-card-title", children: t("cardGate") }),
|
|
@@ -662,6 +785,33 @@ function SessionPanel({ sessionId, t }) {
|
|
|
662
785
|
t("narrowApplyRate"),
|
|
663
786
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("b", { children: narrowApplyRate === null ? t("rateNa") : `${narrowApplyRate}%` })
|
|
664
787
|
] }) })
|
|
788
|
+
] }),
|
|
789
|
+
metrics.complete && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("article", { className: "decision-card", children: [
|
|
790
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { className: "decision-card-head", children: [
|
|
791
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-card-title", children: t("cardComplete") }),
|
|
792
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-card-total", children: [
|
|
793
|
+
metrics.complete.attempts,
|
|
794
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t("cardTimes") })
|
|
795
|
+
] })
|
|
796
|
+
] }),
|
|
797
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "decision-card-stats", children: [
|
|
798
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "decision-stat", children: [
|
|
799
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-stat-num", children: metrics.complete.satisfied }),
|
|
800
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-stat-label", children: t("completeSatisfied") })
|
|
801
|
+
] }),
|
|
802
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "decision-stat", children: [
|
|
803
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-stat-num decision-stat-warn", children: metrics.complete.unsatisfied }),
|
|
804
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-stat-label", children: t("completeUnsatisfied") })
|
|
805
|
+
] }),
|
|
806
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "decision-stat", children: [
|
|
807
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-stat-num decision-stat-muted", children: metrics.complete.insufficient }),
|
|
808
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-stat-label", children: t("completeInsufficient") })
|
|
809
|
+
] })
|
|
810
|
+
] }),
|
|
811
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("footer", { className: "decision-card-foot decision-card-rate", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
812
|
+
t("failures"),
|
|
813
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("b", { children: metrics.complete.failures })
|
|
814
|
+
] }) })
|
|
665
815
|
] })
|
|
666
816
|
] })
|
|
667
817
|
] }),
|
|
@@ -669,8 +819,24 @@ function SessionPanel({ sessionId, t }) {
|
|
|
669
819
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("log") }),
|
|
670
820
|
log.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "decision-empty", role: "status", children: t("logEmpty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className: "decision-log", children: log.slice().reverse().map((entry, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: `decision-log-item decision-log-${outcomeTagOf(entry)}`, children: [
|
|
671
821
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-log-time", children: time(entry.at) }),
|
|
672
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-log-body", children: entry.kind === "gate" ? renderGate(entry) : entry.kind === "narrow" ? renderNarrow(entry) : renderCheck(entry) })
|
|
673
|
-
|
|
822
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "decision-log-body", children: entry.kind === "gate" ? renderGate(entry) : entry.kind === "narrow" ? renderNarrow(entry) : entry.kind === "complete" ? renderComplete(entry) : renderCheck(entry) }),
|
|
823
|
+
entry.id && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-rate", role: "group", "aria-label": t("rateGroup"), children: [
|
|
824
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: `decision-rate-btn${ratings[entry.id] === "good" ? " decision-rate-on decision-rate-good" : ""}`, "aria-label": t("rateGood"), title: t("rateGood"), "aria-pressed": ratings[entry.id] === "good", onClick: () => void annotate(entry.id, "good"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [
|
|
825
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 7.2 8.1 2.4c.6-.9 2-.5 2 .6V6h3.2c.9 0 1.5.8 1.3 1.6l-1.1 4.6c-.2.8-.9 1.3-1.7 1.3H5V7.2Z", stroke: "currentColor", "stroke-width": "1.2", "stroke-linejoin": "round" }),
|
|
826
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 7.2H2.6v6.3H5", stroke: "currentColor", "stroke-width": "1.2", "stroke-linejoin": "round" })
|
|
827
|
+
] }) }),
|
|
828
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: `decision-rate-btn${ratings[entry.id] === "bad" ? " decision-rate-on decision-rate-bad" : ""}`, "aria-label": t("rateBad"), title: t("rateBad"), "aria-pressed": ratings[entry.id] === "bad", onClick: () => void annotate(entry.id, "bad"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [
|
|
829
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M11 8.8 7.9 13.6c-.6.9-2 .5-2-.6V10H2.7c-.9 0-1.5-.8-1.3-1.6l1.1-4.6C2.7 3 3.4 2.5 4.2 2.5H11v6.3Z", stroke: "currentColor", "stroke-width": "1.2", "stroke-linejoin": "round" }),
|
|
830
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M11 8.8h2.4V2.5H11", stroke: "currentColor", "stroke-width": "1.2", "stroke-linejoin": "round" })
|
|
831
|
+
] }) }),
|
|
832
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: `decision-rate-btn${ratings[entry.id] === "unsure" ? " decision-rate-on decision-rate-unsure" : ""}`, "aria-label": t("rateUnsure"), title: t("rateUnsure"), "aria-pressed": ratings[entry.id] === "unsure", onClick: () => void annotate(entry.id, "unsure"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [
|
|
833
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "8", cy: "8", r: "6.2", stroke: "currentColor", "stroke-width": "1.2" }),
|
|
834
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M6.3 6.2c0-1 .8-1.7 1.7-1.7s1.7.7 1.7 1.6c0 1.3-1.6 1.4-1.7 2.6", stroke: "currentColor", "stroke-width": "1.2", "stroke-linecap": "round", "stroke-linejoin": "round" }),
|
|
835
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "8", cy: "11.4", r: ".8", fill: "currentColor" })
|
|
836
|
+
] }) })
|
|
837
|
+
] })
|
|
838
|
+
] }, entry.id ?? `${entry.at}-${index}`)) }),
|
|
839
|
+
analysis && analysis.annotations.rated > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "decision-muted decision-rate-summary", children: t("rateSummary", { rated: analysis.annotations.rated, good: analysis.annotations.good, bad: analysis.annotations.bad, unsure: analysis.annotations.unsure }) })
|
|
674
840
|
] }),
|
|
675
841
|
message && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", className: "decision-message decision-message-warn", children: message })
|
|
676
842
|
] })
|
|
@@ -747,6 +913,8 @@ function ConfigForm({ t, fetchFn = fetch }) {
|
|
|
747
913
|
const [gateOn, setGateOn] = (0, import_react2.useState)(true);
|
|
748
914
|
const [checkOn, setCheckOn] = (0, import_react2.useState)(true);
|
|
749
915
|
const [narrowOn, setNarrowOn] = (0, import_react2.useState)(true);
|
|
916
|
+
const [completeOn, setCompleteOn] = (0, import_react2.useState)(true);
|
|
917
|
+
const [completeMode, setCompleteMode] = (0, import_react2.useState)("observe");
|
|
750
918
|
const [key, setKey] = (0, import_react2.useState)("");
|
|
751
919
|
const [busy, setBusy] = (0, import_react2.useState)(false);
|
|
752
920
|
const [advancedDirty, setAdvancedDirty] = (0, import_react2.useState)(false);
|
|
@@ -758,11 +926,13 @@ function ConfigForm({ t, fetchFn = fetch }) {
|
|
|
758
926
|
setGateOn(features?.gate !== false);
|
|
759
927
|
setCheckOn(features?.check !== false);
|
|
760
928
|
setNarrowOn(features?.narrow !== false);
|
|
929
|
+
setCompleteOn(features?.complete !== false);
|
|
761
930
|
};
|
|
762
931
|
const applyConfig = (value) => {
|
|
763
932
|
setConfig(value);
|
|
764
933
|
setCheckMode(value.checkSettings?.mode ?? "observe");
|
|
765
934
|
setNarrowMode(value.narrowSettings?.mode ?? "enforce");
|
|
935
|
+
setCompleteMode(value.completeSettings?.mode ?? "observe");
|
|
766
936
|
setKeepPrefixes(prefixesText(value.narrowSettings));
|
|
767
937
|
setMaxCandidates(String(maxCandidatesOf(value.narrowSettings)));
|
|
768
938
|
setThreshold(String(thresholdOf(value.narrowSettings)));
|
|
@@ -800,7 +970,8 @@ function ConfigForm({ t, fetchFn = fetch }) {
|
|
|
800
970
|
body: JSON.stringify({
|
|
801
971
|
checkSettings: { mode: checkMode },
|
|
802
972
|
narrowSettings: { mode: narrowMode, keepPrefixes: parsePrefixes(keepPrefixes), maxCandidates: cap, threshold: thresh },
|
|
803
|
-
|
|
973
|
+
completeSettings: { mode: completeMode },
|
|
974
|
+
features: { gate: gateOn, check: checkOn, narrow: narrowOn, complete: completeOn }
|
|
804
975
|
})
|
|
805
976
|
});
|
|
806
977
|
applyConfig(value);
|
|
@@ -918,7 +1089,20 @@ ${url}`)) return;
|
|
|
918
1089
|
}
|
|
919
1090
|
}
|
|
920
1091
|
),
|
|
921
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "decision-muted", children: t("featureNarrowHint") })
|
|
1092
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "decision-muted", children: t("featureNarrowHint") }),
|
|
1093
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1094
|
+
Toggle,
|
|
1095
|
+
{
|
|
1096
|
+
label: t("featureComplete"),
|
|
1097
|
+
checked: completeOn,
|
|
1098
|
+
disabled: !ready,
|
|
1099
|
+
onChange: (next) => {
|
|
1100
|
+
setCompleteOn(next);
|
|
1101
|
+
setAdvancedDirty(true);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
),
|
|
1105
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "decision-muted", children: t("featureCompleteHint") })
|
|
922
1106
|
] })
|
|
923
1107
|
] }),
|
|
924
1108
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("details", { className: "decision-collapse", children: [
|
|
@@ -958,7 +1142,20 @@ ${url}`)) return;
|
|
|
958
1142
|
}
|
|
959
1143
|
}
|
|
960
1144
|
),
|
|
961
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "decision-muted", children: t("narrowHint") })
|
|
1145
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "decision-muted", children: t("narrowHint") }),
|
|
1146
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1147
|
+
Toggle,
|
|
1148
|
+
{
|
|
1149
|
+
label: t("completeSteer"),
|
|
1150
|
+
checked: completeMode === "steer",
|
|
1151
|
+
disabled: !ready || !completeOn,
|
|
1152
|
+
onChange: (next) => {
|
|
1153
|
+
setCompleteMode(next ? "steer" : "observe");
|
|
1154
|
+
setAdvancedDirty(true);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
),
|
|
1158
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "decision-muted", children: t("completeHint") })
|
|
962
1159
|
] }),
|
|
963
1160
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "decision-field-block", children: [
|
|
964
1161
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "decision-subhead", children: t("advancedParams") }),
|
|
@@ -1112,6 +1309,16 @@ var css = `
|
|
|
1112
1309
|
.decision-trigger{position:relative;display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:14rem;border:none;border-radius:var(--dsw-radius-sm,6px);background:0 0;color:var(--dsw-alias-label-tertiary,#77818b);padding:1px 8px;font:inherit;font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);font-weight:400;line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer}
|
|
1113
1310
|
.decision-trigger:hover,.decision-trigger[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover,#eef1f3);color:var(--dsw-alias-label-secondary,#59636e)}
|
|
1114
1311
|
.decision-trigger-icon{flex:none;color:inherit}
|
|
1312
|
+
/* Quality-trend severity on the radar-triangle icon. Only the icon is tinted, via
|
|
1313
|
+
a CSS variable so light/dark are one pair of overrides. Multimodal: the tooltip
|
|
1314
|
+
text and the icon shape carry the meaning too, not color alone. First version
|
|
1315
|
+
is color-only; a pulse animation is added later behind prefers-reduced-motion. */
|
|
1316
|
+
.decision-trend-warn .decision-trigger-icon{color:var(--decision-trend-warn,#c98a1e)}
|
|
1317
|
+
.decision-trend-severe .decision-trigger-icon{color:var(--decision-trend-severe,#c0392b)}
|
|
1318
|
+
@media (prefers-color-scheme:dark){
|
|
1319
|
+
.decision-trend-warn .decision-trigger-icon{color:var(--decision-trend-warn,#e0a83e)}
|
|
1320
|
+
.decision-trend-severe .decision-trigger-icon{color:var(--decision-trend-severe,#f0645a)}
|
|
1321
|
+
}
|
|
1115
1322
|
.decision-trigger-text{min-width:0;flex:0 1 auto;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
|
|
1116
1323
|
.decision-trigger-text b{color:inherit;font-weight:500;font-variant-numeric:tabular-nums}
|
|
1117
1324
|
.decision-trigger:focus-visible,.decision-dialog button:focus-visible,.decision-dialog input:focus-visible,.decision-config input:focus-visible,.decision-config button:focus-visible{outline:2px solid #1f5d50;outline-offset:2px}
|
|
@@ -1163,6 +1370,14 @@ var css = `
|
|
|
1163
1370
|
.decision-log-item.decision-log-ok{border-left-color:#1f8a70}
|
|
1164
1371
|
.decision-log-time{color:var(--dsw-alias-label-tertiary,#77818b);font-variant-numeric:tabular-nums;flex:0 0 auto}
|
|
1165
1372
|
.decision-log-body{color:var(--dsw-alias-label-primary,#202124);flex:1 1 auto;min-width:0}
|
|
1373
|
+
.decision-rate{flex:0 0 auto;display:inline-flex;gap:1px;align-self:center}
|
|
1374
|
+
.decision-rate-btn{display:inline-flex;align-items:center;justify-content:center;width:1.55rem;height:1.55rem;border:1px solid transparent;background:0 0;border-radius:7px;padding:0;cursor:pointer;color:var(--dsw-alias-label-tertiary,#9aa5b1);transition:color .12s,background .12s}
|
|
1375
|
+
.decision-rate-btn:hover{color:var(--dsw-alias-label-secondary,#59636e);background:var(--dsw-alias-interactive-bg-hover,#eef1f3)}
|
|
1376
|
+
.decision-rate-btn.decision-rate-on{background:var(--dsw-alias-interactive-bg-hover,#eef1f3)}
|
|
1377
|
+
.decision-rate-btn.decision-rate-good{color:#1f8a70}
|
|
1378
|
+
.decision-rate-btn.decision-rate-bad{color:#c0392b}
|
|
1379
|
+
.decision-rate-btn.decision-rate-unsure{color:#c98a1e}
|
|
1380
|
+
.decision-rate-summary{margin-top:.5rem}
|
|
1166
1381
|
.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)}
|
|
1167
1382
|
.decision-log-verdict{font-weight:600}
|
|
1168
1383
|
.decision-log-deny .decision-log-verdict{color:#c0392b}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-decision-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|