dsh-decision-layer 0.5.4 → 0.5.6
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 +3 -3
- package/lib/client.js +35 -13
- package/package.json +1 -1
- package/service/config-store.mjs +12 -1
- package/service/narrowing.mjs +54 -5
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
|
|
|
@@ -40,7 +40,7 @@ dsh plugin --profile web add file:/绝对路径/dsh-decision-layer
|
|
|
40
40
|
- **产出自检**:回合结束用评分量表给最终产出打分(0–2,按各档位概率加权,可能是小数)。默认只提示、不打断;可开启「低分补完」,得分偏低且模型有把握时以你的名义追加一次补完提示,同一回合最多补一次。
|
|
41
41
|
- **任务完成核对**:当用户明确列出多个交付条件(编号 / 项目符号 / 多命令)时,回合结束前用单选逐项判定「已满足 / 未满足 / 证据不足」,工具证据优先、回复自述不算证据;确定性提取、不推断隐含期望。默认只观察、可配 steer,与产出自检并存、指标分列。
|
|
42
42
|
- **产出质量趋势告警**:建在自检数据上的只读提醒。连续多次高置信低分时,把输入框旁触发按钮的图标从灰变橙、变红,hover 提示可能是任务变难或模型波动、建议调整 prompt 或更换模型。纯提示、不驱动自动干预;高置信门槛 / 连续轮数 / 严重档三个阈值可配(默认 0.6 / 3 / 5),偏保守、待实测校准。
|
|
43
|
-
- **工具收窄**:回合开始时用 noul 逐个判断可选工具与本轮任务的相关性,摘掉用不上的(相关性概率低于摘除阈值,默认 0.3、可配),只在宿主已允许的集合内取交集、不扩权。默认真正生效,可切到「仅观察」只记录不改工具集。三重保护:核心工具(读写、编辑、Shell、检索、待办,以及问用户、目标、技能、交付、后台作业、协作等 agent 元能力)始终保留、不参与判定;可选工具超过候选上限(默认 20,可配)时自动降为仅观察;保留下来不足 5 个时视为判定不可信、跳过。**保留工具前缀**白名单里的工具(默认含 `mcp__openviking`、`team_task_`)也始终保留、不参与判定,适合记忆 /
|
|
43
|
+
- **工具收窄**:回合开始时用 noul 逐个判断可选工具与本轮任务的相关性,摘掉用不上的(相关性概率低于摘除阈值,默认 0.3、可配),只在宿主已允许的集合内取交集、不扩权。默认真正生效,可切到「仅观察」只记录不改工具集。三重保护:核心工具(读写、编辑、Shell、检索、待办,以及问用户、目标、技能、交付、后台作业、协作等 agent 元能力)始终保留、不参与判定;可选工具超过候选上限(默认 20,可配)时自动降为仅观察;保留下来不足 5 个时视为判定不可信、跳过。**保留工具前缀**白名单里的工具(默认含 `mcp__openviking`、`team_task_`)也始终保留、不参与判定,适合记忆 / 检索、协作这类常驻、按需触发的工具。**分批策略**按后端上下文容量自适应(`batchMode`,默认 `auto`):大上下文后端(如 Jev)一次请求判完全部工具;小上下文后端(如本地 tev1,约 2050 token 单请求上限)在遇到上下文超限错误时自动降级为「每工具一次请求、并发执行」并记住,之后直接走拆分,不再反复探测。也可显式设 `single`(始终整批)或 `split`(始终逐工具)。某个工具的拆分请求失败时该工具保守保留。
|
|
44
44
|
- **防循环**:确定性判断,不走后端;同一调用连续重复超过阈值即拒绝,打断死循环。始终生效,不在决策点开关范围内。
|
|
45
45
|
- **会话面板**:输入框下方的 dock 触发器弹出面板,展示自动裁决指标(评估数、失败回退、模型建议与实际结果分列)、会话总开关和决策日志;无自动裁决时显示空态。总开关关闭只暂停自动介入,不影响手动裁决。
|
|
46
46
|
- **决策分析页**:会话内「决策分析」tab(`conversation.view`,与对话/轨迹平级、天然按会话隔离),基于已落盘日志做只读聚合,展示过程画像(各决策点触发数与结果分布、自检平均分)、质量趋势阈值回测、判断准确度汇总,并可对每条决策标注「判对 / 判错 / 说不准」。标注与介入面板同源:任一处标注都写入同一份日志、按决策 id 关联、取最新。只读日志、只追加标注,不改任何决策行为。
|
|
@@ -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
|
@@ -1113,14 +1113,20 @@ function ConfigForm({ t, fetchFn = fetch }) {
|
|
|
1113
1113
|
setNarrowOn(features?.narrow !== false);
|
|
1114
1114
|
setCompleteOn(features?.complete !== false);
|
|
1115
1115
|
};
|
|
1116
|
-
const
|
|
1116
|
+
const applyBackend = (value) => {
|
|
1117
1117
|
setConfig(value);
|
|
1118
|
+
};
|
|
1119
|
+
const applyAdvanced = (value) => {
|
|
1118
1120
|
setCheckMode(value.checkSettings?.mode ?? "observe");
|
|
1119
1121
|
setNarrowMode(value.narrowSettings?.mode ?? "enforce");
|
|
1120
1122
|
setCompleteMode(value.completeSettings?.mode ?? "observe");
|
|
1121
1123
|
setKeepPrefixes(prefixesText(value.narrowSettings));
|
|
1122
1124
|
setMaxCandidates(String(maxCandidatesOf(value.narrowSettings)));
|
|
1123
1125
|
setThreshold(String(thresholdOf(value.narrowSettings)));
|
|
1126
|
+
};
|
|
1127
|
+
const applyConfig = (value) => {
|
|
1128
|
+
applyBackend(value);
|
|
1129
|
+
applyAdvanced(value);
|
|
1124
1130
|
applyFeatures(value.features);
|
|
1125
1131
|
};
|
|
1126
1132
|
(0, import_react4.useEffect)(() => {
|
|
@@ -1141,6 +1147,23 @@ function ConfigForm({ t, fetchFn = fetch }) {
|
|
|
1141
1147
|
live = false;
|
|
1142
1148
|
};
|
|
1143
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 });
|
|
1144
1167
|
const saveAdvanced = async (event) => {
|
|
1145
1168
|
event.preventDefault();
|
|
1146
1169
|
const parsedCap = Number.parseInt(maxCandidates, 10);
|
|
@@ -1155,11 +1178,10 @@ function ConfigForm({ t, fetchFn = fetch }) {
|
|
|
1155
1178
|
body: JSON.stringify({
|
|
1156
1179
|
checkSettings: { mode: checkMode },
|
|
1157
1180
|
narrowSettings: { mode: narrowMode, keepPrefixes: parsePrefixes(keepPrefixes), maxCandidates: cap, threshold: thresh },
|
|
1158
|
-
completeSettings: { mode: completeMode }
|
|
1159
|
-
features: { gate: gateOn, check: checkOn, narrow: narrowOn, complete: completeOn }
|
|
1181
|
+
completeSettings: { mode: completeMode }
|
|
1160
1182
|
})
|
|
1161
1183
|
});
|
|
1162
|
-
|
|
1184
|
+
applyAdvanced(value);
|
|
1163
1185
|
setAdvancedDirty(false);
|
|
1164
1186
|
setMessage({ text: t("saved"), ok: true });
|
|
1165
1187
|
} catch {
|
|
@@ -1186,7 +1208,7 @@ ${url}`)) return;
|
|
|
1186
1208
|
...needsHttpConsent ? { confirmHttpUrl: url } : {}
|
|
1187
1209
|
})
|
|
1188
1210
|
});
|
|
1189
|
-
|
|
1211
|
+
applyBackend(value);
|
|
1190
1212
|
setKey("");
|
|
1191
1213
|
setBackendDirty(false);
|
|
1192
1214
|
setMessage({ text: t("saved"), ok: true });
|
|
@@ -1241,10 +1263,10 @@ ${url}`)) return;
|
|
|
1241
1263
|
{
|
|
1242
1264
|
label: t("featureGate"),
|
|
1243
1265
|
checked: gateOn,
|
|
1244
|
-
disabled: !ready,
|
|
1266
|
+
disabled: !ready || busy,
|
|
1245
1267
|
onChange: (next) => {
|
|
1246
1268
|
setGateOn(next);
|
|
1247
|
-
|
|
1269
|
+
void toggleFeature({ ...featureState(), gate: next }, () => setGateOn(!next));
|
|
1248
1270
|
}
|
|
1249
1271
|
}
|
|
1250
1272
|
),
|
|
@@ -1254,10 +1276,10 @@ ${url}`)) return;
|
|
|
1254
1276
|
{
|
|
1255
1277
|
label: t("featureCheck"),
|
|
1256
1278
|
checked: checkOn,
|
|
1257
|
-
disabled: !ready,
|
|
1279
|
+
disabled: !ready || busy,
|
|
1258
1280
|
onChange: (next) => {
|
|
1259
1281
|
setCheckOn(next);
|
|
1260
|
-
|
|
1282
|
+
void toggleFeature({ ...featureState(), check: next }, () => setCheckOn(!next));
|
|
1261
1283
|
}
|
|
1262
1284
|
}
|
|
1263
1285
|
),
|
|
@@ -1267,10 +1289,10 @@ ${url}`)) return;
|
|
|
1267
1289
|
{
|
|
1268
1290
|
label: t("featureNarrow"),
|
|
1269
1291
|
checked: narrowOn,
|
|
1270
|
-
disabled: !ready,
|
|
1292
|
+
disabled: !ready || busy,
|
|
1271
1293
|
onChange: (next) => {
|
|
1272
1294
|
setNarrowOn(next);
|
|
1273
|
-
|
|
1295
|
+
void toggleFeature({ ...featureState(), narrow: next }, () => setNarrowOn(!next));
|
|
1274
1296
|
}
|
|
1275
1297
|
}
|
|
1276
1298
|
),
|
|
@@ -1280,10 +1302,10 @@ ${url}`)) return;
|
|
|
1280
1302
|
{
|
|
1281
1303
|
label: t("featureComplete"),
|
|
1282
1304
|
checked: completeOn,
|
|
1283
|
-
disabled: !ready,
|
|
1305
|
+
disabled: !ready || busy,
|
|
1284
1306
|
onChange: (next) => {
|
|
1285
1307
|
setCompleteOn(next);
|
|
1286
|
-
|
|
1308
|
+
void toggleFeature({ ...featureState(), complete: next }, () => setCompleteOn(!next));
|
|
1287
1309
|
}
|
|
1288
1310
|
}
|
|
1289
1311
|
),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-decision-layer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
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/config-store.mjs
CHANGED
|
@@ -97,7 +97,7 @@ function validCheck(value) {
|
|
|
97
97
|
|
|
98
98
|
function validNarrow(value) {
|
|
99
99
|
if (!safeObject(value)) return undefined;
|
|
100
|
-
if (Object.keys(value).some(key => !['mode', 'threshold', 'keepPrefixes', 'maxCandidates'].includes(key))) throw new Error('Invalid narrowing settings');
|
|
100
|
+
if (Object.keys(value).some(key => !['mode', 'threshold', 'keepPrefixes', 'maxCandidates', 'batchMode'].includes(key))) throw new Error('Invalid narrowing settings');
|
|
101
101
|
const narrow = {};
|
|
102
102
|
if (value.mode !== undefined) {
|
|
103
103
|
if (value.mode !== 'observe' && value.mode !== 'enforce') throw new Error('Invalid narrowing settings');
|
|
@@ -124,6 +124,17 @@ function validNarrow(value) {
|
|
|
124
124
|
if (!Number.isSafeInteger(value.maxCandidates) || value.maxCandidates < 1 || value.maxCandidates > 200) throw new Error('Invalid narrowing settings');
|
|
125
125
|
narrow.maxCandidates = value.maxCandidates;
|
|
126
126
|
}
|
|
127
|
+
// Per-request batching strategy for the relevance pass, chosen by backend
|
|
128
|
+
// context capacity. 'single' sends every tool in one systemone request (best
|
|
129
|
+
// for large-context backends like Jev — one call). 'split' sends one request
|
|
130
|
+
// per tool concurrently (required for small-context backends like tev1, whose
|
|
131
|
+
// ~2050-token prompt limit rejects a many-tool request). 'auto' (default)
|
|
132
|
+
// tries 'single' first and falls back to 'split' only after a context-overflow
|
|
133
|
+
// http-error, so Jev stays at one call and tev1 self-corrects.
|
|
134
|
+
if (value.batchMode !== undefined) {
|
|
135
|
+
if (!['auto', 'single', 'split'].includes(value.batchMode)) throw new Error('Invalid narrowing settings');
|
|
136
|
+
narrow.batchMode = value.batchMode;
|
|
137
|
+
}
|
|
127
138
|
return narrow;
|
|
128
139
|
}
|
|
129
140
|
|
package/service/narrowing.mjs
CHANGED
|
@@ -82,15 +82,22 @@ export function relevanceQuestions(state, tools) {
|
|
|
82
82
|
// C. if narrowing would leave fewer than `minKeep` tools total, the verdict is
|
|
83
83
|
// treated as untrustworthy and no restriction is applied.
|
|
84
84
|
export function createNarrowing({ evaluate, sessions, settings, coreTools = DEFAULT_CORE_TOOLS,
|
|
85
|
-
keepPrefixes = DEFAULT_KEEP_PREFIXES, minCandidates = 2, maxCandidates = 20, minKeep = 5 } = {}) {
|
|
85
|
+
keepPrefixes = DEFAULT_KEEP_PREFIXES, minCandidates = 2, maxCandidates = 20, minKeep = 5, splitConcurrency = 8 } = {}) {
|
|
86
86
|
if (typeof evaluate !== 'function') throw new Error('Narrowing requires an evaluator');
|
|
87
87
|
const core = new Set(Array.isArray(coreTools) ? coreTools.filter(name => typeof name === 'string') : []);
|
|
88
88
|
const defaultPrefixes = Array.isArray(keepPrefixes) ? keepPrefixes.filter(prefix => typeof prefix === 'string' && prefix) : [];
|
|
89
|
+
const concurrency = Number.isSafeInteger(splitConcurrency) && splitConcurrency >= 1 ? splitConcurrency : 8;
|
|
90
|
+
// Set once a 'single' (whole-batch) pass hits a context-overflow http-error:
|
|
91
|
+
// from then on 'auto' sends one request per tool. A large-context backend
|
|
92
|
+
// (Jev) never trips this and stays at one call; a small-context backend
|
|
93
|
+
// (tev1, ~2050-token prompt) self-corrects after the first failure.
|
|
94
|
+
let adaptiveSplit = false;
|
|
89
95
|
const resolveSettings = async () => {
|
|
90
96
|
let raw = {};
|
|
91
97
|
try { raw = (typeof settings === 'function' ? await settings() : settings) ?? {}; } catch { raw = {}; }
|
|
92
98
|
const mode = raw?.mode === 'observe' ? 'observe' : 'enforce';
|
|
93
99
|
const threshold = typeof raw?.threshold === 'number' && Number.isFinite(raw.threshold) && raw.threshold >= 0 && raw.threshold <= 1 ? raw.threshold : 0.3;
|
|
100
|
+
const batchMode = raw?.batchMode === 'single' || raw?.batchMode === 'split' ? raw.batchMode : 'auto';
|
|
94
101
|
// An explicit array in settings (including empty) overrides the default set.
|
|
95
102
|
const prefixes = Array.isArray(raw?.keepPrefixes)
|
|
96
103
|
? raw.keepPrefixes.filter(prefix => typeof prefix === 'string' && prefix)
|
|
@@ -100,7 +107,7 @@ export function createNarrowing({ evaluate, sessions, settings, coreTools = DEFA
|
|
|
100
107
|
// never large enough to be rejected before it is sent.
|
|
101
108
|
const configuredCap = Number.isSafeInteger(raw?.maxCandidates) && raw.maxCandidates >= 1 ? raw.maxCandidates : maxCandidates;
|
|
102
109
|
const cap = Math.min(configuredCap, MAX_QUESTIONS);
|
|
103
|
-
return { mode, threshold, prefixes, cap };
|
|
110
|
+
return { mode, threshold, prefixes, cap, batchMode };
|
|
104
111
|
};
|
|
105
112
|
const record = (turn, outcome, detail = {}) => {
|
|
106
113
|
const id = turn?.agent?.session?.id;
|
|
@@ -111,12 +118,34 @@ export function createNarrowing({ evaluate, sessions, settings, coreTools = DEFA
|
|
|
111
118
|
try { sessions.log(id, { kind: 'narrow', outcome, ...detail }); }
|
|
112
119
|
catch { /* logging is best effort */ }
|
|
113
120
|
};
|
|
121
|
+
// Send one relevance request per tool with bounded concurrency, then merge the
|
|
122
|
+
// per-tool answers into one result. Each request's prompt is only the task
|
|
123
|
+
// state plus a single question, so it cannot exceed a small-context backend's
|
|
124
|
+
// prompt limit. A tool whose request fails is omitted from the merged answers,
|
|
125
|
+
// which narrowTools treats as "keep" (best effort). Returns null only when
|
|
126
|
+
// every tool failed, so the caller can still report a backend error.
|
|
127
|
+
const evaluateSplit = async (state, tools, signal) => {
|
|
128
|
+
const answers = Object.create(null);
|
|
129
|
+
let anyOk = false;
|
|
130
|
+
for (let i = 0; i < tools.length; i += concurrency) {
|
|
131
|
+
const group = tools.slice(i, i + concurrency);
|
|
132
|
+
const settled = await Promise.all(group.map(async tool => {
|
|
133
|
+
try {
|
|
134
|
+
const reply = await evaluate(relevanceQuestions(state, [tool]), { signal });
|
|
135
|
+
const answer = reply?.answers?.[tool.name];
|
|
136
|
+
return isObject(answer) ? [tool.name, answer] : undefined;
|
|
137
|
+
} catch { return undefined; }
|
|
138
|
+
}));
|
|
139
|
+
for (const pair of settled) { if (pair) { answers[pair[0]] = pair[1]; anyOk = true; } }
|
|
140
|
+
}
|
|
141
|
+
return anyOk ? { model: 'split', answers } : null;
|
|
142
|
+
};
|
|
114
143
|
const skip = { evaluated: false, applied: false, keep: [], drop: [], mode: 'observe' };
|
|
115
144
|
return {
|
|
116
145
|
async review(turn) {
|
|
117
146
|
const tools = Array.isArray(turn?.tools) ? turn.tools.filter(tool => tool && typeof tool.name === 'string') : [];
|
|
118
147
|
const names = tools.map(tool => tool.name);
|
|
119
|
-
const { mode, threshold, prefixes, cap } = await resolveSettings();
|
|
148
|
+
const { mode, threshold, prefixes, cap, batchMode } = await resolveSettings();
|
|
120
149
|
// A: core tools and keep-prefix matches are always kept and never sent to
|
|
121
150
|
// the model for judging (e.g. mcp__openviking memory tools stay resident).
|
|
122
151
|
const alwaysKeep = name => core.has(name) || prefixes.some(prefix => name.startsWith(prefix));
|
|
@@ -139,9 +168,29 @@ export function createNarrowing({ evaluate, sessions, settings, coreTools = DEFA
|
|
|
139
168
|
const effectiveMode = overCap ? 'observe' : mode;
|
|
140
169
|
const judged = overCap ? optional.slice(0, cap) : optional;
|
|
141
170
|
const judgedNames = judged.map(tool => tool.name);
|
|
171
|
+
// Choose per-request strategy by backend context capacity. 'split' (or an
|
|
172
|
+
// 'auto' pass that already fell back) sends one request per tool so no
|
|
173
|
+
// prompt exceeds a small-context limit. 'single'/'auto' first try one
|
|
174
|
+
// whole-batch request; under 'auto', a context-overflow http-error flips
|
|
175
|
+
// this instance to split from then on and retries the same tools split.
|
|
142
176
|
let result;
|
|
143
|
-
|
|
144
|
-
|
|
177
|
+
if (batchMode === 'split' || adaptiveSplit) {
|
|
178
|
+
result = await evaluateSplit(turn.state, judged, turn.signal);
|
|
179
|
+
if (!result) { record(turn, 'error', { reason: 'unreachable', mode: effectiveMode }); return { ...skip, keep: names, mode: effectiveMode }; }
|
|
180
|
+
} else {
|
|
181
|
+
try { result = await evaluate(relevanceQuestions(turn.state, judged), { signal: turn.signal }); }
|
|
182
|
+
catch (error) {
|
|
183
|
+
const reason = typeof error?.reason === 'string' ? error.reason : 'unreachable';
|
|
184
|
+
if (batchMode === 'auto' && reason === 'http-error') {
|
|
185
|
+
adaptiveSplit = true;
|
|
186
|
+
result = await evaluateSplit(turn.state, judged, turn.signal);
|
|
187
|
+
if (!result) { record(turn, 'error', { reason, mode: effectiveMode }); return { ...skip, keep: names, mode: effectiveMode }; }
|
|
188
|
+
} else {
|
|
189
|
+
record(turn, 'error', { reason, mode: effectiveMode });
|
|
190
|
+
return { ...skip, keep: names, mode: effectiveMode };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
145
194
|
const narrowed = narrowTools(judgedNames, result, { threshold });
|
|
146
195
|
const keep = [...keptNames, ...narrowed.keep];
|
|
147
196
|
const drop = narrowed.drop;
|