dsh-decision-layer 0.7.3 → 0.7.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 +8 -6
- package/README.md +8 -6
- package/docs/en/DETAIL.md +2 -1
- package/docs/zh/DETAIL.md +2 -1
- package/lib/client.js +371 -20
- package/lib/host/backend.d.ts +3 -2
- package/lib/host/backend.js +61 -43
- package/lib/host/checkpoint-review.d.ts +27 -0
- package/lib/host/checkpoint-review.js +334 -0
- package/lib/host/config-store.js +21 -1
- package/lib/host/danger-gate.js +8 -6
- package/lib/host/decision-context.js +20 -1
- package/lib/host/evaluation-trace.d.ts +8 -0
- package/lib/host/evaluation-trace.js +70 -0
- package/lib/host/execution-observer.d.ts +26 -0
- package/lib/host/execution-observer.js +168 -0
- package/lib/host/index.d.ts +1 -0
- package/lib/host/index.js +61 -16
- package/lib/host/log-analyze.d.ts +20 -1
- package/lib/host/log-analyze.js +66 -8
- package/lib/host/log-row.d.ts +3 -0
- package/lib/host/log-row.js +66 -0
- package/lib/host/manager-api.d.ts +1 -0
- package/lib/host/manager-api.js +5 -2
- package/lib/host/narrowing.js +11 -9
- package/lib/host/self-check.js +23 -6
- package/lib/host/session-state.d.ts +3 -1
- package/lib/host/session-state.js +30 -46
- package/lib/host/task-completion.js +6 -4
- package/lib/host/types.d.ts +43 -3
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { redactCredentials } from "./excerpt.js";
|
|
2
|
+
import { isObject } from "./types.js";
|
|
3
|
+
const KINDS = /* @__PURE__ */ new Set(["gate", "check", "narrow", "complete", "checkpoint"]);
|
|
4
|
+
const TRIGGERS = /* @__PURE__ */ new Set(["heartbeat", "unresolved_failure", "verification_after_change"]);
|
|
5
|
+
const INTEGER_FIELDS = [
|
|
6
|
+
"turn",
|
|
7
|
+
"step",
|
|
8
|
+
"dropped",
|
|
9
|
+
"kept",
|
|
10
|
+
"candidates",
|
|
11
|
+
"conditions",
|
|
12
|
+
"satisfied",
|
|
13
|
+
"unsatisfied",
|
|
14
|
+
"insufficient",
|
|
15
|
+
"throughSeq",
|
|
16
|
+
"evidenceCount",
|
|
17
|
+
"inputTokens",
|
|
18
|
+
"outputTokens",
|
|
19
|
+
"requestBytes",
|
|
20
|
+
"backendAttempts"
|
|
21
|
+
];
|
|
22
|
+
function sanitizeLogRow(value) {
|
|
23
|
+
if (!isObject(value) || typeof value.kind !== "string" || !KINDS.has(value.kind)) return void 0;
|
|
24
|
+
const row = { kind: value.kind };
|
|
25
|
+
for (const [key, limit] of [
|
|
26
|
+
["id", 160],
|
|
27
|
+
["tool", 128],
|
|
28
|
+
["suggestion", 16],
|
|
29
|
+
["action", 32],
|
|
30
|
+
["reason", 64],
|
|
31
|
+
["subject", 120],
|
|
32
|
+
["task", 512],
|
|
33
|
+
["command", 512],
|
|
34
|
+
["path", 512],
|
|
35
|
+
["mode", 16],
|
|
36
|
+
["requestId", 160],
|
|
37
|
+
["callId", 160],
|
|
38
|
+
["model", 128]
|
|
39
|
+
]) {
|
|
40
|
+
const text = value[key];
|
|
41
|
+
if (typeof text === "string" && text.trim()) row[key] = redactCredentials(text).replace(/\s+/g, " ").trim().slice(0, limit);
|
|
42
|
+
}
|
|
43
|
+
for (const key of INTEGER_FIELDS) {
|
|
44
|
+
const n = value[key];
|
|
45
|
+
if (Number.isSafeInteger(n) && n >= 0) row[key] = n;
|
|
46
|
+
}
|
|
47
|
+
for (const key of ["at", "latencyMs", "score", "confidence"]) {
|
|
48
|
+
const n = value[key];
|
|
49
|
+
if (typeof n === "number" && Number.isFinite(n) && n >= 0 && (key !== "confidence" || n <= 1)) {
|
|
50
|
+
row[key] = key === "at" ? n : Math.round(n * 100) / 100;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
row.outcome = typeof value.outcome === "string" ? value.outcome.slice(0, 32) : "";
|
|
54
|
+
if (typeof value.applied === "boolean") row.applied = value.applied;
|
|
55
|
+
if (typeof value.steered === "boolean") row.steered = value.steered;
|
|
56
|
+
if (value.lowConfidence === true) row.lowConfidence = true;
|
|
57
|
+
if (typeof value.trigger === "string" && TRIGGERS.has(value.trigger)) row.trigger = value.trigger;
|
|
58
|
+
if (Array.isArray(value.tools)) {
|
|
59
|
+
const names = value.tools.filter((name) => typeof name === "string" && Boolean(name.trim())).slice(0, 64).map((name) => redactCredentials(name.trim()).slice(0, 128));
|
|
60
|
+
if (names.length) row.tools = names;
|
|
61
|
+
}
|
|
62
|
+
return row;
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
sanitizeLogRow
|
|
66
|
+
};
|
package/lib/host/manager-api.js
CHANGED
|
@@ -111,7 +111,9 @@ function createManagerRoutes({ path, sessions, backend, logStore, analyze, listR
|
|
|
111
111
|
}
|
|
112
112
|
}),
|
|
113
113
|
// One newest-first page of this session's full persisted decision rows, for
|
|
114
|
-
// the analysis tab's paginated log.
|
|
114
|
+
// the analysis tab's paginated log. `lowOnly=1` filters to low self-check
|
|
115
|
+
// scores before paging, so a single low score in a long session is one click
|
|
116
|
+
// away instead of a manual scan. Rows carry the same panel fields as /log,
|
|
115
117
|
// including the gate's already-redacted and length-bounded command/path, so
|
|
116
118
|
// this exposes nothing the recent-rows route does not already.
|
|
117
119
|
route("logrows", {
|
|
@@ -121,8 +123,9 @@ function createManagerRoutes({ path, sessions, backend, logStore, analyze, listR
|
|
|
121
123
|
const params = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
122
124
|
const page = Number.parseInt(params.get("page") ?? "1", 10);
|
|
123
125
|
const pageSize = Number.parseInt(params.get("pageSize") ?? "100", 10);
|
|
126
|
+
const lowOnly = params.get("lowOnly") === "1";
|
|
124
127
|
try {
|
|
125
|
-
return await listRows(id, { page: Number.isNaN(page) ? 1 : page, pageSize: Number.isNaN(pageSize) ? 100 : pageSize });
|
|
128
|
+
return await listRows(id, { page: Number.isNaN(page) ? 1 : page, pageSize: Number.isNaN(pageSize) ? 100 : pageSize, ...lowOnly ? { lowOnly } : {} });
|
|
126
129
|
} catch (error) {
|
|
127
130
|
if (error?.code === "BAD_REQUEST") throw error;
|
|
128
131
|
throw codedError("log rows failed", "INTERNAL");
|
package/lib/host/narrowing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createEvaluationTrace } from "./evaluation-trace.js";
|
|
1
2
|
import { taskExcerpt } from "./excerpt.js";
|
|
2
3
|
import { errorReason, isObject, isProbability } from "./types.js";
|
|
3
4
|
const narrowApplied = (record) => {
|
|
@@ -114,14 +115,14 @@ function createNarrowing({
|
|
|
114
115
|
} catch {
|
|
115
116
|
}
|
|
116
117
|
};
|
|
117
|
-
const evaluateSplit = async (state, tools, signal) => {
|
|
118
|
+
const evaluateSplit = async (evaluate2, state, tools, signal) => {
|
|
118
119
|
const answers = /* @__PURE__ */ Object.create(null);
|
|
119
120
|
let anyOk = false;
|
|
120
121
|
for (let i = 0; i < tools.length; i += concurrency) {
|
|
121
122
|
const group = tools.slice(i, i + concurrency);
|
|
122
123
|
const settled = await Promise.all(group.map(async (tool) => {
|
|
123
124
|
try {
|
|
124
|
-
const reply = await
|
|
125
|
+
const reply = await evaluate2(relevanceQuestions(state, [tool]), { signal });
|
|
125
126
|
const answer = reply?.answers?.[tool.name];
|
|
126
127
|
return isObject(answer) ? [tool.name, answer] : void 0;
|
|
127
128
|
} catch {
|
|
@@ -161,28 +162,29 @@ function createNarrowing({
|
|
|
161
162
|
const effectiveMode = overCap ? "observe" : mode;
|
|
162
163
|
const judged = overCap ? optional.slice(0, cap) : optional;
|
|
163
164
|
const judgedNames = judged.map((tool) => tool.name);
|
|
165
|
+
const trace = createEvaluationTrace(evaluate);
|
|
164
166
|
let result = null;
|
|
165
167
|
if (batchMode === "split" || adaptiveSplit) {
|
|
166
|
-
result = await evaluateSplit(turn?.state, judged, turn?.signal);
|
|
168
|
+
result = await evaluateSplit(trace.evaluate, turn?.state, judged, turn?.signal);
|
|
167
169
|
if (!result) {
|
|
168
|
-
record(turn, "error", { reason: "unreachable", mode: effectiveMode });
|
|
170
|
+
record(turn, "error", { ...trace.detail(), reason: "unreachable", mode: effectiveMode });
|
|
169
171
|
return { ...skip, keep: names, mode: effectiveMode };
|
|
170
172
|
}
|
|
171
173
|
} else {
|
|
172
174
|
try {
|
|
173
|
-
const reply = await evaluate(relevanceQuestions(turn?.state, judged), { signal: turn?.signal });
|
|
175
|
+
const reply = await trace.evaluate(relevanceQuestions(turn?.state, judged), { signal: turn?.signal });
|
|
174
176
|
result = reply;
|
|
175
177
|
} catch (error) {
|
|
176
178
|
const reason = errorReason(error);
|
|
177
179
|
if (batchMode === "auto" && reason === "http-error") {
|
|
178
180
|
adaptiveSplit = true;
|
|
179
|
-
result = await evaluateSplit(turn?.state, judged, turn?.signal);
|
|
181
|
+
result = await evaluateSplit(trace.evaluate, turn?.state, judged, turn?.signal);
|
|
180
182
|
if (!result) {
|
|
181
|
-
record(turn, "error", { reason, mode: effectiveMode });
|
|
183
|
+
record(turn, "error", { ...trace.detail(), reason, mode: effectiveMode });
|
|
182
184
|
return { ...skip, keep: names, mode: effectiveMode };
|
|
183
185
|
}
|
|
184
186
|
} else {
|
|
185
|
-
record(turn, "error", { reason, mode: effectiveMode });
|
|
187
|
+
record(turn, "error", { ...trace.detail(), reason, mode: effectiveMode });
|
|
186
188
|
return { ...skip, keep: names, mode: effectiveMode };
|
|
187
189
|
}
|
|
188
190
|
}
|
|
@@ -198,7 +200,7 @@ function createNarrowing({
|
|
|
198
200
|
detail.reason = "too-many-candidates";
|
|
199
201
|
detail.candidates = optional.length;
|
|
200
202
|
} else if (dropped > 0 && !trustworthy) detail.reason = "low-keep";
|
|
201
|
-
record(turn, dropped > 0 ? "applied" : "ok", detail);
|
|
203
|
+
record(turn, dropped > 0 ? "applied" : "ok", { ...detail, ...trace.detail() });
|
|
202
204
|
return { evaluated: true, applied, keep, drop, mode: effectiveMode, overCap, trustworthy };
|
|
203
205
|
}
|
|
204
206
|
};
|
package/lib/host/self-check.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
import { createEvaluationTrace } from "./evaluation-trace.js";
|
|
1
2
|
import { taskExcerpt } from "./excerpt.js";
|
|
2
3
|
import { createSteerOnce } from "./steer-once.js";
|
|
3
4
|
import { errorReason, isProbability } from "./types.js";
|
|
4
5
|
const MAX_OUTPUT = 24 * 1024;
|
|
6
|
+
const ANSWER_TAIL = 8 * 1024;
|
|
7
|
+
const OMITTED = "\n\n\u2026\uFF08\u4E2D\u95F4\u5185\u5BB9\u5DF2\u7701\u7565\uFF09\u2026\n\n";
|
|
5
8
|
const DEFAULT_RUBRIC = Object.freeze([
|
|
6
9
|
"\u660E\u663E\u7B54\u975E\u6240\u95EE\uFF0C\u6216\u5B58\u5728\u4E25\u91CD\u9057\u6F0F / \u81EA\u76F8\u77DB\u76FE",
|
|
7
10
|
"\u57FA\u672C\u7B54\u5230\u95EE\u9898\uFF0C\u4F46\u6709\u53EF\u5BDF\u89C9\u7684\u9057\u6F0F\u6216\u4E0D\u4E25\u8C28",
|
|
8
11
|
"\u5B8C\u6574\u3001\u51C6\u786E\u3001\u65E0\u81EA\u76F8\u77DB\u76FE\u5730\u56DE\u5E94\u4E86\u8BF7\u6C42"
|
|
9
12
|
]);
|
|
10
13
|
const STEER_TEXT = "\u672C\u56DE\u5408\u81EA\u68C0\u5F97\u5206\u504F\u4F4E\uFF1A\u8BF7\u590D\u6838\u662F\u5426\u7B54\u5230\u4E86\u7528\u6237\u7684\u95EE\u9898\u3001\u6709\u65E0\u9057\u6F0F\u6216\u81EA\u76F8\u77DB\u76FE\uFF0C\u5E76\u5728\u9700\u8981\u65F6\u8865\u5B8C\u3002";
|
|
14
|
+
function answerExcerpt(output) {
|
|
15
|
+
const text = String(output);
|
|
16
|
+
if (text.length <= MAX_OUTPUT) return text;
|
|
17
|
+
return `${text.slice(0, MAX_OUTPUT - ANSWER_TAIL)}${OMITTED}${text.slice(-ANSWER_TAIL)}`;
|
|
18
|
+
}
|
|
11
19
|
function boundedRubric(rubric) {
|
|
12
20
|
if (!Array.isArray(rubric)) return DEFAULT_RUBRIC;
|
|
13
21
|
const levels = rubric.filter((item) => typeof item === "string" && Boolean(item.trim()) && item.length <= 256).slice(0, 10);
|
|
@@ -20,7 +28,12 @@ function scoreQuestion(output, rubric = DEFAULT_RUBRIC, context) {
|
|
|
20
28
|
// against nothing and cannot detect an off-topic reply.
|
|
21
29
|
...context?.request ? { request: context.request } : {},
|
|
22
30
|
...context?.background?.length ? { background: context.background } : {},
|
|
23
|
-
answer:
|
|
31
|
+
answer: answerExcerpt(output),
|
|
32
|
+
// Short follow-ups ("继续", "补上", "按这个标准重新打分") have no standalone
|
|
33
|
+
// meaning; the task they continue is in background. Saying so explicitly
|
|
34
|
+
// stops the model from treating the follow-up as a new, empty request and
|
|
35
|
+
// from borrowing an unrelated earlier task as this turn's goal.
|
|
36
|
+
...context ? { note: "request \u662F\u672C\u56DE\u5408\u7528\u6237\u8BF7\u6C42\uFF1B\u82E5\u5B83\u53EA\u662F\u300C\u7EE7\u7EED\u300D\u300C\u8865\u4E0A\u300D\u8FD9\u7C7B\u77ED\u8FFD\u95EE\uFF0C\u8BF7\u7ED3\u5408 background \u4E2D\u627F\u63A5\u7684\u4EFB\u52A1\u5224\u65AD\uFF0C\u4E0D\u8981\u628A background \u91CC\u7684\u65E7\u4EFB\u52A1\u5F53\u6210\u65B0\u7684\u8981\u6C42\u3002background \u53EA\u662F\u80CC\u666F\uFF0C\u4E0D\u662F\u672C\u8F6E\u7684\u65B0\u8981\u6C42\u3002answer \u662F\u672C\u56DE\u5408\u6700\u7EC8\u56DE\u590D\u3002" } : {}
|
|
24
37
|
},
|
|
25
38
|
questions: { quality: {
|
|
26
39
|
type: "score",
|
|
@@ -78,16 +91,17 @@ function createSelfCheck({ evaluate, sessions, steer, settings, minConfidence =
|
|
|
78
91
|
const taskDetail = task ? { task } : {};
|
|
79
92
|
if (Number.isSafeInteger(turn?.turn) && turn?.turn >= 0) taskDetail.turn = turn?.turn;
|
|
80
93
|
const { mode, rubric, lowScoreThreshold, minConfidence: confidenceFloor, trendMinConfidence, trendRun, trendSevereRun } = await resolveSettings();
|
|
94
|
+
const trace = createEvaluationTrace(evaluate);
|
|
81
95
|
let answer;
|
|
82
96
|
try {
|
|
83
|
-
const result = await evaluate(scoreQuestion(output, rubric, turn?.context), { signal: turn?.signal });
|
|
97
|
+
const result = await trace.evaluate(scoreQuestion(output, rubric, turn?.context), { signal: turn?.signal });
|
|
84
98
|
answer = result?.answers?.quality;
|
|
85
99
|
} catch (error) {
|
|
86
|
-
record(turn, "error", { ...taskDetail, reason: errorReason(error) });
|
|
100
|
+
record(turn, "error", { ...taskDetail, ...trace.detail(), reason: errorReason(error) });
|
|
87
101
|
return { evaluated: false, lowScore: false, steered: false };
|
|
88
102
|
}
|
|
89
103
|
if (!answer || answer.type !== "score" || typeof answer.score !== "number" || !Number.isFinite(answer.score)) {
|
|
90
|
-
record(turn, "error", { ...taskDetail, reason: "invalid-response" });
|
|
104
|
+
record(turn, "error", { ...taskDetail, ...trace.detail(), reason: "invalid-response" });
|
|
91
105
|
return { evaluated: false, lowScore: false, steered: false };
|
|
92
106
|
}
|
|
93
107
|
const confidence = typeof answer.confidence === "number" && Number.isFinite(answer.confidence) ? answer.confidence : void 0;
|
|
@@ -95,10 +109,13 @@ function createSelfCheck({ evaluate, sessions, steer, settings, minConfidence =
|
|
|
95
109
|
const lowScore = answer.score <= lowScoreThreshold;
|
|
96
110
|
const trend = { confident: lowScore && confidence !== void 0 && confidence >= trendMinConfidence, trendRun, trendSevereRun };
|
|
97
111
|
if (lowScore && confidence !== void 0 && !trend.confident) detail.lowConfidence = true;
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
if (!lowScore) {
|
|
113
|
+
record(turn, "ok", { score: answer.score, ...detail, ...trace.detail(), steered: false }, trend);
|
|
114
|
+
return { evaluated: true, lowScore: false, steered: false, score: answer.score };
|
|
115
|
+
}
|
|
100
116
|
const confident = confidence !== void 0 && confidence >= confidenceFloor;
|
|
101
117
|
const steered = mode === "steer" && confident && typeof steer === "function" && turn?.agent ? steerOnce.attempt(turn, () => steer(turn.agent, STEER_TEXT)) : false;
|
|
118
|
+
record(turn, "low", { score: answer.score, ...detail, ...trace.detail(), steered }, trend);
|
|
102
119
|
return { evaluated: true, lowScore: true, steered, score: answer.score };
|
|
103
120
|
}
|
|
104
121
|
};
|
|
@@ -5,8 +5,9 @@ export type SessionStateOptions = {
|
|
|
5
5
|
onLog?: (entry: SessionSinkRow) => void;
|
|
6
6
|
loadState?: (id: string) => Promise<unknown> | unknown;
|
|
7
7
|
persistState?: (id: string, state: SessionState) => unknown;
|
|
8
|
+
onEnabled?: (id: string, enabled: boolean) => void;
|
|
8
9
|
};
|
|
9
|
-
export declare function createSessionState({ maxSessions, maxLogEntries, onLog, loadState, persistState }?: SessionStateOptions): {
|
|
10
|
+
export declare function createSessionState({ maxSessions, maxLogEntries, onLog, loadState, persistState, onEnabled }?: SessionStateOptions): {
|
|
10
11
|
ensureLoaded(id: unknown): Promise<void>;
|
|
11
12
|
setEnabled(id: unknown, enabled: unknown): void;
|
|
12
13
|
snapshot(id: unknown): SessionSnapshot;
|
|
@@ -15,5 +16,6 @@ export declare function createSessionState({ maxSessions, maxLogEntries, onLog,
|
|
|
15
16
|
recordCheck(id: unknown, outcome: string, trend?: Record<string, unknown>): void;
|
|
16
17
|
recordNarrow(id: unknown, outcome: string, dropped?: number, applied?: boolean): void;
|
|
17
18
|
recordComplete(id: unknown, outcome: string, detail?: Record<string, unknown>): void;
|
|
19
|
+
recordCheckpoint(id: unknown, outcome: string): void;
|
|
18
20
|
log(id: unknown, entry: Record<string, unknown>): void;
|
|
19
21
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { sanitizeLogRow } from "./log-row.js";
|
|
3
3
|
import { advanceRun, severityOf } from "./quality-trend.js";
|
|
4
|
-
const KINDS = ["gate", "check", "narrow", "complete"];
|
|
5
4
|
const SEVERITIES = ["normal", "warn", "severe"];
|
|
6
5
|
function sessionKey(value) {
|
|
7
6
|
if (typeof value !== "string" || !value.trim() || value.length > 512) throw new Error("Invalid session ID");
|
|
@@ -15,15 +14,13 @@ function emptyCounters() {
|
|
|
15
14
|
check: { attempts: 0, failures: 0, low: 0, run: 0, severity: "normal" },
|
|
16
15
|
narrow: { attempts: 0, failures: 0, applied: 0, dropped: 0 },
|
|
17
16
|
complete: { attempts: 0, failures: 0, unsatisfied: 0, satisfied: 0, insufficient: 0, steered: 0 },
|
|
17
|
+
checkpoint: { attempts: 0, failures: 0, concern: 0, uncertain: 0, skipped: 0 },
|
|
18
18
|
log: []
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
function emptyState() {
|
|
22
22
|
return { enabled: true, ...emptyCounters() };
|
|
23
23
|
}
|
|
24
|
-
function safeTool(value) {
|
|
25
|
-
return typeof value === "string" && value.trim() ? value.trim().slice(0, 128) : void 0;
|
|
26
|
-
}
|
|
27
24
|
const toCount = (value) => Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
28
25
|
const asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
29
26
|
function coerceState(raw) {
|
|
@@ -71,12 +68,12 @@ function coerceState(raw) {
|
|
|
71
68
|
state.complete.insufficient = toCount(k.insufficient);
|
|
72
69
|
state.complete.steered = toCount(k.steered);
|
|
73
70
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
const cp = asRecord(record.checkpoint);
|
|
72
|
+
if (cp) for (const key of ["attempts", "failures", "concern", "uncertain", "skipped"]) state.checkpoint[key] = toCount(cp[key]);
|
|
73
|
+
if (Array.isArray(record.log)) state.log = record.log.map(sanitizeLogRow).filter((row) => Boolean(row));
|
|
77
74
|
return state;
|
|
78
75
|
}
|
|
79
|
-
function createSessionState({ maxSessions = 1024, maxLogEntries = 50, onLog, loadState, persistState } = {}) {
|
|
76
|
+
function createSessionState({ maxSessions = 1024, maxLogEntries = 50, onLog, loadState, persistState, onEnabled } = {}) {
|
|
80
77
|
if (!Number.isSafeInteger(maxSessions) || maxSessions < 1) throw new Error("Invalid session limit");
|
|
81
78
|
if (!Number.isSafeInteger(maxLogEntries) || maxLogEntries < 1) throw new Error("Invalid log limit");
|
|
82
79
|
const sessions = /* @__PURE__ */ new Map();
|
|
@@ -142,6 +139,10 @@ function createSessionState({ maxSessions = 1024, maxLogEntries = 50, onLog, loa
|
|
|
142
139
|
if (typeof enabled !== "boolean") throw new Error("Invalid enabled value");
|
|
143
140
|
const state = writable(id);
|
|
144
141
|
state.enabled = enabled;
|
|
142
|
+
try {
|
|
143
|
+
onEnabled?.(sessionKey(id), enabled);
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
145
146
|
bump(sessionKey(id), state);
|
|
146
147
|
},
|
|
147
148
|
snapshot(id) {
|
|
@@ -152,6 +153,7 @@ function createSessionState({ maxSessions = 1024, maxLogEntries = 50, onLog, loa
|
|
|
152
153
|
if (state.check.attempts > 0) value.check = { attempts: state.check.attempts, failures: state.check.failures, low: state.check.low, trend: { run: state.check.run, severity: state.check.severity } };
|
|
153
154
|
if (state.narrow.attempts > 0) value.narrow = { ...state.narrow };
|
|
154
155
|
if (state.complete.attempts > 0) value.complete = { ...state.complete };
|
|
156
|
+
if (state.checkpoint.attempts > 0 || state.checkpoint.skipped > 0) value.checkpoint = { ...state.checkpoint };
|
|
155
157
|
if (state.log.length > 0) value.log = state.log.map((entry) => ({ ...entry }));
|
|
156
158
|
return value;
|
|
157
159
|
},
|
|
@@ -233,48 +235,30 @@ function createSessionState({ maxSessions = 1024, maxLogEntries = 50, onLog, loa
|
|
|
233
235
|
}
|
|
234
236
|
bump(sessionKey(id), state);
|
|
235
237
|
},
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (typeof entry.subject === "string" && entry.subject.trim()) record.subject = redactCredentials(entry.subject).replace(/\s+/g, " ").trim().slice(0, 120);
|
|
247
|
-
if (typeof entry.command === "string" && entry.command.trim()) record.command = entry.command.replace(/\s+/g, " ").trim().slice(0, 512);
|
|
248
|
-
if (typeof entry.path === "string" && entry.path.trim()) record.path = entry.path.replace(/\s+/g, " ").trim().slice(0, 512);
|
|
249
|
-
if (typeof entry.task === "string" && entry.task.trim()) record.task = redactCredentials(entry.task).replace(/\s+/g, " ").trim().slice(0, 512);
|
|
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;
|
|
252
|
-
if (typeof entry.mode === "string" && entry.mode) record.mode = entry.mode.slice(0, 16);
|
|
253
|
-
if (typeof entry.score === "number" && Number.isFinite(entry.score)) record.score = Math.round(entry.score * 100) / 100;
|
|
254
|
-
if (typeof entry.confidence === "number" && Number.isFinite(entry.confidence)) record.confidence = Math.round(entry.confidence * 100) / 100;
|
|
255
|
-
if (Number.isSafeInteger(entry.dropped) && entry.dropped >= 0) record.dropped = entry.dropped;
|
|
256
|
-
if (Number.isSafeInteger(entry.kept) && entry.kept >= 0) record.kept = entry.kept;
|
|
257
|
-
if (typeof entry.applied === "boolean") record.applied = entry.applied;
|
|
258
|
-
if (Number.isSafeInteger(entry.candidates) && entry.candidates >= 0) record.candidates = entry.candidates;
|
|
259
|
-
if (Number.isSafeInteger(entry.conditions) && entry.conditions >= 0) record.conditions = entry.conditions;
|
|
260
|
-
if (Number.isSafeInteger(entry.satisfied) && entry.satisfied >= 0) record.satisfied = entry.satisfied;
|
|
261
|
-
if (Number.isSafeInteger(entry.unsatisfied) && entry.unsatisfied >= 0) record.unsatisfied = entry.unsatisfied;
|
|
262
|
-
if (Number.isSafeInteger(entry.insufficient) && entry.insufficient >= 0) record.insufficient = entry.insufficient;
|
|
263
|
-
if (entry.steered === true) record.steered = true;
|
|
264
|
-
if (entry.lowConfidence === true) record.lowConfidence = true;
|
|
265
|
-
if (Array.isArray(entry.tools)) {
|
|
266
|
-
const names = entry.tools.filter((name) => typeof name === "string" && Boolean(name.trim())).slice(0, 12).map((name) => name.trim().slice(0, 64));
|
|
267
|
-
if (names.length > 0) record.tools = names;
|
|
238
|
+
recordCheckpoint(id, outcome) {
|
|
239
|
+
if (!["ok", "concern", "uncertain", "error", "skipped"].includes(outcome)) throw new Error("Invalid checkpoint outcome");
|
|
240
|
+
const state = writable(id);
|
|
241
|
+
const checkpoint = state.checkpoint;
|
|
242
|
+
if (outcome === "skipped") checkpoint.skipped++;
|
|
243
|
+
else {
|
|
244
|
+
checkpoint.attempts++;
|
|
245
|
+
if (outcome === "error") checkpoint.failures++;
|
|
246
|
+
else if (outcome === "concern") checkpoint.concern++;
|
|
247
|
+
else if (outcome === "uncertain") checkpoint.uncertain++;
|
|
268
248
|
}
|
|
249
|
+
bump(sessionKey(id), state);
|
|
250
|
+
},
|
|
251
|
+
log(id, entry) {
|
|
252
|
+
const sanitized = sanitizeLogRow(entry);
|
|
253
|
+
if (!sanitized) throw new Error("Invalid log entry");
|
|
254
|
+
const fullRow = { ...sanitized, id: randomUUID(), at: Date.now() };
|
|
255
|
+
const record = { ...fullRow };
|
|
256
|
+
if (record.tools) record.tools = record.tools.slice(0, 12).map((name) => name.slice(0, 64));
|
|
269
257
|
const state = writable(id);
|
|
270
258
|
state.log.push(record);
|
|
271
259
|
if (state.log.length > maxLogEntries) state.log.splice(0, state.log.length - maxLogEntries);
|
|
272
260
|
if (sink) {
|
|
273
|
-
const full = { ...
|
|
274
|
-
if (Array.isArray(entry.tools)) {
|
|
275
|
-
const allNames = entry.tools.filter((name) => typeof name === "string" && Boolean(name.trim())).map((name) => name.trim());
|
|
276
|
-
if (allNames.length > 0) full.tools = allNames;
|
|
277
|
-
}
|
|
261
|
+
const full = { ...fullRow, sessionId: sessionKey(id) };
|
|
278
262
|
try {
|
|
279
263
|
sink(full);
|
|
280
264
|
} catch {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createEvaluationTrace } from "./evaluation-trace.js";
|
|
1
2
|
import { taskExcerpt } from "./excerpt.js";
|
|
2
3
|
import { createSteerOnce } from "./steer-once.js";
|
|
3
4
|
import { errorReason, isObject, isProbability } from "./types.js";
|
|
@@ -122,16 +123,17 @@ function createTaskCompletion({ evaluate, sessions, steer, settings, minConfiden
|
|
|
122
123
|
return { ...skip };
|
|
123
124
|
}
|
|
124
125
|
}
|
|
126
|
+
const trace = createEvaluationTrace(evaluate);
|
|
125
127
|
let answers;
|
|
126
128
|
try {
|
|
127
|
-
const result = await evaluate(completionQuestions(conditions, evidence, output, turn?.context), { signal: turn?.signal });
|
|
129
|
+
const result = await trace.evaluate(completionQuestions(conditions, evidence, output, turn?.context), { signal: turn?.signal });
|
|
128
130
|
answers = isObject(result?.answers) ? result.answers : void 0;
|
|
129
131
|
} catch (error) {
|
|
130
|
-
record(turn, "error", { ...taskDetail, reason: errorReason(error) });
|
|
132
|
+
record(turn, "error", { ...taskDetail, ...trace.detail(), reason: errorReason(error) });
|
|
131
133
|
return { ...skip };
|
|
132
134
|
}
|
|
133
135
|
if (!answers) {
|
|
134
|
-
record(turn, "error", { ...taskDetail, reason: "invalid-response" });
|
|
136
|
+
record(turn, "error", { ...taskDetail, ...trace.detail(), reason: "invalid-response" });
|
|
135
137
|
return { ...skip };
|
|
136
138
|
}
|
|
137
139
|
let satisfied = 0, unsatisfied = 0, insufficient = 0;
|
|
@@ -153,7 +155,7 @@ function createTaskCompletion({ evaluate, sessions, steer, settings, minConfiden
|
|
|
153
155
|
const confident = unsatisfied > 0 && unsatisfiedMinConf >= confidenceFloor;
|
|
154
156
|
const steered = mode === "steer" && confident && typeof steer === "function" && turn?.agent ? steerOnce.attempt(turn, () => steer(turn.agent, STEER_TEXT)) : false;
|
|
155
157
|
if (steered) detail.steered = true;
|
|
156
|
-
record(turn, outcome, detail);
|
|
158
|
+
record(turn, outcome, { ...detail, ...trace.detail(), steered });
|
|
157
159
|
return { evaluated: true, steered, satisfied, unsatisfied, insufficient };
|
|
158
160
|
}
|
|
159
161
|
};
|
package/lib/host/types.d.ts
CHANGED
|
@@ -55,13 +55,24 @@ export type ChoiceAnswer = ScoredAnswer & {
|
|
|
55
55
|
};
|
|
56
56
|
export type Answer = NoulAnswer | ScoreAnswer | ChoiceAnswer;
|
|
57
57
|
export type AnswerMap = Record<string, Answer>;
|
|
58
|
+
export type EvaluationTelemetry = {
|
|
59
|
+
requestId: string;
|
|
60
|
+
latencyMs: number;
|
|
61
|
+
requestBytes?: number;
|
|
62
|
+
model?: string;
|
|
63
|
+
inputTokens?: number;
|
|
64
|
+
outputTokens?: number;
|
|
65
|
+
backendAttempts?: number;
|
|
66
|
+
};
|
|
58
67
|
export type EvaluateResult = {
|
|
59
68
|
model: string;
|
|
60
69
|
answers: AnswerMap;
|
|
61
70
|
usage?: {
|
|
62
|
-
input_tokens
|
|
63
|
-
output_tokens
|
|
71
|
+
input_tokens?: number;
|
|
72
|
+
output_tokens?: number;
|
|
64
73
|
};
|
|
74
|
+
/** Local measurement only; never part of the backend wire request. */
|
|
75
|
+
telemetry?: EvaluationTelemetry;
|
|
65
76
|
};
|
|
66
77
|
export type Evaluator = (input: EvaluateInput, options?: {
|
|
67
78
|
signal?: AbortSignal;
|
|
@@ -102,6 +113,19 @@ export type NarrowSettings = {
|
|
|
102
113
|
maxCandidates?: number;
|
|
103
114
|
batchMode?: BatchMode;
|
|
104
115
|
};
|
|
116
|
+
export type CheckpointSettings = {
|
|
117
|
+
enabled?: boolean;
|
|
118
|
+
heartbeatSteps?: number;
|
|
119
|
+
failureThreshold?: number;
|
|
120
|
+
timeoutMs?: number;
|
|
121
|
+
};
|
|
122
|
+
export type CheckpointTrigger = 'heartbeat' | 'unresolved_failure' | 'verification_after_change';
|
|
123
|
+
export type CheckpointOutcome = 'ok' | 'concern' | 'uncertain' | 'error' | 'skipped';
|
|
124
|
+
export type CheckpointCounters = SessionCounters & {
|
|
125
|
+
concern: number;
|
|
126
|
+
uncertain: number;
|
|
127
|
+
skipped: number;
|
|
128
|
+
};
|
|
105
129
|
export type CompleteSettings = {
|
|
106
130
|
mode?: CheckMode;
|
|
107
131
|
minConditions?: number;
|
|
@@ -126,6 +150,7 @@ export type ResolvedConfig = {
|
|
|
126
150
|
checkSettings?: CheckSettings;
|
|
127
151
|
narrowSettings?: NarrowSettings;
|
|
128
152
|
completeSettings?: CompleteSettings;
|
|
153
|
+
checkpointSettings?: CheckpointSettings;
|
|
129
154
|
features?: Features;
|
|
130
155
|
};
|
|
131
156
|
/** What the config page may read back: the API key is reduced to a flag. */
|
|
@@ -139,6 +164,7 @@ export type ConfigView = {
|
|
|
139
164
|
checkSettings?: CheckSettings;
|
|
140
165
|
narrowSettings?: NarrowSettings;
|
|
141
166
|
completeSettings?: CompleteSettings;
|
|
167
|
+
checkpointSettings?: CheckpointSettings;
|
|
142
168
|
features?: Features;
|
|
143
169
|
};
|
|
144
170
|
/** The quality-trend knobs, shared by the live path and the log backtest. */
|
|
@@ -148,7 +174,7 @@ export type TrendConfig = {
|
|
|
148
174
|
trendSevereRun?: number;
|
|
149
175
|
lowScoreThreshold?: number;
|
|
150
176
|
};
|
|
151
|
-
export type LogKind = 'gate' | 'check' | 'narrow' | 'complete';
|
|
177
|
+
export type LogKind = 'gate' | 'check' | 'narrow' | 'complete' | 'checkpoint';
|
|
152
178
|
/**
|
|
153
179
|
* One decision-log row. Fields are optional because rows are also read back
|
|
154
180
|
* from disk: persisted snapshots are untrusted input, and only `kind` is
|
|
@@ -183,6 +209,17 @@ export type LogRow = {
|
|
|
183
209
|
steered?: boolean;
|
|
184
210
|
lowConfidence?: boolean;
|
|
185
211
|
tools?: string[];
|
|
212
|
+
trigger?: CheckpointTrigger;
|
|
213
|
+
throughSeq?: number;
|
|
214
|
+
evidenceCount?: number;
|
|
215
|
+
requestId?: string;
|
|
216
|
+
latencyMs?: number;
|
|
217
|
+
requestBytes?: number;
|
|
218
|
+
model?: string;
|
|
219
|
+
inputTokens?: number;
|
|
220
|
+
outputTokens?: number;
|
|
221
|
+
backendAttempts?: number;
|
|
222
|
+
callId?: string;
|
|
186
223
|
};
|
|
187
224
|
/** The research-log sink record: the panel row plus its session tag. */
|
|
188
225
|
export type SessionSinkRow = LogRow & {
|
|
@@ -226,6 +263,7 @@ export type SessionState = {
|
|
|
226
263
|
check: CheckCounters;
|
|
227
264
|
narrow: NarrowCounters;
|
|
228
265
|
complete: CompleteCounters;
|
|
266
|
+
checkpoint: CheckpointCounters;
|
|
229
267
|
log: LogRow[];
|
|
230
268
|
};
|
|
231
269
|
/** The panel-facing projection; sections without attempts are omitted. */
|
|
@@ -246,6 +284,7 @@ export type SessionSnapshot = {
|
|
|
246
284
|
};
|
|
247
285
|
narrow?: NarrowCounters;
|
|
248
286
|
complete?: CompleteCounters;
|
|
287
|
+
checkpoint?: CheckpointCounters;
|
|
249
288
|
log?: LogRow[];
|
|
250
289
|
};
|
|
251
290
|
/** The counter shape rebuilt from persisted JSONL, without the master switch. */
|
|
@@ -262,6 +301,7 @@ export type SessionsFace = {
|
|
|
262
301
|
recordCheck?: (id: string, outcome: string, trend?: Record<string, unknown>) => void;
|
|
263
302
|
recordNarrow?: (id: string, outcome: string, dropped: number, applied: boolean) => void;
|
|
264
303
|
recordComplete?: (id: string, outcome: string, detail?: Record<string, unknown>) => void;
|
|
304
|
+
recordCheckpoint?: (id: string, outcome: CheckpointOutcome) => void;
|
|
265
305
|
ensureLoaded?: (id: string) => Promise<unknown>;
|
|
266
306
|
setEnabled?: (id: string, enabled: boolean) => void;
|
|
267
307
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-decision-layer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.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",
|