dsh-decision-layer 0.5.3 → 0.5.5
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 +2 -2
- package/README.md +2 -2
- package/lib/client.js +75 -29
- package/package.json +1 -1
- package/service/self-check.mjs +4 -0
- package/service/session-state.mjs +1 -0
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.5.
|
|
7
|
+
> **0.5.5 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
|
|
|
@@ -49,4 +49,4 @@ Restart `dsh web` and refresh after installing or updating: the host and client
|
|
|
49
49
|
|
|
50
50
|
## Safety boundary and development
|
|
51
51
|
|
|
52
|
-
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.
|
|
52
|
+
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.5 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.5.
|
|
7
|
+
> **0.5.5 是体验版。** 插件功能已可用,但裁决介入能带来多少实际价值仍在探索中——各决策点的阈值都是未经验证的启发式默认值,建议先以观察心态体验,别当成经过验证的效果保证。功能与后续进度以 [ROADMAP.md](<ROADMAP.md>) 为准。
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
@@ -53,4 +53,4 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
|
|
|
53
53
|
|
|
54
54
|
## 开发
|
|
55
55
|
|
|
56
|
-
服务端使用 Node.js `>=20.11`(`.mjs`,无需构建);客户端用 TypeScript + esbuild 构建。`npm test` 跑本地测试,`npm run build:client` 生成 `lib/client.js`。改动客户端后需重建并连同 `lib/client.js` 一起提交,否则 GitHub 安装拿到的是旧 bundle。0.5.
|
|
56
|
+
服务端使用 Node.js `>=20.11`(`.mjs`,无需构建);客户端用 TypeScript + esbuild 构建。`npm test` 跑本地测试,`npm run build:client` 生成 `lib/client.js`。改动客户端后需重建并连同 `lib/client.js` 一起提交,否则 GitHub 安装拿到的是旧 bundle。0.5.5 为体验版:介入能力已可用,但实际价值与阈值仍在探索,欢迎反馈决策日志观察到的误判与体感。
|
package/lib/client.js
CHANGED
|
@@ -96,6 +96,9 @@ var dictionaries = {
|
|
|
96
96
|
gatePassThrough: "\u672A\u5E72\u9884\uFF08\u6309\u539F\u8DEF\u5F84\u6267\u884C\uFF09",
|
|
97
97
|
checkResultLow: "\u5F97\u5206\u504F\u4F4E",
|
|
98
98
|
checkResultOk: "\u901A\u8FC7",
|
|
99
|
+
checkResultLowNoisy: "\u5F97\u5206\u504F\u4F4E\uFF08\u4F4E\u7F6E\u4FE1\uFF09",
|
|
100
|
+
checkNoiseTag: "\u566A\u58F0\uFF0C\u53EF\u5FFD\u7565",
|
|
101
|
+
checkNoiseHint: "\u6A21\u578B\u5BF9\u8FD9\u6B21\u6253\u5206\u51E0\u4E4E\u6CA1\u6709\u628A\u63E1\uFF08\u7F6E\u4FE1\u5EA6\u6781\u4F4E\uFF09\uFF0C\u8FD9\u6761\u4F4E\u5206\u4E0D\u8BA1\u5165\u8D28\u91CF\u8D8B\u52BF\u3001\u4E5F\u4E0D\u89E6\u53D1\u4EFB\u4F55\u8865\u5B8C\uFF0C\u89C6\u4F5C\u566A\u58F0\u5FFD\u7565\u5373\u53EF\u3002\u771F\u6B63\u503C\u5F97\u5173\u6CE8\u7684\u662F\u8FDE\u7EED\u591A\u6B21\u9AD8\u7F6E\u4FE1\u7684\u4F4E\u5206\u3002",
|
|
99
102
|
reasonUnavailable: "\u540E\u7AEF\u4E0D\u53EF\u8FBE",
|
|
100
103
|
reasonHttpError: "\u540E\u7AEF\u8FD4\u56DE\u9519\u8BEF",
|
|
101
104
|
reasonInvalidResponse: "\u54CD\u5E94\u683C\u5F0F\u4E0D\u7B26",
|
|
@@ -245,6 +248,9 @@ var dictionaries = {
|
|
|
245
248
|
gatePassThrough: "no intervention (proceeded on its original path)",
|
|
246
249
|
checkResultLow: "low score",
|
|
247
250
|
checkResultOk: "passed",
|
|
251
|
+
checkResultLowNoisy: "low score (low confidence)",
|
|
252
|
+
checkNoiseTag: "noise, ignore",
|
|
253
|
+
checkNoiseHint: "The model had almost no confidence in this score (near-zero confidence). This low score does not count toward the quality trend and triggers no re-prompt \u2014 treat it as noise. What matters is several consecutive high-confidence low scores.",
|
|
248
254
|
reasonUnavailable: "backend unreachable",
|
|
249
255
|
reasonHttpError: "backend returned an error",
|
|
250
256
|
reasonInvalidResponse: "unexpected response shape",
|
|
@@ -422,24 +428,33 @@ function renderCheck({ t }, entry) {
|
|
|
422
428
|
entry.reason ? ` \xB7 ${failDetail(t, entry)}` : ""
|
|
423
429
|
] })
|
|
424
430
|
] });
|
|
425
|
-
const
|
|
431
|
+
const noisy = entry.outcome === "low" && entry.lowConfidence === true;
|
|
432
|
+
const result = entry.outcome === "low" ? noisy ? t("checkResultLowNoisy") : t("checkResultLow") : t("checkResultOk");
|
|
433
|
+
const verdict = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: noisy ? "decision-log-muted-verdict" : "decision-log-verdict", children: result });
|
|
434
|
+
const score = entry.score !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", title: t("logScoreHint"), children: [
|
|
435
|
+
" \xB7 ",
|
|
436
|
+
t("logScore"),
|
|
437
|
+
" ",
|
|
438
|
+
entry.score,
|
|
439
|
+
"/2"
|
|
440
|
+
] }) : null;
|
|
441
|
+
const conf = typeof entry.confidence === "number" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
442
|
+
" \xB7 ",
|
|
443
|
+
t("logConfidence"),
|
|
444
|
+
" ",
|
|
445
|
+
entry.confidence
|
|
446
|
+
] }) : null;
|
|
447
|
+
const hint = noisy ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", title: t("checkNoiseHint"), children: [
|
|
448
|
+
" \xB7 ",
|
|
449
|
+
t("checkNoiseTag")
|
|
450
|
+
] }) : null;
|
|
426
451
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
427
452
|
label,
|
|
428
453
|
" \xB7 ",
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
" ",
|
|
434
|
-
entry.score,
|
|
435
|
-
"/2"
|
|
436
|
-
] }) : null,
|
|
437
|
-
typeof entry.confidence === "number" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "decision-log-detail", children: [
|
|
438
|
-
" \xB7 ",
|
|
439
|
-
t("logConfidence"),
|
|
440
|
-
" ",
|
|
441
|
-
entry.confidence
|
|
442
|
-
] }) : null
|
|
454
|
+
verdict,
|
|
455
|
+
score,
|
|
456
|
+
conf,
|
|
457
|
+
hint
|
|
443
458
|
] });
|
|
444
459
|
}
|
|
445
460
|
function renderNarrow({ t }, entry) {
|
|
@@ -538,7 +553,10 @@ function renderComplete({ t }, entry) {
|
|
|
538
553
|
function outcomeTagOf(entry) {
|
|
539
554
|
if (entry.kind === "narrow") return entry.outcome === "error" ? "error" : "ok";
|
|
540
555
|
if (entry.kind === "complete") return entry.outcome === "error" ? "error" : entry.outcome === "unsatisfied" ? "warn" : "ok";
|
|
541
|
-
|
|
556
|
+
if (entry.outcome === "deny") return "deny";
|
|
557
|
+
if (entry.outcome === "error") return "error";
|
|
558
|
+
if (entry.outcome === "low") return entry.kind === "check" && entry.lowConfidence === true ? "muted" : "warn";
|
|
559
|
+
return "ok";
|
|
542
560
|
}
|
|
543
561
|
function renderBody(props, entry) {
|
|
544
562
|
return entry.kind === "gate" ? renderGate(props, entry) : entry.kind === "narrow" ? renderNarrow(props, entry) : entry.kind === "complete" ? renderComplete(props, entry) : renderCheck(props, entry);
|
|
@@ -1095,14 +1113,20 @@ function ConfigForm({ t, fetchFn = fetch }) {
|
|
|
1095
1113
|
setNarrowOn(features?.narrow !== false);
|
|
1096
1114
|
setCompleteOn(features?.complete !== false);
|
|
1097
1115
|
};
|
|
1098
|
-
const
|
|
1116
|
+
const applyBackend = (value) => {
|
|
1099
1117
|
setConfig(value);
|
|
1118
|
+
};
|
|
1119
|
+
const applyAdvanced = (value) => {
|
|
1100
1120
|
setCheckMode(value.checkSettings?.mode ?? "observe");
|
|
1101
1121
|
setNarrowMode(value.narrowSettings?.mode ?? "enforce");
|
|
1102
1122
|
setCompleteMode(value.completeSettings?.mode ?? "observe");
|
|
1103
1123
|
setKeepPrefixes(prefixesText(value.narrowSettings));
|
|
1104
1124
|
setMaxCandidates(String(maxCandidatesOf(value.narrowSettings)));
|
|
1105
1125
|
setThreshold(String(thresholdOf(value.narrowSettings)));
|
|
1126
|
+
};
|
|
1127
|
+
const applyConfig = (value) => {
|
|
1128
|
+
applyBackend(value);
|
|
1129
|
+
applyAdvanced(value);
|
|
1106
1130
|
applyFeatures(value.features);
|
|
1107
1131
|
};
|
|
1108
1132
|
(0, import_react4.useEffect)(() => {
|
|
@@ -1123,6 +1147,23 @@ function ConfigForm({ t, fetchFn = fetch }) {
|
|
|
1123
1147
|
live = false;
|
|
1124
1148
|
};
|
|
1125
1149
|
}, [t, fetchFn]);
|
|
1150
|
+
const toggleFeature = async (features, revert) => {
|
|
1151
|
+
setBusy(true);
|
|
1152
|
+
try {
|
|
1153
|
+
const value = await request2("config", fetchFn, {
|
|
1154
|
+
method: "PUT",
|
|
1155
|
+
headers: { "content-type": "application/json" },
|
|
1156
|
+
body: JSON.stringify({ features })
|
|
1157
|
+
});
|
|
1158
|
+
applyFeatures(value.features);
|
|
1159
|
+
} catch {
|
|
1160
|
+
revert();
|
|
1161
|
+
setMessage({ text: t("error"), ok: false });
|
|
1162
|
+
} finally {
|
|
1163
|
+
setBusy(false);
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
const featureState = () => ({ gate: gateOn, check: checkOn, narrow: narrowOn, complete: completeOn });
|
|
1126
1167
|
const saveAdvanced = async (event) => {
|
|
1127
1168
|
event.preventDefault();
|
|
1128
1169
|
const parsedCap = Number.parseInt(maxCandidates, 10);
|
|
@@ -1137,11 +1178,10 @@ function ConfigForm({ t, fetchFn = fetch }) {
|
|
|
1137
1178
|
body: JSON.stringify({
|
|
1138
1179
|
checkSettings: { mode: checkMode },
|
|
1139
1180
|
narrowSettings: { mode: narrowMode, keepPrefixes: parsePrefixes(keepPrefixes), maxCandidates: cap, threshold: thresh },
|
|
1140
|
-
completeSettings: { mode: completeMode }
|
|
1141
|
-
features: { gate: gateOn, check: checkOn, narrow: narrowOn, complete: completeOn }
|
|
1181
|
+
completeSettings: { mode: completeMode }
|
|
1142
1182
|
})
|
|
1143
1183
|
});
|
|
1144
|
-
|
|
1184
|
+
applyAdvanced(value);
|
|
1145
1185
|
setAdvancedDirty(false);
|
|
1146
1186
|
setMessage({ text: t("saved"), ok: true });
|
|
1147
1187
|
} catch {
|
|
@@ -1168,7 +1208,7 @@ ${url}`)) return;
|
|
|
1168
1208
|
...needsHttpConsent ? { confirmHttpUrl: url } : {}
|
|
1169
1209
|
})
|
|
1170
1210
|
});
|
|
1171
|
-
|
|
1211
|
+
applyBackend(value);
|
|
1172
1212
|
setKey("");
|
|
1173
1213
|
setBackendDirty(false);
|
|
1174
1214
|
setMessage({ text: t("saved"), ok: true });
|
|
@@ -1223,10 +1263,10 @@ ${url}`)) return;
|
|
|
1223
1263
|
{
|
|
1224
1264
|
label: t("featureGate"),
|
|
1225
1265
|
checked: gateOn,
|
|
1226
|
-
disabled: !ready,
|
|
1266
|
+
disabled: !ready || busy,
|
|
1227
1267
|
onChange: (next) => {
|
|
1228
1268
|
setGateOn(next);
|
|
1229
|
-
|
|
1269
|
+
void toggleFeature({ ...featureState(), gate: next }, () => setGateOn(!next));
|
|
1230
1270
|
}
|
|
1231
1271
|
}
|
|
1232
1272
|
),
|
|
@@ -1236,10 +1276,10 @@ ${url}`)) return;
|
|
|
1236
1276
|
{
|
|
1237
1277
|
label: t("featureCheck"),
|
|
1238
1278
|
checked: checkOn,
|
|
1239
|
-
disabled: !ready,
|
|
1279
|
+
disabled: !ready || busy,
|
|
1240
1280
|
onChange: (next) => {
|
|
1241
1281
|
setCheckOn(next);
|
|
1242
|
-
|
|
1282
|
+
void toggleFeature({ ...featureState(), check: next }, () => setCheckOn(!next));
|
|
1243
1283
|
}
|
|
1244
1284
|
}
|
|
1245
1285
|
),
|
|
@@ -1249,10 +1289,10 @@ ${url}`)) return;
|
|
|
1249
1289
|
{
|
|
1250
1290
|
label: t("featureNarrow"),
|
|
1251
1291
|
checked: narrowOn,
|
|
1252
|
-
disabled: !ready,
|
|
1292
|
+
disabled: !ready || busy,
|
|
1253
1293
|
onChange: (next) => {
|
|
1254
1294
|
setNarrowOn(next);
|
|
1255
|
-
|
|
1295
|
+
void toggleFeature({ ...featureState(), narrow: next }, () => setNarrowOn(!next));
|
|
1256
1296
|
}
|
|
1257
1297
|
}
|
|
1258
1298
|
),
|
|
@@ -1262,10 +1302,10 @@ ${url}`)) return;
|
|
|
1262
1302
|
{
|
|
1263
1303
|
label: t("featureComplete"),
|
|
1264
1304
|
checked: completeOn,
|
|
1265
|
-
disabled: !ready,
|
|
1305
|
+
disabled: !ready || busy,
|
|
1266
1306
|
onChange: (next) => {
|
|
1267
1307
|
setCompleteOn(next);
|
|
1268
|
-
|
|
1308
|
+
void toggleFeature({ ...featureState(), complete: next }, () => setCompleteOn(!next));
|
|
1269
1309
|
}
|
|
1270
1310
|
}
|
|
1271
1311
|
),
|
|
@@ -1538,6 +1578,7 @@ var css = `
|
|
|
1538
1578
|
.decision-log-item.decision-log-deny{border-left-color:#c0392b}
|
|
1539
1579
|
.decision-log-item.decision-log-error,.decision-log-item.decision-log-warn{border-left-color:#c98a1e}
|
|
1540
1580
|
.decision-log-item.decision-log-ok{border-left-color:#1f8a70}
|
|
1581
|
+
.decision-log-item.decision-log-muted{border-left-color:var(--dsw-alias-border-l2,#dce1e6)}
|
|
1541
1582
|
.decision-log-time{color:var(--dsw-alias-label-tertiary,#77818b);font-variant-numeric:tabular-nums;flex:0 0 auto}
|
|
1542
1583
|
.decision-log-body{color:var(--dsw-alias-label-primary,#202124);flex:1 1 auto;min-width:0}
|
|
1543
1584
|
.decision-rate{flex:0 0 auto;display:inline-flex;gap:1px;align-self:center}
|
|
@@ -1554,6 +1595,11 @@ var css = `
|
|
|
1554
1595
|
.decision-log-ok .decision-log-verdict{color:#1f8a70}
|
|
1555
1596
|
.decision-log-warn .decision-log-verdict{color:#c98a1e}
|
|
1556
1597
|
.decision-log-detail{color:var(--dsw-alias-label-secondary,#59636e)}
|
|
1598
|
+
/* A low-confidence low score is noise: its verdict is shown muted (not the amber
|
|
1599
|
+
warn accent) and a "noise, ignore" tag hangs off the row so it never reads as a
|
|
1600
|
+
real quality dip. */
|
|
1601
|
+
.decision-log-muted-verdict{font-weight:600;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
1602
|
+
.decision-log-muted .decision-log-time{opacity:.75}
|
|
1557
1603
|
|
|
1558
1604
|
.decision-status{display:flex;align-items:center;gap:.5rem;margin-top:.7rem;font-size:.82rem}
|
|
1559
1605
|
.decision-status-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto;background:#9aa5b1}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-decision-layer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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",
|
package/service/self-check.mjs
CHANGED
|
@@ -80,6 +80,10 @@ export function createSelfCheck({ evaluate, sessions, steer, settings, minConfid
|
|
|
80
80
|
// The trend run advances over every evaluated score; a high-confidence low
|
|
81
81
|
// score is the only thing that grows it. Thresholds flow to session-state.
|
|
82
82
|
const trend = { confident: lowScore && confidence !== undefined && confidence >= trendMinConfidence, trendRun, trendSevereRun };
|
|
83
|
+
// A low score the model is not confident about (confidence present but below
|
|
84
|
+
// the trend threshold) does not advance the quality-trend judgment. Flag it
|
|
85
|
+
// so the decision log can mark it as low-signal noise instead of a real dip.
|
|
86
|
+
if (lowScore && confidence !== undefined && !trend.confident) detail.lowConfidence = true;
|
|
83
87
|
record(turn, lowScore ? 'low' : 'ok', { score: answer.score, ...detail }, trend);
|
|
84
88
|
if (!lowScore) return { evaluated: true, lowScore: false, steered: false, score: answer.score };
|
|
85
89
|
const confident = confidence !== undefined && confidence >= minConfidence;
|
|
@@ -131,6 +131,7 @@ export function createSessionState({ maxSessions = 1024, maxLogEntries = 50, onL
|
|
|
131
131
|
if (Number.isSafeInteger(entry.unsatisfied) && entry.unsatisfied >= 0) record.unsatisfied = entry.unsatisfied;
|
|
132
132
|
if (Number.isSafeInteger(entry.insufficient) && entry.insufficient >= 0) record.insufficient = entry.insufficient;
|
|
133
133
|
if (entry.steered === true) record.steered = true;
|
|
134
|
+
if (entry.lowConfidence === true) record.lowConfidence = true;
|
|
134
135
|
if (Array.isArray(entry.tools)) {
|
|
135
136
|
const names = entry.tools.filter(name => typeof name === 'string' && name.trim()).slice(0, 12).map(name => name.trim().slice(0, 64));
|
|
136
137
|
if (names.length > 0) record.tools = names;
|