dsh-decision-layer 0.7.2 → 0.7.3
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 +10 -10
- package/README.md +10 -10
- package/docs/en/DETAIL.md +2 -2
- package/docs/zh/DETAIL.md +2 -2
- package/lib/client.js +20 -14
- package/lib/host/decision-context.d.ts +21 -0
- package/lib/host/decision-context.js +187 -0
- package/lib/host/excerpt.d.ts +1 -0
- package/lib/host/excerpt.js +17 -3
- package/lib/host/index.js +87 -90
- package/lib/host/narrowing.d.ts +3 -0
- package/lib/host/narrowing.js +3 -2
- package/lib/host/self-check.d.ts +3 -2
- package/lib/host/self-check.js +11 -4
- package/lib/host/session-state.js +1 -0
- package/lib/host/task-completion.d.ts +3 -2
- package/lib/host/task-completion.js +26 -8
- package/lib/host/types.d.ts +22 -0
- 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.3 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
|
|
|
@@ -39,8 +39,8 @@ Restart `dsh web` and refresh after installing or updating: the host and client
|
|
|
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
41
|
- **Output self-check and quality trend** — scores the final reply at turn end (0–2), observe-only by default; an opt-in low-score re-prompt fires at most once per turn. Several consecutive high-confidence low scores tint the dock trigger grey → amber → red as a reminder. Self-check rows carry the turn number and the same task excerpt, so you can tell which turn was scored.
|
|
42
|
-
- **Tool narrowing** — at turn start, drops optional tools by task relevance, intersecting the host-allowed set and never expanding it;
|
|
43
|
-
- **Task-completion review** — when the user spells out multiple delivery conditions, each is judged satisfied / unsatisfied / insufficient evidence,
|
|
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.
|
|
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.
|
|
46
46
|
- **Configuration and persistence** — each decision point has its own switch and configurable thresholds in the plugin details, written to `~/.config/dsh-decision-layer/config.json` and applied live. Defaults and field names are in [ADR-0006](<docs/adr/0006-configurable-thresholds.md>). You can also edit that file by hand, but only with valid values: an out-of-range value, a misspelled field, or a wrong type makes the whole config fail validation, the config page cannot load it, and every decision point reverts to its own safe default (see [ADR-0005](<docs/adr/0005-config-wholesale-replace-and-live-reload.md>)). Decision logs and session metrics persist for 30 days for review and calibration.
|
|
@@ -50,18 +50,18 @@ Restart `dsh web` and refresh after installing or updating: the host and client
|
|
|
50
50
|
|
|
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
|
-
- Gate: the tool name plus redacted command/path summaries (up to 2048 characters each).
|
|
54
|
-
- Self-check: the turn's final reply (up to about 24K characters).
|
|
55
|
-
- Narrowing: the
|
|
56
|
-
- Task-completion review: delivery conditions extracted from this turn's user
|
|
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 (up to about 24K characters).
|
|
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
|
+
- 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.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Only real user messages are used as requests: host-injected content such as time notices, runtime details, memory snippets, system reminders and background-job updates is excluded or stripped, but anything you paste into the conversation still leaves your machine. The remote provider may log, retain, or otherwise use it depending on its terms; redaction and truncation reduce risk but are not anonymization.
|
|
60
60
|
|
|
61
|
-
Two kinds of data are also stored locally for 30 days: decision logs in `~/.config/dsh-decision-layer/logs/` and session snapshots in `~/.config/dsh-decision-layer/state/`. They include redacted command/path summaries;
|
|
61
|
+
Two kinds of data are also stored locally for 30 days: decision logs in `~/.config/dsh-decision-layer/logs/` and session snapshots in `~/.config/dsh-decision-layer/state/`. They include redacted command/path summaries; every decision row's task excerpt is at most 512 characters (injected memory/system-reminder blocks removed, common credential forms masked) and a row may carry its turn and step numbers. No extra copies of the full background, execution progress, tool results or reply text are persisted — only the excerpt and decision counters. The excerpts are shown in the local session panel and decision-analysis view only; outbound adjudication content is the list above, and you can delete the local data yourself.
|
|
62
62
|
|
|
63
63
|
**If your session content is sensitive, prefer a locally deployed model** so adjudication data never leaves the machine. `tev1` is served natively by ollama at `/v1/systemone`; see [Running with a local tev1](<docs/en/deploy-tev1-with-plugin.md>). Any other local model works as long as it exposes a compatible `/v1/systemone` endpoint, pointed to from the config page.
|
|
64
64
|
|
|
65
65
|
## Safety boundary and development
|
|
66
66
|
|
|
67
|
-
Automatic intervention only subtracts: the gate only denies, narrowing only removes tools, and neither grants new permissions. The gate only blocks on a high-confidence deny; on backend failure, low confidence, or an invalid response it does not intervene and runs the host's original path. Self-check, narrowing, and loop-guard errors skip intervention. The redacted gate summary sent to the backend carries only tool name and command/path with secret redaction; the plugin never reads file content, but the command itself (for example a heredoc or text written with echo) is included as-is. 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.3 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.3 是体验版。** 插件功能已可用,但裁决介入能带来多少实际价值仍在探索中——各决策点的阈值都是未经验证的启发式默认值,建议先以观察心态体验,别当成经过验证的效果保证。功能与后续进度以 [ROADMAP.md](<ROADMAP.md>) 为准。
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
@@ -39,8 +39,8 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
|
|
|
39
39
|
- **手动裁决**:`decision_evaluate` 工具与 `decision-layer` skill 可显式提交 `noul` / `score` / `choice` 问题,`decision_check_connection` 做连通测试;手动调用不计入自动裁决指标,也不改变 agent 行为。
|
|
40
40
|
- **危险门控**:破坏性工具调用执行前用脱敏摘要请后端裁决,只有高置信「拒绝」才拦截;放行、询问、低置信、后端失败或响应不合法一律不介入,按宿主原路径执行。危险规则与拦截置信度门槛可配。门控日志带脱敏命令 / 路径与本轮任务节选,便于回看拦截是否合理。
|
|
41
41
|
- **产出自检与质量趋势**:回合结束给最终回复打分(0–2),默认只记录;可开启低分补完,同一回合最多补一次。连续多次高置信低分时,dock 触发器图标会从灰变橙、变红作为提醒。自检日志带回合号与同一套任务节选,便于对照是哪一轮被打分。
|
|
42
|
-
-
|
|
43
|
-
- **任务完成核对**:用户明确列出多个交付条件时,逐项判定「已满足 / 未满足 /
|
|
42
|
+
- **工具收窄**:回合开始时、以及同一回合内你提出新要求时,按任务相关性摘掉可选工具,只在宿主已允许的集合内取交集、不扩权;每次重新判断前先释放上一轮的旧限制,回合结束即解除,时间提示、后台通知等非用户要求不会触发重判。核心工具与保留前缀命中的工具始终保留,可选工具过多时自动降为仅观察、保留过少时视为判定不可信并跳过,对小上下文后端自动分批。默认真正生效,也可切到仅观察。收窄日志附带回合号、步骤号和可展开的任务节选,便于对照被摘除的工具评估判断是否合理;旧日志不会补录场景。
|
|
43
|
+
- **任务完成核对**:用户明确列出多个交付条件时,逐项判定「已满足 / 未满足 / 证据不足」,条件只取本回合最近一次要求(避免旧清单被恢复),以本回合工具证据为准(上一回合的成功不算数,回复自述不算证据);默认只观察,可配补完。
|
|
44
44
|
- **防循环**:同一调用连续重复超过阈值即拒绝,确定性本地逻辑,不依赖后端;不受各决策点开关影响,但会话总开关关闭时同样暂停。
|
|
45
45
|
- **会话面板与决策分析页**:面板展示自动裁决指标、会话总开关和最近日志;「决策分析」tab 按会话展示过程画像、趋势回测与判断准确度,并支持逐条标注和开关本会话的自动介入。
|
|
46
46
|
- **配置与持久化**:各决策点可独立开关,阈值在插件详情页调整,写入 `~/.config/dsh-decision-layer/config.json` 并热生效。默认值与字段说明见 [ADR-0006](<docs/adr/0006-configurable-thresholds.md>)。也可直接手改该文件,但只能写合法值:越界、拼错字段名或类型不符会让整份配置校验失败,配置页读不出来,各决策点退回各自的安全默认(见 [ADR-0005](<docs/adr/0005-config-wholesale-replace-and-live-reload.md>))。决策日志与会话指标落盘 30 天,供回看和校准。
|
|
@@ -50,15 +50,15 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
|
|
|
50
50
|
|
|
51
51
|
配置 Key 后,插件默认把裁决请求发往远程端点 `https://api.typesafe.ai`(模型 `jev-latest`)。裁决会携带具体上下文:
|
|
52
52
|
|
|
53
|
-
- 门控:工具名与脱敏后的命令 / 路径摘要(各最多 2048
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
53
|
+
- 门控:工具名与脱敏后的命令 / 路径摘要(各最多 2048 字符),不含对话内容。
|
|
54
|
+
- 产出自检:本轮请求、最近必要的用户要求与 AI 回复(作背景,各有限额度,已遮蔽凭据),以及本轮最终回复(最多约 24K 字符)。
|
|
55
|
+
- 工具收窄:当前有效请求、必要前文与最近执行进展(工具名、成败、有限的命令 / 路径与 shell 结果摘录)与候选工具的名称 / 描述。
|
|
56
|
+
- 完成核对:从本回合最近一次用户要求中提取的交付条件(最多 20 条,不合并同一回合的多次要求)、本回合最近一次要求与必要前文(作背景,已遮蔽凭据)、本回合工具证据(工具名、成败、有限的命令 / 路径,shell 类工具另有有限结果摘录,并对无标签的长随机串再遮蔽一次)、回复前约 8K 字符。文件读取、搜索与网络工具的结果原文不外发。
|
|
57
57
|
- 手动裁决:`decision_evaluate` 与 `decision-layer` skill 会把 agent 提交的内容原样发送。
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
进入裁决的用户消息只取真实的那些,时间提示、运行环境、记忆片段、系统提醒与后台通知等宿主注入内容会被排除或剥离;但用户自己粘贴进对话的内容仍会离开本机。远程服务提供方可能记录、留存或用于其他用途,具体取决于其服务条款;脱敏和截断只降低风险,不等于匿名化。
|
|
60
60
|
|
|
61
|
-
本地还会落盘两类数据,各保留 30 天:决策日志在 `~/.config/dsh-decision-layer/logs/`,会话快照在 `~/.config/dsh-decision-layer/state/`,其中包含脱敏后的命令 /
|
|
61
|
+
本地还会落盘两类数据,各保留 30 天:决策日志在 `~/.config/dsh-decision-layer/logs/`,会话快照在 `~/.config/dsh-decision-layer/state/`,其中包含脱敏后的命令 / 路径;各决策行的任务节选最多 512 字符(过滤记忆 / 系统提醒注入块并遮蔽常见凭据写法),行上可带回合号与步骤号。落盘不额外保存完整前文、执行进展、工具结果或回复原文,只保留节选与决策计数。节选展示在本地会话面板和决策分析页,外发内容以上一节逐项列出为准,可按需自行清理。
|
|
62
62
|
|
|
63
63
|
**会话内容敏感时,推荐改用本地部署的模型**,让裁决数据不出本机。`tev1` 可经 ollama 原生的 `/v1/systemone` 端点直接接入,部署步骤见 [用本地 tev1 配合插件(部署指南)](<docs/zh/deploy-tev1-with-plugin.md>);其他本地模型只要提供兼容 `/v1/systemone` 的端点,同样可以在配置页把服务地址指向本机。
|
|
64
64
|
|
|
@@ -72,4 +72,4 @@ user 消息的文本里可能含有宿主注入的上下文,例如工作区指
|
|
|
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.3 为体验版:介入能力已可用,但实际价值与阈值仍在探索,欢迎反馈决策日志观察到的误判与体感。
|
package/docs/en/DETAIL.md
CHANGED
|
@@ -6,8 +6,8 @@ A structured set of explicitly scoped decisions for the DeepSeek Harness agent l
|
|
|
6
6
|
|
|
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
|
-
- **Task-completion review** — when the user lists multiple delivery conditions, each is judged satisfied / unsatisfied / insufficient evidence, with tool evidence primary and self-reported completion not counting.
|
|
10
|
-
- **Tool narrowing** — at turn start, judges each optional tool's relevance to the task and drops the ones not needed,
|
|
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.
|
|
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
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
- **危险调用门控**:破坏性工具调用执行前,用脱敏摘要请后端裁决;只有高置信「拒绝」才拦截,后端失败、低置信或响应不合法时一律按宿主原路径执行。
|
|
8
8
|
- **产出自检与质量趋势**:回合结束给最终回复打分,低分且模型有把握时可自动补一轮;连续多次高置信低分会在触发按钮上给出告警。
|
|
9
|
-
- **任务完成核对**:用户明确列出多个交付条件时,逐项判定「已满足 / 未满足 /
|
|
10
|
-
-
|
|
9
|
+
- **任务完成核对**:用户明确列出多个交付条件时,逐项判定「已满足 / 未满足 / 证据不足」,条件只取本回合最近一次要求,以本回合工具证据为准(上一回合的成功不算数),回复自述不算证据。
|
|
10
|
+
- **工具收窄**:回合开始时、以及同一回合内出现新的用户要求时,判断各可选工具与任务的相关性并摘掉用不上的;每次重新判断前先释放旧限制,时间提示与后台通知不触发重判。只在宿主已允许的工具集内取交集,绝不扩权。
|
|
11
11
|
- **防循环**:同一调用连续重复超过阈值即拒绝,确定性本地逻辑,不依赖后端。
|
|
12
12
|
- **决策面板与分析页**:展示自动裁决指标与决策日志,并可逐条标注「判对 / 判错 / 说不准」;分析页顶部可直接开关本会话的自动介入。
|
|
13
13
|
|
package/lib/client.js
CHANGED
|
@@ -81,6 +81,7 @@ var zh = {
|
|
|
81
81
|
logNarrow: "\u6536\u7A84",
|
|
82
82
|
logComplete: "\u5B8C\u6210\u6838\u5BF9",
|
|
83
83
|
logTurn: "\u56DE\u5408 {turn}",
|
|
84
|
+
logTurnStep: "\u56DE\u5408 {turn} \xB7 \u7B2C {step} \u6B65",
|
|
84
85
|
logTask: "\u573A\u666F\uFF08\u4EFB\u52A1\u8282\u9009\uFF09\uFF1A",
|
|
85
86
|
logSuggested: "\u6A21\u578B\u5EFA\u8BAE",
|
|
86
87
|
logEvalFailed: "\u8BC4\u4F30\u5931\u8D25",
|
|
@@ -91,8 +92,8 @@ var zh = {
|
|
|
91
92
|
logPathLabel: "\u8DEF\u5F84\uFF1A",
|
|
92
93
|
logSubjectLabel: "\u8BF7\u6C42\uFF1A",
|
|
93
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",
|
|
94
|
-
logCheckHint: "\u6BCF\u56DE\u5408\u7ED3\u675F\u540E\uFF0C\u628A\u8FD9\u8F6E AI \u7684\u6700\u7EC8\u56DE\u590D\u4EA4\u7ED9\u88C1\u51B3\u6A21\u578B\uFF0C\u6309\u300C\u4E25\u91CD\u9057\u6F0F/\u81EA\u76F8\u77DB\u76FE \u2192 \u6709\u5C0F\u9057\u6F0F \u2192 \u5B8C\u6574\u51C6\u786E\u300D\u4E09\u6863\u6253\u4E00\u4E2A 0\u20132 \u7684\u8D28\u91CF\u5206\uFF0C\u7528\u6765\u53D1\u73B0\u9057\u6F0F\u6216\u77DB\u76FE\uFF1B\u81EA\u68C0\u884C\uFF08\u542B\u540E\u7AEF\u5931\u8D25\u884C\uFF09\u4F1A\u5E26\u672C\u56DE\u5408\u7684\u4EFB\u52A1\u8282\u9009\uFF08\u6700\u591A 512 \u5B57\uFF0C\u5DF2\u906E\u853D\u51ED\u636E\uFF09\u3002",
|
|
95
|
-
logNarrowHint: "\u6BCF\u56DE\u5408\u5F00\u59CB\u65F6\uFF0C\
|
|
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\u5BF9\u672C\u56DE\u5408\u9650\u5236\u5DE5\u5177\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",
|
|
96
97
|
actionDeny: "\u5DF2\u62D2\u7EDD",
|
|
97
98
|
actionAsk: "\u8F6C\u4EA4\u5BA1\u6279",
|
|
98
99
|
actionPass: "\u672A\u5E72\u9884",
|
|
@@ -117,7 +118,7 @@ var zh = {
|
|
|
117
118
|
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",
|
|
118
119
|
narrow: "\u5DE5\u5177\u6536\u7A84",
|
|
119
120
|
narrowObserveLabel: "\u4EC5\u89C2\u5BDF\uFF08\u4E0D\u771F\u6B63\u9650\u5236\u5DE5\u5177\uFF09",
|
|
120
|
-
narrowHint: "\u6BCF\u56DE\u5408\u5F00\u59CB\u65F6\uFF0C\u7528\u88C1\u51B3\u6A21\u578B\u5224\u65AD\u5F53\u524D\u53EF\u9009\u5DE5\u5177\u4E0E\
|
|
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\u672C\u56DE\u5408\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\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",
|
|
121
122
|
narrowKeepLabel: "\u59CB\u7EC8\u4FDD\u7559\u7684\u5DE5\u5177\u524D\u7F00",
|
|
122
123
|
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",
|
|
123
124
|
narrowMaxLabel: "\u5019\u9009\u5DE5\u5177\u4E0A\u9650",
|
|
@@ -160,7 +161,7 @@ var zh = {
|
|
|
160
161
|
featureCheck: "\u4EA7\u51FA\u81EA\u68C0",
|
|
161
162
|
featureCheckHint: "\u5173\u95ED\u540E\u4E0D\u518D\u5728\u56DE\u5408\u7ED3\u675F\u65F6\u7ED9\u6700\u7EC8\u56DE\u590D\u6253\u5206\u3002",
|
|
162
163
|
featureNarrow: "\u5DE5\u5177\u6536\u7A84",
|
|
163
|
-
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\u4E0D\u53D7\u5404\u51B3\u7B56\u70B9\u5F00\u5173\u5F71\u54CD\uFF0C\u4F46\u4F1A\u8BDD\u603B\u5F00\u5173\u5173\u95ED\u65F6\u540C\u6837\u6682\u505C\u3002",
|
|
164
|
+
featureNarrowHint: "\u5173\u95ED\u540E\u4E0D\u518D\u5728\u56DE\u5408\u5F00\u59CB\u3001\u6216\u540C\u4E00\u56DE\u5408\u5185\u51FA\u73B0\u65B0\u7684\u7528\u6237\u8981\u6C42\u65F6\u8BC4\u4F30\u5DE5\u5177\u76F8\u5173\u6027\u3002\u9632\u5FAA\u73AF\u4E3A\u786E\u5B9A\u6027\u672C\u5730\u4FDD\u62A4\uFF0C\u4E0D\u53D7\u5404\u51B3\u7B56\u70B9\u5F00\u5173\u5F71\u54CD\uFF0C\u4F46\u4F1A\u8BDD\u603B\u5F00\u5173\u5173\u95ED\u65F6\u540C\u6837\u6682\u505C\u3002",
|
|
164
165
|
featureComplete: "\u4EFB\u52A1\u5B8C\u6210\u6838\u5BF9",
|
|
165
166
|
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",
|
|
166
167
|
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",
|
|
@@ -172,7 +173,7 @@ var zh = {
|
|
|
172
173
|
rateSummary: "\u5DF2\u6807\u6CE8 {rated} \u6761\uFF08\u5224\u5BF9 {good} / \u5224\u9519 {bad} / \u8BF4\u4E0D\u51C6 {unsure}\uFF09",
|
|
173
174
|
annotateFailed: "\u6807\u6CE8\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002",
|
|
174
175
|
completeSteer: "\u6709\u672A\u6EE1\u8DB3\u6761\u4EF6\u65F6\u81EA\u52A8\u8865\u4E00\u8F6E",
|
|
175
|
-
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",
|
|
176
|
+
completeHint: "\u56DE\u5408\u7ED3\u675F\u65F6\uFF0C\u5BF9\u7528\u6237\u5728\u672C\u56DE\u5408\u6700\u8FD1\u4E00\u6B21\u8981\u6C42\u4E2D\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",
|
|
176
177
|
completeResultOk: "\u6761\u4EF6\u9F50\u5907",
|
|
177
178
|
completeResultUnsatisfied: "\u6709\u672A\u6EE1\u8DB3",
|
|
178
179
|
completeConditions: "\u6761\u4EF6",
|
|
@@ -278,6 +279,7 @@ var en = {
|
|
|
278
279
|
logNarrow: "Narrow",
|
|
279
280
|
logComplete: "Completion",
|
|
280
281
|
logTurn: "Turn {turn}",
|
|
282
|
+
logTurnStep: "Turn {turn} \xB7 step {step}",
|
|
281
283
|
logTask: "Context (task excerpt): ",
|
|
282
284
|
logSuggested: "model suggested",
|
|
283
285
|
logEvalFailed: "evaluation failed",
|
|
@@ -288,8 +290,8 @@ var en = {
|
|
|
288
290
|
logPathLabel: "path: ",
|
|
289
291
|
logSubjectLabel: "request: ",
|
|
290
292
|
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.",
|
|
291
|
-
logCheckHint: "After each turn, the AI\u2019s final reply for that turn is sent to the adjudication model, which grades its quality on three levels (severe omission/contradiction \u2192 minor gaps \u2192 complete and accurate) as a 0\u20132 score, to catch omissions or contradictions; every self-check row (including a failed one) carries this turn\u2019s task excerpt (up to 512 characters, credentials masked).",
|
|
292
|
-
logNarrowHint: "At the start of each turn, the current
|
|
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 this turn.",
|
|
293
295
|
actionDeny: "denied",
|
|
294
296
|
actionAsk: "sent to approval",
|
|
295
297
|
actionPass: "no intervention",
|
|
@@ -314,7 +316,7 @@ var en = {
|
|
|
314
316
|
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.",
|
|
315
317
|
narrow: "Tool narrowing",
|
|
316
318
|
narrowObserveLabel: "Observe only (do not restrict tools)",
|
|
317
|
-
narrowHint: "At the start of each turn, 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 for this turn, intersected with the host-allowed set and never expanded. 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.",
|
|
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 for this turn, intersected with the host-allowed set and never expanded. 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.",
|
|
318
320
|
narrowKeepLabel: "Always-kept tool prefixes",
|
|
319
321
|
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.",
|
|
320
322
|
narrowMaxLabel: "Candidate tool cap",
|
|
@@ -357,7 +359,7 @@ var en = {
|
|
|
357
359
|
featureCheck: "Output self-check",
|
|
358
360
|
featureCheckHint: "When off, the final reply is no longer scored at the end of a turn.",
|
|
359
361
|
featureNarrow: "Tool narrowing",
|
|
360
|
-
featureNarrowHint: "When off, tool relevance is no longer evaluated at the start of a turn. The loop guard is a deterministic local safeguard outside these switches, but the session master switch pauses it too.",
|
|
362
|
+
featureNarrowHint: "When off, tool relevance is no longer evaluated at the start of a turn or when a new user request arrives inside it. The loop guard is a deterministic local safeguard outside these switches, but the session master switch pauses it too.",
|
|
361
363
|
featureComplete: "Task completion check",
|
|
362
364
|
featureCompleteHint: "When off, the explicit deliverable conditions the user listed are no longer checked at turn end.",
|
|
363
365
|
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.",
|
|
@@ -369,7 +371,7 @@ var en = {
|
|
|
369
371
|
rateSummary: "Rated {rated} ({good} correct / {bad} wrong / {unsure} unsure)",
|
|
370
372
|
annotateFailed: "Could not save the rating. Please retry.",
|
|
371
373
|
completeSteer: "Auto re-prompt on an unmet condition",
|
|
372
|
-
completeHint: "At turn end, each explicit deliverable condition the user
|
|
374
|
+
completeHint: "At turn end, each explicit deliverable condition in the latest user request of the turn (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.",
|
|
373
375
|
completeResultOk: "conditions met",
|
|
374
376
|
completeResultUnsatisfied: "unmet found",
|
|
375
377
|
completeConditions: "conditions",
|
|
@@ -477,10 +479,11 @@ function gateArgs({ t }, entry) {
|
|
|
477
479
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "decision-log-cmd", title: `${label}${value}`, children: preview });
|
|
478
480
|
}
|
|
479
481
|
function turnTag(t, entry) {
|
|
480
|
-
|
|
482
|
+
if (typeof entry.turn !== "number") return null;
|
|
483
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
481
484
|
" \xB7 ",
|
|
482
|
-
t("logTurn", { turn: entry.turn })
|
|
483
|
-
] })
|
|
485
|
+
Number.isSafeInteger(entry.step) && entry.step >= 0 ? t("logTurnStep", { turn: entry.turn, step: entry.step }) : t("logTurn", { turn: entry.turn })
|
|
486
|
+
] });
|
|
484
487
|
}
|
|
485
488
|
function renderGate({ t }, entry) {
|
|
486
489
|
const tool = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "decision-log-tool", children: entry.tool ?? "\u2014" });
|
|
@@ -667,7 +670,10 @@ function renderNarrow({ t }, entry) {
|
|
|
667
670
|
] });
|
|
668
671
|
}
|
|
669
672
|
function renderComplete({ t }, entry) {
|
|
670
|
-
const label = /* @__PURE__ */ (0, import_jsx_runtime.
|
|
673
|
+
const label = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
674
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("logComplete") }),
|
|
675
|
+
turnTag(t, entry)
|
|
676
|
+
] });
|
|
671
677
|
if (entry.outcome === "error") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
672
678
|
label,
|
|
673
679
|
" ",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentLike, DecisionSnapshot } from './types.js';
|
|
2
|
+
type Position = {
|
|
3
|
+
turn?: number;
|
|
4
|
+
messages?: unknown;
|
|
5
|
+
};
|
|
6
|
+
export declare function decisionText(value: unknown, maxBytes?: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* One agent's turn boundary, captured before inbox messages enter derived history.
|
|
9
|
+
* `ignored` drops messages the plugin produced itself (a steer carries `kind:user`
|
|
10
|
+
* for host compatibility but is our prompt, not the user's request).
|
|
11
|
+
*/
|
|
12
|
+
export declare function createDecisionContext({ ignored }?: {
|
|
13
|
+
ignored?: (id: string) => boolean;
|
|
14
|
+
}): {
|
|
15
|
+
enter(agent: AgentLike | undefined, position: Position): {
|
|
16
|
+
changed: boolean;
|
|
17
|
+
snapshot?: DecisionSnapshot;
|
|
18
|
+
};
|
|
19
|
+
snapshot(agent: AgentLike | undefined): DecisionSnapshot;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { redactCredentials, stripInjected } from "./excerpt.js";
|
|
2
|
+
import { summarizeExecution } from "./danger-gate.js";
|
|
3
|
+
import { isObject } from "./types.js";
|
|
4
|
+
function clip(text, maxBytes) {
|
|
5
|
+
let size = 0;
|
|
6
|
+
let result = "";
|
|
7
|
+
for (const char of text) {
|
|
8
|
+
size += Buffer.byteLength(char);
|
|
9
|
+
if (size > maxBytes) break;
|
|
10
|
+
result += char;
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
function decisionText(value, maxBytes = 1536) {
|
|
15
|
+
if (typeof value !== "string" || maxBytes <= 0) return "";
|
|
16
|
+
const clean = redactCredentials(stripInjected(value.slice(0, 32 * 1024))).trim();
|
|
17
|
+
return clip(clean, maxBytes);
|
|
18
|
+
}
|
|
19
|
+
const OPAQUE = /[A-Za-z0-9_+-]{24,}/g;
|
|
20
|
+
const resultExcerpt = (value, maxBytes) => clip(decisionText(value, 32 * 1024).replace(OPAQUE, "[redacted]"), maxBytes);
|
|
21
|
+
const messagesOf = (value) => Array.isArray(value) ? value.filter(isObject) : [];
|
|
22
|
+
const textOf = (message) => Array.isArray(message.content) ? message.content.filter(isObject).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n") : "";
|
|
23
|
+
const human = (message) => message.role === "user" && (!message.source || isObject(message.source) && message.source.kind === "user");
|
|
24
|
+
function historyOf(agent) {
|
|
25
|
+
try {
|
|
26
|
+
const derived = agent?.session?.deriveMessages?.();
|
|
27
|
+
return Array.isArray(derived) ? messagesOf(derived) : void 0;
|
|
28
|
+
} catch {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function backgroundOf(messages, isHuman) {
|
|
33
|
+
const lastUser = messages.findLastIndex(isHuman);
|
|
34
|
+
const lastAssistant = messages.findLastIndex((message) => message.role === "assistant");
|
|
35
|
+
return [lastUser, lastAssistant].filter((index) => index >= 0).sort((a, b) => a - b).map((index) => {
|
|
36
|
+
const message = messages[index];
|
|
37
|
+
if (!message) return void 0;
|
|
38
|
+
const role = isHuman(message) ? "user" : "assistant";
|
|
39
|
+
return { role, text: decisionText(textOf(message), 512) };
|
|
40
|
+
}).filter((item) => Boolean(item?.text));
|
|
41
|
+
}
|
|
42
|
+
function evidenceOf(messages) {
|
|
43
|
+
const calls = /* @__PURE__ */ new Map();
|
|
44
|
+
const evidence = [];
|
|
45
|
+
for (const message of messages) {
|
|
46
|
+
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
47
|
+
for (const block of message.content.filter(isObject)) {
|
|
48
|
+
if (block.type !== "tool-call" || typeof block.id !== "string" || typeof block.name !== "string") continue;
|
|
49
|
+
let args = block.arguments;
|
|
50
|
+
if (typeof args === "string") {
|
|
51
|
+
try {
|
|
52
|
+
args = JSON.parse(args);
|
|
53
|
+
} catch {
|
|
54
|
+
args = void 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
calls.set(block.id, { name: block.name, arguments: args });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (message.role !== "tool") continue;
|
|
61
|
+
const source = isObject(message.source) ? message.source : {};
|
|
62
|
+
const id = typeof message.toolCallId === "string" ? message.toolCallId : source.callId;
|
|
63
|
+
const call = typeof id === "string" ? calls.get(id) : void 0;
|
|
64
|
+
const name = call?.name ?? (typeof message.name === "string" ? message.name : typeof message.toolName === "string" ? message.toolName : "");
|
|
65
|
+
if (!name) continue;
|
|
66
|
+
const item = { tool: decisionText(name, 96), outcome: message.isError === true ? "error" : "ok" };
|
|
67
|
+
if (call) {
|
|
68
|
+
const summary = summarizeExecution(call);
|
|
69
|
+
if (summary.command) item.command = decisionText(summary.command, 160);
|
|
70
|
+
if (summary.path) item.path = decisionText(summary.path, 160);
|
|
71
|
+
}
|
|
72
|
+
if (["bash", "pwsh", "shell", "exec", "terminal", "run_command"].includes(name)) {
|
|
73
|
+
const result = resultExcerpt(textOf(message), 256);
|
|
74
|
+
if (result) item.result = result;
|
|
75
|
+
}
|
|
76
|
+
evidence.push(item);
|
|
77
|
+
}
|
|
78
|
+
const bounded = evidence.length > 12 ? [...evidence.slice(0, 4), ...evidence.slice(-8)] : [...evidence];
|
|
79
|
+
while (Buffer.byteLength(JSON.stringify(bounded)) > 4096) bounded.shift();
|
|
80
|
+
return bounded;
|
|
81
|
+
}
|
|
82
|
+
const REPLACEMENTS = /* @__PURE__ */ new Set(["compact-checkpoint", "dsh-rewind"]);
|
|
83
|
+
function afterLastReplacement(messages) {
|
|
84
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
85
|
+
const message = messages[i];
|
|
86
|
+
if (!message) continue;
|
|
87
|
+
const source = isObject(message.source) ? message.source : void 0;
|
|
88
|
+
if (source && typeof source.kind === "string" && REPLACEMENTS.has(source.kind)) return messages.slice(i + 1);
|
|
89
|
+
}
|
|
90
|
+
return messages;
|
|
91
|
+
}
|
|
92
|
+
function boundaryHolds(history, boundary) {
|
|
93
|
+
if (boundary.start <= 0) return true;
|
|
94
|
+
const previous = history[boundary.start - 1];
|
|
95
|
+
if (!previous) return false;
|
|
96
|
+
return previous === boundary.anchor || boundary.anchorId !== void 0 && previous.id === boundary.anchorId;
|
|
97
|
+
}
|
|
98
|
+
function snapshotOf(history, boundary, isHuman) {
|
|
99
|
+
const messages = history ?? [];
|
|
100
|
+
const lastHuman = messages.findLastIndex(isHuman);
|
|
101
|
+
const window = boundary === void 0 ? messages.slice(lastHuman >= 0 ? lastHuman : messages.length) : boundary.broken ? [] : boundaryHolds(messages, boundary) ? messages.slice(boundary.start) : [];
|
|
102
|
+
const current = afterLastReplacement(window);
|
|
103
|
+
const requests = boundary?.requests ?? current.filter(isHuman).map((message) => decisionText(textOf(message), 8192));
|
|
104
|
+
const request = decisionText(requests.at(-1) ?? "", 1536);
|
|
105
|
+
const evidence = evidenceOf(current);
|
|
106
|
+
const context = {
|
|
107
|
+
request,
|
|
108
|
+
// Earlier real requests in this turn stay visible as background: with only the
|
|
109
|
+
// last one, a short follow-up ("继续") would hide the main task. Pre-turn
|
|
110
|
+
// context is kept too, but the size cap below drops it first.
|
|
111
|
+
background: [
|
|
112
|
+
...boundary?.background ?? backgroundOf(messages.slice(0, Math.max(lastHuman, 0)), isHuman),
|
|
113
|
+
...requests.slice(0, -1).slice(-2).map((text) => ({ role: "user", text: decisionText(text, 1024) }))
|
|
114
|
+
].slice(-4),
|
|
115
|
+
progress: evidence.slice(-3)
|
|
116
|
+
};
|
|
117
|
+
while (Buffer.byteLength(JSON.stringify(context.progress)) > 1024 && context.progress.length > 1) context.progress.shift();
|
|
118
|
+
while (Buffer.byteLength(JSON.stringify(context)) > 4096 && (context.background.length || context.progress.length > 1)) {
|
|
119
|
+
if (context.background.length) context.background.shift();
|
|
120
|
+
else context.progress.shift();
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
context,
|
|
124
|
+
evidence,
|
|
125
|
+
// Completion conditions come from the latest real request only. Joining a
|
|
126
|
+
// turn's requests would resurrect work the user just cancelled ("drop the
|
|
127
|
+
// earlier list, only review"); when the latest request has no explicit
|
|
128
|
+
// conditions the check skips rather than infer from an older revision.
|
|
129
|
+
conditionsText: requests.at(-1) ?? ""
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function createDecisionContext({ ignored } = {}) {
|
|
133
|
+
const boundaries = /* @__PURE__ */ new WeakMap();
|
|
134
|
+
const isHuman = (message) => human(message) && !(typeof message.id === "string" && ignored?.(message.id) === true);
|
|
135
|
+
const enter = (agent, position) => {
|
|
136
|
+
const known = agent ? boundaries.get(agent) : void 0;
|
|
137
|
+
const first = !known || known.turn !== position.turn;
|
|
138
|
+
const inputs = messagesOf(position.messages).filter(isHuman).map((message) => decisionText(textOf(message), 8192)).filter(Boolean);
|
|
139
|
+
if (!first && inputs.length === 0) return { changed: false };
|
|
140
|
+
const history = historyOf(agent);
|
|
141
|
+
let boundary = known;
|
|
142
|
+
const anchor = history?.at(-1);
|
|
143
|
+
if (first) {
|
|
144
|
+
boundary = {
|
|
145
|
+
turn: position.turn,
|
|
146
|
+
start: history?.length ?? 0,
|
|
147
|
+
anchor,
|
|
148
|
+
broken: history === void 0,
|
|
149
|
+
...isObject(anchor) && typeof anchor.id === "string" ? { anchorId: anchor.id } : {},
|
|
150
|
+
requests: [],
|
|
151
|
+
background: history ? backgroundOf(history, isHuman) : []
|
|
152
|
+
};
|
|
153
|
+
if (agent) boundaries.set(agent, boundary);
|
|
154
|
+
} else if (boundary?.broken) {
|
|
155
|
+
boundary.broken = history === void 0;
|
|
156
|
+
boundary.start = history?.length ?? 0;
|
|
157
|
+
boundary.anchor = anchor;
|
|
158
|
+
if (isObject(anchor) && typeof anchor.id === "string") boundary.anchorId = anchor.id;
|
|
159
|
+
else delete boundary.anchorId;
|
|
160
|
+
boundary.background = history ? backgroundOf(history, isHuman) : [];
|
|
161
|
+
}
|
|
162
|
+
const active = boundary;
|
|
163
|
+
active.requests.push(...inputs);
|
|
164
|
+
active.requests = active.requests.slice(-8);
|
|
165
|
+
return { changed: true, snapshot: snapshotOf(history, active, isHuman) };
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
enter(agent, position) {
|
|
169
|
+
try {
|
|
170
|
+
return enter(agent, position);
|
|
171
|
+
} catch {
|
|
172
|
+
return { changed: false };
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
snapshot(agent) {
|
|
176
|
+
try {
|
|
177
|
+
return snapshotOf(historyOf(agent), agent ? boundaries.get(agent) : void 0, isHuman);
|
|
178
|
+
} catch {
|
|
179
|
+
return { context: { request: "", background: [], progress: [] }, conditionsText: "", evidence: [] };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export {
|
|
185
|
+
createDecisionContext,
|
|
186
|
+
decisionText
|
|
187
|
+
};
|
package/lib/host/excerpt.d.ts
CHANGED
|
@@ -22,4 +22,5 @@ export declare const TASK_EXCERPT_LIMIT = 512;
|
|
|
22
22
|
* a secret to the log.
|
|
23
23
|
*/
|
|
24
24
|
export declare function redactCredentials(input: string): string;
|
|
25
|
+
export declare function stripInjected(input: string): string;
|
|
25
26
|
export declare function taskExcerpt(state: unknown, limit?: number): string | undefined;
|
package/lib/host/excerpt.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const MAX_INPUT = 8 * 1024;
|
|
2
2
|
const TASK_EXCERPT_LIMIT = 512;
|
|
3
3
|
const TOKEN_KEYS = String.raw`token|api[ _-]*key`;
|
|
4
|
-
const PHRASE_KEYS = String.raw`password|
|
|
4
|
+
const PHRASE_KEYS = String.raw`password|passw(?:or)?d|passphrase|secret[ _-]?key|access[ _-]?key|private[ _-]?key|secret`;
|
|
5
5
|
const CN_KEYS = String.raw`密码|口令|密码短语|密钥|令牌|凭证|私钥`;
|
|
6
6
|
const COMBINED_KEYS = String.raw`${PHRASE_KEYS}|${TOKEN_KEYS}`;
|
|
7
|
+
const ENV_PHRASE_KEYS = String.raw`PASSW(?:OR)?D|SECRET|ACCESS[_-]?KEY|PRIVATE[_-]?KEY`;
|
|
8
|
+
const ENV_TOKEN_KEYS = String.raw`TOKEN|API[_-]?KEY`;
|
|
7
9
|
const SEPARATOR = String.raw`[:=:=]`;
|
|
8
10
|
const VALUE = String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;},;、]+)`;
|
|
9
11
|
const CLAUSE = String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\r\n,;,;、]*)`;
|
|
@@ -30,6 +32,13 @@ const RULE = {
|
|
|
30
32
|
asciiCopula: new RegExp(String.raw`(\b(?:${COMBINED_KEYS})\b\s*(?:${COPULA})\s*)${VALUE_LIKE}${VALUE}`, "gi"),
|
|
31
33
|
cnCopula: new RegExp(String.raw`((?:${CN_KEYS})\s*(?:是|为)\s*)${VALUE_LIKE}${VALUE}`, "g"),
|
|
32
34
|
flag: new RegExp(String.raw`(^|\s)(--(?:${COMBINED_KEYS})(?:\s+|=))${VALUE}`, "gi"),
|
|
35
|
+
// Env-style keys are often a suffix of a longer identifier (`DB_PASSWORD`,
|
|
36
|
+
// `AWS_SECRET_ACCESS_KEY`), where `\b` cannot match across `_`. JSON and quoted
|
|
37
|
+
// .env output place a closing quote between key and separator, and a quoted
|
|
38
|
+
// passphrase may contain spaces, so the key allows a quote and the value is the
|
|
39
|
+
// whole clause (over-redaction is the accepted direction).
|
|
40
|
+
envPhrase: new RegExp(String.raw`([A-Za-z0-9_]*(?:${ENV_PHRASE_KEYS})[A-Za-z0-9_]*["']?\s*${SEPARATOR}\s*)${CLAUSE}`, "gi"),
|
|
41
|
+
envToken: new RegExp(String.raw`([A-Za-z0-9_]*(?:${ENV_TOKEN_KEYS})[A-Za-z0-9_]*["']?\s*${SEPARATOR}\s*)${VALUE}`, "gi"),
|
|
33
42
|
// Legacy space form (`token abc`), but never swallow a copula: the rule above
|
|
34
43
|
// masks the value that follows it.
|
|
35
44
|
loose: new RegExp(String.raw`(\b(?:${COMBINED_KEYS})\b\s+)(?!(?:${COPULA})(?:\s|$)|${SEPARATOR})${VALUE}`, "gi")
|
|
@@ -45,18 +54,23 @@ function redactCredentials(input) {
|
|
|
45
54
|
text = text.replace(RULE.asciiCopula, "$1[redacted]");
|
|
46
55
|
text = text.replace(RULE.cnCopula, "$1[redacted]");
|
|
47
56
|
text = text.replace(RULE.flag, "$1$2[redacted]");
|
|
57
|
+
text = text.replace(RULE.envPhrase, "$1[redacted]");
|
|
58
|
+
text = text.replace(RULE.envToken, "$1[redacted]");
|
|
48
59
|
text = text.replace(RULE.loose, "$1[redacted]");
|
|
49
60
|
for (const [pattern, replacement] of TOKEN_SHAPES) text = text.replace(pattern, replacement);
|
|
50
61
|
return text;
|
|
51
62
|
}
|
|
63
|
+
function stripInjected(input) {
|
|
64
|
+
return input.replace(/<(openviking-context|system-reminder)\b[^>]*>[\s\S]*?<\/\1>/gi, " ").replace(/<(openviking-context|system-reminder)\b[\s\S]*$/i, " ");
|
|
65
|
+
}
|
|
52
66
|
function taskExcerpt(state, limit = TASK_EXCERPT_LIMIT) {
|
|
53
67
|
if (typeof state !== "string") return void 0;
|
|
54
|
-
const
|
|
55
|
-
const text = redactCredentials(stripped).replace(/\s+/g, " ").trim();
|
|
68
|
+
const text = redactCredentials(stripInjected(state.slice(0, MAX_INPUT))).replace(/\s+/g, " ").trim();
|
|
56
69
|
return text ? text.slice(0, limit) : void 0;
|
|
57
70
|
}
|
|
58
71
|
export {
|
|
59
72
|
TASK_EXCERPT_LIMIT,
|
|
60
73
|
redactCredentials,
|
|
74
|
+
stripInjected,
|
|
61
75
|
taskExcerpt
|
|
62
76
|
};
|
package/lib/host/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createTaskCompletion } from "./task-completion.js";
|
|
|
11
11
|
import { createLoopGuard } from "./loop-guard.js";
|
|
12
12
|
import { createLogStore } from "./log-store.js";
|
|
13
13
|
import { createStateStore } from "./state-store.js";
|
|
14
|
+
import { createDecisionContext } from "./decision-context.js";
|
|
14
15
|
import { analyzeSessionLogs, reconstructSessionSeed, listSessionDecisions } from "./log-analyze.js";
|
|
15
16
|
import { isObject } from "./types.js";
|
|
16
17
|
const name = "dsh-decision-layer";
|
|
@@ -35,50 +36,6 @@ function lastAssistantText(agent) {
|
|
|
35
36
|
return "";
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
function stepTaskText(messages) {
|
|
39
|
-
try {
|
|
40
|
-
if (!Array.isArray(messages)) return "";
|
|
41
|
-
return messages.map(asMessage).filter((message) => message?.role === "user" && Array.isArray(message.content)).flatMap((message) => message.content.filter((block) => isObject(block) && block.type === "text" && typeof block.text === "string").map((block) => block.text)).join("\n").slice(0, 8 * 1024);
|
|
42
|
-
} catch {
|
|
43
|
-
return "";
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function turnUserText(agent) {
|
|
47
|
-
try {
|
|
48
|
-
const messages = derivedMessages(agent);
|
|
49
|
-
if (!messages) return "";
|
|
50
|
-
const texts = [];
|
|
51
|
-
let collecting = false;
|
|
52
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
53
|
-
const message = asMessage(messages[i]);
|
|
54
|
-
if (message?.role === "user" && Array.isArray(message.content)) {
|
|
55
|
-
collecting = true;
|
|
56
|
-
texts.unshift(message.content.filter((block) => isObject(block) && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n"));
|
|
57
|
-
} else if (collecting) break;
|
|
58
|
-
}
|
|
59
|
-
return texts.join("\n").slice(0, 8 * 1024);
|
|
60
|
-
} catch {
|
|
61
|
-
return "";
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function turnToolEvidence(agent) {
|
|
65
|
-
try {
|
|
66
|
-
const messages = derivedMessages(agent);
|
|
67
|
-
if (!messages) return [];
|
|
68
|
-
const evidence = [];
|
|
69
|
-
for (const item of messages) {
|
|
70
|
-
const message = asMessage(item);
|
|
71
|
-
if (message?.role !== "tool" || !Array.isArray(message.content)) continue;
|
|
72
|
-
const tool = typeof message.name === "string" ? message.name : typeof message.toolName === "string" ? message.toolName : "";
|
|
73
|
-
if (!tool) continue;
|
|
74
|
-
evidence.push({ tool: tool.slice(0, 128), outcome: message.isError === true ? "error" : "ok" });
|
|
75
|
-
if (evidence.length >= 40) break;
|
|
76
|
-
}
|
|
77
|
-
return evidence;
|
|
78
|
-
} catch {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
39
|
function agentTools(ctx, agent) {
|
|
83
40
|
try {
|
|
84
41
|
const schemas = typeof ctx.tools?.schemas === "function" ? ctx.tools.schemas(agent) : void 0;
|
|
@@ -108,6 +65,20 @@ async function apply(ctx, options = {}) {
|
|
|
108
65
|
if (typeof ctx.effect === "function") ctx.effect(() => () => {
|
|
109
66
|
void stateStore.dispose();
|
|
110
67
|
});
|
|
68
|
+
const steeredIds = /* @__PURE__ */ new Map();
|
|
69
|
+
const rememberSteer = (message) => {
|
|
70
|
+
const id = isObject(message) && typeof message.id === "string" ? message.id : void 0;
|
|
71
|
+
if (!id) return;
|
|
72
|
+
steeredIds.set(id, true);
|
|
73
|
+
if (steeredIds.size > 256) steeredIds.delete(steeredIds.keys().next().value);
|
|
74
|
+
};
|
|
75
|
+
const steerAgent = (agent, text) => {
|
|
76
|
+
if (typeof agent.steer !== "function") throw new Error("Agent cannot be steered");
|
|
77
|
+
const message = createUserMessage({ content: [{ type: "text", text }], source: { kind: "user" } });
|
|
78
|
+
rememberSteer(message);
|
|
79
|
+
return agent.steer(message);
|
|
80
|
+
};
|
|
81
|
+
const decisionContext = createDecisionContext({ ignored: (id) => steeredIds.has(id) });
|
|
111
82
|
const gate = createDangerGate({
|
|
112
83
|
evaluate: (input, request) => backend.evaluate(input, request),
|
|
113
84
|
sessions,
|
|
@@ -116,8 +87,10 @@ async function apply(ctx, options = {}) {
|
|
|
116
87
|
// is at least this confident. Read per call so an edit takes effect at once.
|
|
117
88
|
minConfidence: async () => (await resolveConfig({ path })).gateSettings?.minConfidence,
|
|
118
89
|
// Read lazily (only when a dangerous call is recorded) so ordinary tool calls
|
|
119
|
-
// never pay for deriveMessages().
|
|
120
|
-
|
|
90
|
+
// never pay for deriveMessages(). The gate itself still sends only the call
|
|
91
|
+
// summary; this excerpt is local-log context, from the same snapshot the other
|
|
92
|
+
// decision points use.
|
|
93
|
+
taskText: (agent) => decisionContext.snapshot(agent).context.request
|
|
121
94
|
});
|
|
122
95
|
const loopGuard = createLoopGuard({ sessions });
|
|
123
96
|
const featureOn = async (key) => {
|
|
@@ -152,19 +125,17 @@ async function apply(ctx, options = {}) {
|
|
|
152
125
|
evaluate: (input, request) => backend.evaluate(input, request),
|
|
153
126
|
sessions,
|
|
154
127
|
settings: async () => (await resolveConfig({ path })).checkSettings,
|
|
155
|
-
steer:
|
|
156
|
-
if (typeof agent.steer !== "function") throw new Error("Agent cannot be steered");
|
|
157
|
-
return agent.steer(createUserMessage({ content: [{ type: "text", text }], source: { kind: "user" } }));
|
|
158
|
-
}
|
|
128
|
+
steer: steerAgent
|
|
159
129
|
});
|
|
160
130
|
const installCheck = () => ctx.on("agent/turn-stopping", async (payload) => {
|
|
161
131
|
if (!await featureOn("check")) return;
|
|
132
|
+
const snapshot = decisionContext.snapshot(payload.agent);
|
|
162
133
|
return selfCheck.review({
|
|
163
134
|
agent: payload.agent,
|
|
164
135
|
turn: payload.turn,
|
|
165
136
|
signal: payload.signal,
|
|
166
137
|
output: lastAssistantText(payload.agent),
|
|
167
|
-
|
|
138
|
+
context: snapshot.context
|
|
168
139
|
});
|
|
169
140
|
});
|
|
170
141
|
if (typeof ctx.effect === "function") ctx.effect(installCheck);
|
|
@@ -173,20 +144,21 @@ async function apply(ctx, options = {}) {
|
|
|
173
144
|
evaluate: (input, request) => backend.evaluate(input, request),
|
|
174
145
|
sessions,
|
|
175
146
|
settings: async () => (await resolveConfig({ path })).completeSettings,
|
|
176
|
-
steer:
|
|
177
|
-
if (typeof agent.steer !== "function") throw new Error("Agent cannot be steered");
|
|
178
|
-
return agent.steer(createUserMessage({ content: [{ type: "text", text }], source: { kind: "user" } }));
|
|
179
|
-
}
|
|
147
|
+
steer: steerAgent
|
|
180
148
|
});
|
|
181
149
|
const installCompletion = () => ctx.on("agent/turn-stopping", async (payload) => {
|
|
182
150
|
if (!await featureOn("complete")) return;
|
|
151
|
+
const snapshot = decisionContext.snapshot(payload.agent);
|
|
183
152
|
return taskCompletion.review({
|
|
184
153
|
agent: payload.agent,
|
|
185
154
|
turn: payload.turn,
|
|
186
155
|
signal: payload.signal,
|
|
187
156
|
output: lastAssistantText(payload.agent),
|
|
188
|
-
|
|
189
|
-
|
|
157
|
+
// Conditions come only from this turn's real user text; evidence only from
|
|
158
|
+
// this turn's window, so an earlier task's successes cannot vouch for it.
|
|
159
|
+
userText: snapshot.conditionsText,
|
|
160
|
+
evidence: snapshot.evidence,
|
|
161
|
+
context: snapshot.context
|
|
190
162
|
});
|
|
191
163
|
});
|
|
192
164
|
if (typeof ctx.effect === "function") ctx.effect(installCompletion);
|
|
@@ -197,46 +169,71 @@ async function apply(ctx, options = {}) {
|
|
|
197
169
|
settings: async () => (await resolveConfig({ path })).narrowSettings
|
|
198
170
|
});
|
|
199
171
|
const narrowedTurns = /* @__PURE__ */ new Map();
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
172
|
+
const releaseNarrowing = (id) => {
|
|
173
|
+
const entry = narrowedTurns.get(id);
|
|
174
|
+
if (!entry) return;
|
|
175
|
+
try {
|
|
176
|
+
entry.dispose?.();
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
narrowedTurns.delete(id);
|
|
180
|
+
};
|
|
181
|
+
const installNarrowingHook = () => {
|
|
182
|
+
const disposeHook = ctx.on("agent/pre-step", async (payload, next) => {
|
|
183
|
+
const agent = payload?.agent;
|
|
184
|
+
const id = agent?.session?.id;
|
|
185
|
+
const turn = payload?.turn;
|
|
186
|
+
if (typeof id === "string") await sessions.ensureLoaded(id);
|
|
187
|
+
if (typeof id === "string" && Number.isInteger(turn)) {
|
|
188
|
+
const entered = decisionContext.enter(agent, { turn, messages: payload.messages });
|
|
189
|
+
const previous = narrowedTurns.get(id);
|
|
190
|
+
const turnChanged = !previous || previous.turn !== turn;
|
|
191
|
+
if (turnChanged || entered.changed) {
|
|
192
|
+
releaseNarrowing(id);
|
|
193
|
+
if (await featureOn("narrow")) {
|
|
194
|
+
const entry = { turn, dispose: void 0 };
|
|
195
|
+
narrowedTurns.set(id, entry);
|
|
196
|
+
if (narrowedTurns.size > 1024) {
|
|
197
|
+
const oldest = narrowedTurns.keys().next().value;
|
|
198
|
+
if (oldest !== id) releaseNarrowing(oldest);
|
|
218
199
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
200
|
+
try {
|
|
201
|
+
const { context } = entered.snapshot ?? decisionContext.snapshot(agent);
|
|
202
|
+
const result = await narrowing.review({
|
|
203
|
+
agent,
|
|
204
|
+
turn,
|
|
205
|
+
step: payload.step,
|
|
206
|
+
signal: payload.signal,
|
|
207
|
+
state: { request: context.request, background: context.background, progress: context.progress },
|
|
208
|
+
taskText: context.request,
|
|
209
|
+
tools: agentTools(ctx, agent)
|
|
210
|
+
});
|
|
211
|
+
if (result.mode === "enforce" && result.applied && typeof agent?.ctx?.tools?.restrict === "function") {
|
|
212
|
+
const restriction = agent.ctx.tools.restrict({ allow: result.keep });
|
|
213
|
+
if (narrowedTurns.get(id) === entry) entry.dispose = restriction;
|
|
214
|
+
else restriction?.();
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
230
217
|
}
|
|
231
|
-
} catch {
|
|
232
218
|
}
|
|
233
219
|
}
|
|
234
220
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
221
|
+
return next();
|
|
222
|
+
});
|
|
223
|
+
return () => {
|
|
224
|
+
disposeHook?.();
|
|
225
|
+
for (const key of [...narrowedTurns.keys()]) releaseNarrowing(key);
|
|
226
|
+
};
|
|
227
|
+
};
|
|
238
228
|
if (typeof ctx.effect === "function") ctx.effect(installNarrowingHook);
|
|
239
229
|
else installNarrowingHook();
|
|
230
|
+
const installNarrowingRelease = () => ctx.on("session/event", (subject, event) => {
|
|
231
|
+
if (!isObject(event) || event.type !== "turn/end") return;
|
|
232
|
+
const id = isObject(subject) && typeof subject.id === "string" ? subject.id : void 0;
|
|
233
|
+
if (id !== void 0) releaseNarrowing(id);
|
|
234
|
+
});
|
|
235
|
+
if (typeof ctx.effect === "function") ctx.effect(installNarrowingRelease);
|
|
236
|
+
else installNarrowingRelease();
|
|
240
237
|
}
|
|
241
238
|
const rawSkill = await readFile(new URL("../../skills/decision-layer/SKILL.md", import.meta.url), "utf8");
|
|
242
239
|
const content = rawSkill.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
|
package/lib/host/narrowing.d.ts
CHANGED
|
@@ -19,8 +19,11 @@ export declare function relevanceQuestions(state: unknown, tools: readonly ToolC
|
|
|
19
19
|
export type NarrowTurn = {
|
|
20
20
|
agent?: AgentLike;
|
|
21
21
|
turn?: number;
|
|
22
|
+
step?: number;
|
|
22
23
|
signal?: AbortSignal;
|
|
23
24
|
state?: unknown;
|
|
25
|
+
/** Raw request text for the log row; `state` may now be a structured object. */
|
|
26
|
+
taskText?: unknown;
|
|
24
27
|
tools?: unknown;
|
|
25
28
|
};
|
|
26
29
|
export type NarrowResult = {
|
package/lib/host/narrowing.js
CHANGED
|
@@ -106,9 +106,10 @@ function createNarrowing({
|
|
|
106
106
|
}
|
|
107
107
|
if (typeof sessions.log !== "function") return;
|
|
108
108
|
try {
|
|
109
|
-
const task = taskExcerpt(turn?.state);
|
|
109
|
+
const task = taskExcerpt(turn?.taskText ?? turn?.state);
|
|
110
110
|
const context = task ? { task } : {};
|
|
111
111
|
if (Number.isSafeInteger(turn?.turn) && turn?.turn >= 0) context.turn = turn?.turn;
|
|
112
|
+
if (Number.isSafeInteger(turn?.step) && turn?.step >= 0) context.step = turn?.step;
|
|
112
113
|
sessions.log(id, { kind: "narrow", outcome, ...context, ...detail });
|
|
113
114
|
} catch {
|
|
114
115
|
}
|
|
@@ -146,7 +147,7 @@ function createNarrowing({
|
|
|
146
147
|
const keptNames = names.filter(alwaysKeep);
|
|
147
148
|
const optional = tools.filter((tool) => !alwaysKeep(tool.name));
|
|
148
149
|
if (optional.length < minCandidates) return { ...skip, keep: names };
|
|
149
|
-
const hasState = typeof turn?.state === "string" ? turn.state.trim().length > 0 : isObject(turn?.state) ? Object.keys(turn.state).length > 0 : false;
|
|
150
|
+
const hasState = typeof turn?.taskText === "string" ? turn.taskText.trim().length > 0 : typeof turn?.state === "string" ? turn.state.trim().length > 0 : isObject(turn?.state) ? Object.keys(turn.state).length > 0 : false;
|
|
150
151
|
if (!hasState) return { ...skip, keep: names };
|
|
151
152
|
const id = turn?.agent?.session?.id;
|
|
152
153
|
if (sessions && typeof id === "string") {
|
package/lib/host/self-check.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AgentLike, CheckSettings, EvaluateInput, Evaluator, SessionsFace } from './types.js';
|
|
1
|
+
import type { AgentLike, CheckSettings, DecisionContext, EvaluateInput, Evaluator, SessionsFace } from './types.js';
|
|
2
2
|
export declare const DEFAULT_RUBRIC: readonly string[];
|
|
3
|
-
export declare function scoreQuestion(output: unknown, rubric?: readonly string[]): EvaluateInput;
|
|
3
|
+
export declare function scoreQuestion(output: unknown, rubric?: readonly string[], context?: DecisionContext): EvaluateInput;
|
|
4
4
|
export declare const DEFAULT_MIN_CONFIDENCE = 0.4;
|
|
5
5
|
export type CheckTurn = {
|
|
6
6
|
agent?: AgentLike;
|
|
@@ -8,6 +8,7 @@ export type CheckTurn = {
|
|
|
8
8
|
signal?: AbortSignal;
|
|
9
9
|
output?: unknown;
|
|
10
10
|
userText?: unknown;
|
|
11
|
+
context?: DecisionContext;
|
|
11
12
|
};
|
|
12
13
|
export type CheckResult = {
|
|
13
14
|
evaluated: boolean;
|
package/lib/host/self-check.js
CHANGED
|
@@ -13,9 +13,15 @@ function boundedRubric(rubric) {
|
|
|
13
13
|
const levels = rubric.filter((item) => typeof item === "string" && Boolean(item.trim()) && item.length <= 256).slice(0, 10);
|
|
14
14
|
return levels.length >= 2 ? levels : DEFAULT_RUBRIC;
|
|
15
15
|
}
|
|
16
|
-
function scoreQuestion(output, rubric = DEFAULT_RUBRIC) {
|
|
16
|
+
function scoreQuestion(output, rubric = DEFAULT_RUBRIC, context) {
|
|
17
17
|
return {
|
|
18
|
-
state: {
|
|
18
|
+
state: {
|
|
19
|
+
// What the turn was asked to do. Without it the model scores an answer
|
|
20
|
+
// against nothing and cannot detect an off-topic reply.
|
|
21
|
+
...context?.request ? { request: context.request } : {},
|
|
22
|
+
...context?.background?.length ? { background: context.background } : {},
|
|
23
|
+
answer: String(output).slice(0, MAX_OUTPUT)
|
|
24
|
+
},
|
|
19
25
|
questions: { quality: {
|
|
20
26
|
type: "score",
|
|
21
27
|
instructions: "\u6309\u4E0B\u5217\u4ECE\u4F4E\u5230\u9AD8\u7684\u6863\u4F4D\uFF0C\u8BC4\u4F30\u8FD9\u6B21\u6700\u7EC8\u4EA7\u51FA\u5BF9\u7528\u6237\u8BF7\u6C42\u7684\u5B8C\u6210\u8D28\u91CF\u3002",
|
|
@@ -59,6 +65,7 @@ function createSelfCheck({ evaluate, sessions, steer, settings, minConfidence =
|
|
|
59
65
|
async review(turn) {
|
|
60
66
|
const output = typeof turn?.output === "string" ? turn.output.trim() : "";
|
|
61
67
|
if (!output) return { evaluated: false, lowScore: false, steered: false };
|
|
68
|
+
if (turn?.context !== void 0 && !turn.context.request) return { evaluated: false, lowScore: false, steered: false };
|
|
62
69
|
const id = turn?.agent?.session?.id;
|
|
63
70
|
if (sessions && typeof id === "string") {
|
|
64
71
|
try {
|
|
@@ -67,13 +74,13 @@ function createSelfCheck({ evaluate, sessions, steer, settings, minConfidence =
|
|
|
67
74
|
return { evaluated: false, lowScore: false, steered: false };
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
|
-
const task = taskExcerpt(turn?.userText);
|
|
77
|
+
const task = taskExcerpt(turn?.context?.request ?? turn?.userText);
|
|
71
78
|
const taskDetail = task ? { task } : {};
|
|
72
79
|
if (Number.isSafeInteger(turn?.turn) && turn?.turn >= 0) taskDetail.turn = turn?.turn;
|
|
73
80
|
const { mode, rubric, lowScoreThreshold, minConfidence: confidenceFloor, trendMinConfidence, trendRun, trendSevereRun } = await resolveSettings();
|
|
74
81
|
let answer;
|
|
75
82
|
try {
|
|
76
|
-
const result = await evaluate(scoreQuestion(output, rubric), { signal: turn?.signal });
|
|
83
|
+
const result = await evaluate(scoreQuestion(output, rubric, turn?.context), { signal: turn?.signal });
|
|
77
84
|
answer = result?.answers?.quality;
|
|
78
85
|
} catch (error) {
|
|
79
86
|
record(turn, "error", { ...taskDetail, reason: errorReason(error) });
|
|
@@ -248,6 +248,7 @@ function createSessionState({ maxSessions = 1024, maxLogEntries = 50, onLog, loa
|
|
|
248
248
|
if (typeof entry.path === "string" && entry.path.trim()) record.path = entry.path.replace(/\s+/g, " ").trim().slice(0, 512);
|
|
249
249
|
if (typeof entry.task === "string" && entry.task.trim()) record.task = redactCredentials(entry.task).replace(/\s+/g, " ").trim().slice(0, 512);
|
|
250
250
|
if (Number.isSafeInteger(entry.turn) && entry.turn >= 0) record.turn = entry.turn;
|
|
251
|
+
if (Number.isSafeInteger(entry.step) && entry.step >= 0) record.step = entry.step;
|
|
251
252
|
if (typeof entry.mode === "string" && entry.mode) record.mode = entry.mode.slice(0, 16);
|
|
252
253
|
if (typeof entry.score === "number" && Number.isFinite(entry.score)) record.score = Math.round(entry.score * 100) / 100;
|
|
253
254
|
if (typeof entry.confidence === "number" && Number.isFinite(entry.confidence)) record.confidence = Math.round(entry.confidence * 100) / 100;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AgentLike, CompleteSettings, EvaluateInput, Evaluator, SessionsFace } from './types.js';
|
|
1
|
+
import type { AgentLike, CompleteSettings, DecisionContext, EvaluateInput, Evaluator, SessionsFace } from './types.js';
|
|
2
2
|
export declare function extractConditions(userText: unknown): string[];
|
|
3
|
-
export declare function completionQuestions(conditions: unknown, evidence: unknown, answer: unknown): EvaluateInput;
|
|
3
|
+
export declare function completionQuestions(conditions: unknown, evidence: unknown, answer: unknown, context?: DecisionContext): EvaluateInput;
|
|
4
4
|
export declare const DEFAULT_MIN_CONFIDENCE = 0.4;
|
|
5
5
|
export type CompletionTurn = {
|
|
6
6
|
agent?: AgentLike;
|
|
@@ -9,6 +9,7 @@ export type CompletionTurn = {
|
|
|
9
9
|
output?: unknown;
|
|
10
10
|
userText?: unknown;
|
|
11
11
|
evidence?: unknown;
|
|
12
|
+
context?: DecisionContext;
|
|
12
13
|
};
|
|
13
14
|
export type CompletionResult = {
|
|
14
15
|
evaluated: boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { taskExcerpt } from "./excerpt.js";
|
|
1
2
|
import { createSteerOnce } from "./steer-once.js";
|
|
2
3
|
import { errorReason, isObject, isProbability } from "./types.js";
|
|
3
4
|
const MAX_STATE = 8 * 1024;
|
|
@@ -30,19 +31,33 @@ function extractConditions(userText) {
|
|
|
30
31
|
const compact = text.trim();
|
|
31
32
|
if (!/[\r\n]/.test(compact)) {
|
|
32
33
|
const parts = compact.split(/[、,,]/).map((part) => part.trim()).filter(Boolean);
|
|
33
|
-
|
|
34
|
+
const floor = compact.includes("\u3001") ? 2 : 3;
|
|
35
|
+
if (parts.length >= floor) for (const part of parts) push(part);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
return conditions.slice(0, MAX_CONDITIONS);
|
|
37
39
|
}
|
|
38
|
-
function completionQuestions(conditions, evidence, answer) {
|
|
40
|
+
function completionQuestions(conditions, evidence, answer, context) {
|
|
39
41
|
const items = Array.isArray(conditions) ? conditions.slice(0, MAX_CONDITIONS) : [];
|
|
40
|
-
const tools = Array.isArray(evidence) ? evidence.filter((item) => isObject(item) && typeof item.tool === "string").slice(0, MAX_EVIDENCE).map((item) =>
|
|
42
|
+
const tools = Array.isArray(evidence) ? evidence.filter((item) => isObject(item) && typeof item.tool === "string").slice(0, MAX_EVIDENCE).map((item) => {
|
|
43
|
+
const source = item;
|
|
44
|
+
const entry = {
|
|
45
|
+
tool: source.tool.slice(0, 128),
|
|
46
|
+
outcome: source.outcome === "error" ? "error" : "ok"
|
|
47
|
+
};
|
|
48
|
+
for (const key of ["command", "path", "result"]) {
|
|
49
|
+
const value = source[key];
|
|
50
|
+
if (typeof value === "string" && value.trim()) entry[key] = value.slice(0, 256);
|
|
51
|
+
}
|
|
52
|
+
return entry;
|
|
53
|
+
}) : [];
|
|
41
54
|
const state = {
|
|
55
|
+
...context?.request ? { request: context.request } : {},
|
|
56
|
+
...context?.background?.length ? { background: context.background } : {},
|
|
42
57
|
conditions: items.map((text, index) => ({ id: `c${index}`, text })),
|
|
43
58
|
toolEvidence: tools,
|
|
44
59
|
finalReply: String(answer ?? "").slice(0, MAX_STATE),
|
|
45
|
-
note: "\u5DE5\u5177\u8BC1\u636E\uFF08toolEvidence\uFF09\u662F\u5224\u5B9A\u4F9D\u636E\uFF1BfinalReply \u81EA\u79F0\u5B8C\u6210\u4E0D\u4F5C\u4E3A\u8BC1\u636E\u3002\u7F3A\u5C11\u8BC1\u636E\u65F6\u9009 insufficient_evidence\u3002"
|
|
60
|
+
note: "\u5DE5\u5177\u8BC1\u636E\uFF08toolEvidence\uFF09\u662F\u5224\u5B9A\u4F9D\u636E\uFF1BfinalReply \u81EA\u79F0\u5B8C\u6210\u4E0D\u4F5C\u4E3A\u8BC1\u636E\uFF0Cbackground \u53EA\u662F\u80CC\u666F\u800C\u4E0D\u662F\u672C\u8F6E\u8981\u6C42\u3002\u7F3A\u5C11\u8BC1\u636E\u65F6\u9009 insufficient_evidence\u3002"
|
|
46
61
|
};
|
|
47
62
|
const questions = Object.fromEntries(items.map((text, index) => [`c${index}`, {
|
|
48
63
|
type: "choice",
|
|
@@ -91,6 +106,9 @@ function createTaskCompletion({ evaluate, sessions, steer, settings, minConfiden
|
|
|
91
106
|
return {
|
|
92
107
|
async review(turn) {
|
|
93
108
|
const output = typeof turn?.output === "string" ? turn.output.trim() : "";
|
|
109
|
+
const task = taskExcerpt(turn?.context?.request ?? turn?.userText);
|
|
110
|
+
const taskDetail = task ? { task } : {};
|
|
111
|
+
if (Number.isSafeInteger(turn?.turn) && turn?.turn >= 0) taskDetail.turn = turn?.turn;
|
|
94
112
|
const { mode, minConditions, minConfidence: confidenceFloor } = await resolveSettings();
|
|
95
113
|
const conditions = extractConditions(turn?.userText);
|
|
96
114
|
if (conditions.length < minConditions) return { ...skip };
|
|
@@ -106,14 +124,14 @@ function createTaskCompletion({ evaluate, sessions, steer, settings, minConfiden
|
|
|
106
124
|
}
|
|
107
125
|
let answers;
|
|
108
126
|
try {
|
|
109
|
-
const result = await evaluate(completionQuestions(conditions, evidence, output), { signal: turn?.signal });
|
|
127
|
+
const result = await evaluate(completionQuestions(conditions, evidence, output, turn?.context), { signal: turn?.signal });
|
|
110
128
|
answers = isObject(result?.answers) ? result.answers : void 0;
|
|
111
129
|
} catch (error) {
|
|
112
|
-
record(turn, "error", { reason: errorReason(error) });
|
|
130
|
+
record(turn, "error", { ...taskDetail, reason: errorReason(error) });
|
|
113
131
|
return { ...skip };
|
|
114
132
|
}
|
|
115
133
|
if (!answers) {
|
|
116
|
-
record(turn, "error", { reason: "invalid-response" });
|
|
134
|
+
record(turn, "error", { ...taskDetail, reason: "invalid-response" });
|
|
117
135
|
return { ...skip };
|
|
118
136
|
}
|
|
119
137
|
let satisfied = 0, unsatisfied = 0, insufficient = 0;
|
|
@@ -131,7 +149,7 @@ function createTaskCompletion({ evaluate, sessions, steer, settings, minConfiden
|
|
|
131
149
|
} else insufficient++;
|
|
132
150
|
}
|
|
133
151
|
const outcome = unsatisfied > 0 ? "unsatisfied" : "ok";
|
|
134
|
-
const detail = { conditions: conditions.length, satisfied, unsatisfied, insufficient };
|
|
152
|
+
const detail = { ...taskDetail, conditions: conditions.length, satisfied, unsatisfied, insufficient };
|
|
135
153
|
const confident = unsatisfied > 0 && unsatisfiedMinConf >= confidenceFloor;
|
|
136
154
|
const steered = mode === "steer" && confident && typeof steer === "function" && turn?.agent ? steerOnce.attempt(turn, () => steer(turn.agent, STEER_TEXT)) : false;
|
|
137
155
|
if (steered) detail.steered = true;
|
package/lib/host/types.d.ts
CHANGED
|
@@ -166,6 +166,7 @@ export type LogRow = {
|
|
|
166
166
|
subject?: string;
|
|
167
167
|
task?: string;
|
|
168
168
|
turn?: number;
|
|
169
|
+
step?: number;
|
|
169
170
|
command?: string;
|
|
170
171
|
path?: string;
|
|
171
172
|
mode?: string;
|
|
@@ -268,6 +269,27 @@ export type ToolCandidate = {
|
|
|
268
269
|
name: string;
|
|
269
270
|
description?: string;
|
|
270
271
|
};
|
|
272
|
+
/** Bounded, masked data for adjudication; background is never authorization. */
|
|
273
|
+
export type ToolEvidence = {
|
|
274
|
+
tool: string;
|
|
275
|
+
outcome: 'ok' | 'error';
|
|
276
|
+
command?: string;
|
|
277
|
+
path?: string;
|
|
278
|
+
result?: string;
|
|
279
|
+
};
|
|
280
|
+
export type DecisionContext = {
|
|
281
|
+
request: string;
|
|
282
|
+
background: {
|
|
283
|
+
role: 'user' | 'assistant';
|
|
284
|
+
text: string;
|
|
285
|
+
}[];
|
|
286
|
+
progress: ToolEvidence[];
|
|
287
|
+
};
|
|
288
|
+
export type DecisionSnapshot = {
|
|
289
|
+
context: DecisionContext;
|
|
290
|
+
conditionsText: string;
|
|
291
|
+
evidence: ToolEvidence[];
|
|
292
|
+
};
|
|
271
293
|
export type AgentLike = {
|
|
272
294
|
session?: {
|
|
273
295
|
id?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-decision-layer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|