dsh-decision-layer 0.7.1 → 0.7.2

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.
Files changed (57) hide show
  1. package/README.en.md +8 -8
  2. package/README.md +10 -10
  3. package/lib/client.js +39 -14
  4. package/lib/host/backend.d.ts +15 -0
  5. package/lib/host/backend.js +149 -0
  6. package/lib/host/config-store.d.ts +12 -0
  7. package/lib/host/config-store.js +254 -0
  8. package/lib/host/danger-gate.d.ts +45 -0
  9. package/lib/host/danger-gate.js +196 -0
  10. package/lib/host/excerpt.d.ts +25 -0
  11. package/lib/host/excerpt.js +62 -0
  12. package/lib/host/index.d.ts +6 -0
  13. package/lib/host/index.js +290 -0
  14. package/lib/host/log-analyze.d.ts +121 -0
  15. package/lib/host/log-analyze.js +254 -0
  16. package/lib/host/log-read.d.ts +4 -0
  17. package/lib/host/log-read.js +34 -0
  18. package/lib/host/log-store.d.ts +15 -0
  19. package/lib/host/log-store.js +98 -0
  20. package/lib/host/loop-guard.d.ts +10 -0
  21. package/lib/host/loop-guard.js +52 -0
  22. package/lib/host/manager-api.d.ts +20 -0
  23. package/lib/host/manager-api.js +174 -0
  24. package/lib/host/narrowing.d.ts +48 -0
  25. package/lib/host/narrowing.js +213 -0
  26. package/lib/host/quality-trend.d.ts +14 -0
  27. package/lib/host/quality-trend.js +22 -0
  28. package/lib/host/self-check.d.ts +27 -0
  29. package/lib/host/self-check.js +104 -0
  30. package/lib/host/session-state.d.ts +19 -0
  31. package/lib/host/session-state.js +288 -0
  32. package/lib/host/state-store.d.ts +14 -0
  33. package/lib/host/state-store.js +109 -0
  34. package/lib/host/steer-once.d.ts +13 -0
  35. package/lib/host/steer-once.js +25 -0
  36. package/lib/host/task-completion.d.ts +29 -0
  37. package/lib/host/task-completion.js +148 -0
  38. package/lib/host/types.d.ts +343 -0
  39. package/lib/host/types.js +11 -0
  40. package/package.json +17 -10
  41. package/service/.gitkeep +0 -0
  42. package/service/backend.mjs +0 -127
  43. package/service/config-store.mjs +0 -311
  44. package/service/danger-gate.mjs +0 -182
  45. package/service/dsh.mjs +0 -288
  46. package/service/log-analyze.mjs +0 -257
  47. package/service/log-read.mjs +0 -27
  48. package/service/log-store.mjs +0 -82
  49. package/service/loop-guard.mjs +0 -51
  50. package/service/manager-api.mjs +0 -137
  51. package/service/narrowing.mjs +0 -226
  52. package/service/quality-trend.mjs +0 -29
  53. package/service/self-check.mjs +0 -131
  54. package/service/session-state.mjs +0 -260
  55. package/service/state-store.mjs +0 -100
  56. package/service/steer-once.mjs +0 -25
  57. package/service/task-completion.mjs +0 -163
@@ -0,0 +1,22 @@
1
+ const DEFAULT_TREND_MIN_CONFIDENCE = 0.6;
2
+ const DEFAULT_TREND_RUN = 3;
3
+ const DEFAULT_TREND_SEVERE_RUN = 5;
4
+ function advanceRun(run, sample) {
5
+ const current = Number.isSafeInteger(run) && run >= 0 ? run : 0;
6
+ if (!sample || sample.lowScore !== true) return 0;
7
+ if (sample.confident === true) return current + 1;
8
+ return current;
9
+ }
10
+ function severityOf(run, { trendRun = DEFAULT_TREND_RUN, trendSevereRun = DEFAULT_TREND_SEVERE_RUN } = {}) {
11
+ const value = Number.isSafeInteger(run) && run >= 0 ? run : 0;
12
+ if (value >= trendSevereRun) return "severe";
13
+ if (value >= trendRun) return "warn";
14
+ return "normal";
15
+ }
16
+ export {
17
+ DEFAULT_TREND_MIN_CONFIDENCE,
18
+ DEFAULT_TREND_RUN,
19
+ DEFAULT_TREND_SEVERE_RUN,
20
+ advanceRun,
21
+ severityOf
22
+ };
@@ -0,0 +1,27 @@
1
+ import type { AgentLike, CheckSettings, EvaluateInput, Evaluator, SessionsFace } from './types.js';
2
+ export declare const DEFAULT_RUBRIC: readonly string[];
3
+ export declare function scoreQuestion(output: unknown, rubric?: readonly string[]): EvaluateInput;
4
+ export declare const DEFAULT_MIN_CONFIDENCE = 0.4;
5
+ export type CheckTurn = {
6
+ agent?: AgentLike;
7
+ turn?: number;
8
+ signal?: AbortSignal;
9
+ output?: unknown;
10
+ userText?: unknown;
11
+ };
12
+ export type CheckResult = {
13
+ evaluated: boolean;
14
+ lowScore: boolean;
15
+ steered: boolean;
16
+ score?: number;
17
+ };
18
+ export type SelfCheckOptions = {
19
+ evaluate?: Evaluator;
20
+ sessions?: SessionsFace;
21
+ steer?: (agent: AgentLike, text: string) => unknown;
22
+ settings?: CheckSettings | (() => Promise<CheckSettings | undefined>);
23
+ minConfidence?: number;
24
+ };
25
+ export declare function createSelfCheck({ evaluate, sessions, steer, settings, minConfidence }?: SelfCheckOptions): {
26
+ review(turn: CheckTurn | undefined): Promise<CheckResult>;
27
+ };
@@ -0,0 +1,104 @@
1
+ import { taskExcerpt } from "./excerpt.js";
2
+ import { createSteerOnce } from "./steer-once.js";
3
+ import { errorReason, isProbability } from "./types.js";
4
+ const MAX_OUTPUT = 24 * 1024;
5
+ const DEFAULT_RUBRIC = Object.freeze([
6
+ "\u660E\u663E\u7B54\u975E\u6240\u95EE\uFF0C\u6216\u5B58\u5728\u4E25\u91CD\u9057\u6F0F / \u81EA\u76F8\u77DB\u76FE",
7
+ "\u57FA\u672C\u7B54\u5230\u95EE\u9898\uFF0C\u4F46\u6709\u53EF\u5BDF\u89C9\u7684\u9057\u6F0F\u6216\u4E0D\u4E25\u8C28",
8
+ "\u5B8C\u6574\u3001\u51C6\u786E\u3001\u65E0\u81EA\u76F8\u77DB\u76FE\u5730\u56DE\u5E94\u4E86\u8BF7\u6C42"
9
+ ]);
10
+ 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";
11
+ function boundedRubric(rubric) {
12
+ if (!Array.isArray(rubric)) return DEFAULT_RUBRIC;
13
+ const levels = rubric.filter((item) => typeof item === "string" && Boolean(item.trim()) && item.length <= 256).slice(0, 10);
14
+ return levels.length >= 2 ? levels : DEFAULT_RUBRIC;
15
+ }
16
+ function scoreQuestion(output, rubric = DEFAULT_RUBRIC) {
17
+ return {
18
+ state: { answer: String(output).slice(0, MAX_OUTPUT) },
19
+ questions: { quality: {
20
+ type: "score",
21
+ 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",
22
+ criteria: boundedRubric(rubric)
23
+ } }
24
+ };
25
+ }
26
+ const DEFAULT_MIN_CONFIDENCE = 0.4;
27
+ function createSelfCheck({ evaluate, sessions, steer, settings, minConfidence = DEFAULT_MIN_CONFIDENCE } = {}) {
28
+ if (typeof evaluate !== "function") throw new Error("Self-check requires an evaluator");
29
+ const steerOnce = createSteerOnce();
30
+ const resolveSettings = async () => {
31
+ let raw = {};
32
+ try {
33
+ raw = (typeof settings === "function" ? await settings() : settings) ?? {};
34
+ } catch {
35
+ raw = {};
36
+ }
37
+ const mode = raw?.mode === "steer" ? "steer" : "observe";
38
+ const lowScoreThreshold = Number.isSafeInteger(raw?.lowScoreThreshold) && raw.lowScoreThreshold >= 0 ? raw.lowScoreThreshold : 1;
39
+ const confidenceFloor = isProbability(raw?.minConfidence) ? raw.minConfidence : minConfidence;
40
+ const trendMinConfidence = typeof raw?.trendMinConfidence === "number" && Number.isFinite(raw.trendMinConfidence) && raw.trendMinConfidence >= 0 && raw.trendMinConfidence <= 1 ? raw.trendMinConfidence : 0.6;
41
+ const trendRun = Number.isSafeInteger(raw?.trendRun) && raw.trendRun >= 2 ? raw.trendRun : 3;
42
+ const trendSevereRun = Number.isSafeInteger(raw?.trendSevereRun) && raw.trendSevereRun > trendRun ? raw.trendSevereRun : Math.max(trendRun + 1, 5);
43
+ return { mode, rubric: boundedRubric(raw?.rubric), lowScoreThreshold, minConfidence: confidenceFloor, trendMinConfidence, trendRun, trendSevereRun };
44
+ };
45
+ const record = (turn, outcome, detail = {}, trend) => {
46
+ const id = turn?.agent?.session?.id;
47
+ if (!sessions || typeof id !== "string" || typeof sessions.recordCheck !== "function") return;
48
+ try {
49
+ sessions.recordCheck(id, outcome, trend);
50
+ } catch {
51
+ }
52
+ if (typeof sessions.log !== "function") return;
53
+ try {
54
+ sessions.log(id, { kind: "check", outcome, ...detail });
55
+ } catch {
56
+ }
57
+ };
58
+ return {
59
+ async review(turn) {
60
+ const output = typeof turn?.output === "string" ? turn.output.trim() : "";
61
+ if (!output) return { evaluated: false, lowScore: false, steered: false };
62
+ const id = turn?.agent?.session?.id;
63
+ if (sessions && typeof id === "string") {
64
+ try {
65
+ if (sessions.snapshot(id).enabled === false) return { evaluated: false, lowScore: false, steered: false };
66
+ } catch {
67
+ return { evaluated: false, lowScore: false, steered: false };
68
+ }
69
+ }
70
+ const task = taskExcerpt(turn?.userText);
71
+ const taskDetail = task ? { task } : {};
72
+ if (Number.isSafeInteger(turn?.turn) && turn?.turn >= 0) taskDetail.turn = turn?.turn;
73
+ const { mode, rubric, lowScoreThreshold, minConfidence: confidenceFloor, trendMinConfidence, trendRun, trendSevereRun } = await resolveSettings();
74
+ let answer;
75
+ try {
76
+ const result = await evaluate(scoreQuestion(output, rubric), { signal: turn?.signal });
77
+ answer = result?.answers?.quality;
78
+ } catch (error) {
79
+ record(turn, "error", { ...taskDetail, reason: errorReason(error) });
80
+ return { evaluated: false, lowScore: false, steered: false };
81
+ }
82
+ if (!answer || answer.type !== "score" || typeof answer.score !== "number" || !Number.isFinite(answer.score)) {
83
+ record(turn, "error", { ...taskDetail, reason: "invalid-response" });
84
+ return { evaluated: false, lowScore: false, steered: false };
85
+ }
86
+ const confidence = typeof answer.confidence === "number" && Number.isFinite(answer.confidence) ? answer.confidence : void 0;
87
+ const detail = confidence === void 0 ? { ...taskDetail } : { ...taskDetail, confidence };
88
+ const lowScore = answer.score <= lowScoreThreshold;
89
+ const trend = { confident: lowScore && confidence !== void 0 && confidence >= trendMinConfidence, trendRun, trendSevereRun };
90
+ if (lowScore && confidence !== void 0 && !trend.confident) detail.lowConfidence = true;
91
+ record(turn, lowScore ? "low" : "ok", { score: answer.score, ...detail }, trend);
92
+ if (!lowScore) return { evaluated: true, lowScore: false, steered: false, score: answer.score };
93
+ const confident = confidence !== void 0 && confidence >= confidenceFloor;
94
+ const steered = mode === "steer" && confident && typeof steer === "function" && turn?.agent ? steerOnce.attempt(turn, () => steer(turn.agent, STEER_TEXT)) : false;
95
+ return { evaluated: true, lowScore: true, steered, score: answer.score };
96
+ }
97
+ };
98
+ }
99
+ export {
100
+ DEFAULT_MIN_CONFIDENCE,
101
+ DEFAULT_RUBRIC,
102
+ createSelfCheck,
103
+ scoreQuestion
104
+ };
@@ -0,0 +1,19 @@
1
+ import type { SessionSinkRow, SessionSnapshot, SessionState } from './types.js';
2
+ export type SessionStateOptions = {
3
+ maxSessions?: number;
4
+ maxLogEntries?: number;
5
+ onLog?: (entry: SessionSinkRow) => void;
6
+ loadState?: (id: string) => Promise<unknown> | unknown;
7
+ persistState?: (id: string, state: SessionState) => unknown;
8
+ };
9
+ export declare function createSessionState({ maxSessions, maxLogEntries, onLog, loadState, persistState }?: SessionStateOptions): {
10
+ ensureLoaded(id: unknown): Promise<void>;
11
+ setEnabled(id: unknown, enabled: unknown): void;
12
+ snapshot(id: unknown): SessionSnapshot;
13
+ record(id: unknown, kind: string, outcome: string): void;
14
+ recordActual(id: unknown, outcome: string): void;
15
+ recordCheck(id: unknown, outcome: string, trend?: Record<string, unknown>): void;
16
+ recordNarrow(id: unknown, outcome: string, dropped?: number, applied?: boolean): void;
17
+ recordComplete(id: unknown, outcome: string, detail?: Record<string, unknown>): void;
18
+ log(id: unknown, entry: Record<string, unknown>): void;
19
+ };
@@ -0,0 +1,288 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { redactCredentials } from "./excerpt.js";
3
+ import { advanceRun, severityOf } from "./quality-trend.js";
4
+ const KINDS = ["gate", "check", "narrow", "complete"];
5
+ const SEVERITIES = ["normal", "warn", "severe"];
6
+ function sessionKey(value) {
7
+ if (typeof value !== "string" || !value.trim() || value.length > 512) throw new Error("Invalid session ID");
8
+ return value.trim();
9
+ }
10
+ function emptyCounters() {
11
+ return {
12
+ attempts: 0,
13
+ failures: 0,
14
+ gate: { attempts: 0, failures: 0, ask: 0, deny: 0, allow: 0, actual: { allow: 0, deny: 0, error: 0 } },
15
+ check: { attempts: 0, failures: 0, low: 0, run: 0, severity: "normal" },
16
+ narrow: { attempts: 0, failures: 0, applied: 0, dropped: 0 },
17
+ complete: { attempts: 0, failures: 0, unsatisfied: 0, satisfied: 0, insufficient: 0, steered: 0 },
18
+ log: []
19
+ };
20
+ }
21
+ function emptyState() {
22
+ return { enabled: true, ...emptyCounters() };
23
+ }
24
+ function safeTool(value) {
25
+ return typeof value === "string" && value.trim() ? value.trim().slice(0, 128) : void 0;
26
+ }
27
+ const toCount = (value) => Number.isSafeInteger(value) && value >= 0 ? value : 0;
28
+ const asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
29
+ function coerceState(raw) {
30
+ const state = emptyState();
31
+ const record = asRecord(raw);
32
+ if (!record) return state;
33
+ if (typeof record.enabled === "boolean") state.enabled = record.enabled;
34
+ state.attempts = toCount(record.attempts);
35
+ state.failures = toCount(record.failures);
36
+ const g = asRecord(record.gate);
37
+ if (g) {
38
+ state.gate.attempts = toCount(g.attempts);
39
+ state.gate.failures = toCount(g.failures);
40
+ state.gate.ask = toCount(g.ask);
41
+ state.gate.deny = toCount(g.deny);
42
+ state.gate.allow = toCount(g.allow);
43
+ const a = asRecord(g.actual);
44
+ if (a) {
45
+ state.gate.actual.allow = toCount(a.allow);
46
+ state.gate.actual.deny = toCount(a.deny);
47
+ state.gate.actual.error = toCount(a.error);
48
+ }
49
+ }
50
+ const c = asRecord(record.check);
51
+ if (c) {
52
+ state.check.attempts = toCount(c.attempts);
53
+ state.check.failures = toCount(c.failures);
54
+ state.check.low = toCount(c.low);
55
+ state.check.run = toCount(c.run);
56
+ state.check.severity = typeof c.severity === "string" && SEVERITIES.includes(c.severity) ? c.severity : "normal";
57
+ }
58
+ const n = asRecord(record.narrow);
59
+ if (n) {
60
+ state.narrow.attempts = toCount(n.attempts);
61
+ state.narrow.failures = toCount(n.failures);
62
+ state.narrow.applied = toCount(n.applied);
63
+ state.narrow.dropped = toCount(n.dropped);
64
+ }
65
+ const k = asRecord(record.complete);
66
+ if (k) {
67
+ state.complete.attempts = toCount(k.attempts);
68
+ state.complete.failures = toCount(k.failures);
69
+ state.complete.unsatisfied = toCount(k.unsatisfied);
70
+ state.complete.satisfied = toCount(k.satisfied);
71
+ state.complete.insufficient = toCount(k.insufficient);
72
+ state.complete.steered = toCount(k.steered);
73
+ }
74
+ if (Array.isArray(record.log)) {
75
+ state.log = record.log.filter((entry) => Boolean(asRecord(entry)) && typeof entry.kind === "string").map((entry) => ({ ...entry }));
76
+ }
77
+ return state;
78
+ }
79
+ function createSessionState({ maxSessions = 1024, maxLogEntries = 50, onLog, loadState, persistState } = {}) {
80
+ if (!Number.isSafeInteger(maxSessions) || maxSessions < 1) throw new Error("Invalid session limit");
81
+ if (!Number.isSafeInteger(maxLogEntries) || maxLogEntries < 1) throw new Error("Invalid log limit");
82
+ const sessions = /* @__PURE__ */ new Map();
83
+ const sink = typeof onLog === "function" ? onLog : void 0;
84
+ const load = typeof loadState === "function" ? loadState : void 0;
85
+ const persist = typeof persistState === "function" ? persistState : void 0;
86
+ const loaded = /* @__PURE__ */ new Map();
87
+ const bump = (key, state) => {
88
+ if (persist) {
89
+ try {
90
+ persist(key, state);
91
+ } catch {
92
+ }
93
+ }
94
+ };
95
+ const writable = (id) => {
96
+ const key = sessionKey(id);
97
+ let state = sessions.get(key);
98
+ if (!state) {
99
+ if (sessions.size >= maxSessions) sessions.delete(sessions.keys().next().value);
100
+ state = emptyState();
101
+ sessions.set(key, state);
102
+ }
103
+ return state;
104
+ };
105
+ return {
106
+ // Rehydrate a session's live counters/log from the durable store the first
107
+ // time it is touched this process, before any new event mutates it — so disk
108
+ // history and in-process events never double-count. Memoized per id; a no-op
109
+ // when no loadState was given or the session already has live state. Callers
110
+ // should await this before the first snapshot/record of a resumed session
111
+ // (the plugin does so at the turn's first step and in the panel routes).
112
+ async ensureLoaded(id) {
113
+ if (!load) return;
114
+ let key;
115
+ try {
116
+ key = sessionKey(id);
117
+ } catch {
118
+ return;
119
+ }
120
+ const existing = loaded.get(key);
121
+ if (existing) return existing;
122
+ const promise = (async () => {
123
+ if (sessions.has(key)) return;
124
+ let raw;
125
+ try {
126
+ raw = await load(key);
127
+ } catch {
128
+ raw = void 0;
129
+ }
130
+ if (!raw) return;
131
+ if (sessions.has(key)) return;
132
+ const state = coerceState(raw);
133
+ if (state.log.length > maxLogEntries) state.log.splice(0, state.log.length - maxLogEntries);
134
+ if (sessions.size >= maxSessions) sessions.delete(sessions.keys().next().value);
135
+ sessions.set(key, state);
136
+ })();
137
+ if (loaded.size >= maxSessions) loaded.delete(loaded.keys().next().value);
138
+ loaded.set(key, promise);
139
+ return promise;
140
+ },
141
+ setEnabled(id, enabled) {
142
+ if (typeof enabled !== "boolean") throw new Error("Invalid enabled value");
143
+ const state = writable(id);
144
+ state.enabled = enabled;
145
+ bump(sessionKey(id), state);
146
+ },
147
+ snapshot(id) {
148
+ const stored = sessions.get(sessionKey(id));
149
+ const state = stored ?? emptyState();
150
+ const value = { enabled: state.enabled, hasAutomaticDecisions: state.attempts > 0, attempts: state.attempts, failures: state.failures };
151
+ if (state.gate.attempts > 0) value.gate = { ...state.gate, actual: { ...state.gate.actual } };
152
+ 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
+ if (state.narrow.attempts > 0) value.narrow = { ...state.narrow };
154
+ if (state.complete.attempts > 0) value.complete = { ...state.complete };
155
+ if (state.log.length > 0) value.log = state.log.map((entry) => ({ ...entry }));
156
+ return value;
157
+ },
158
+ record(id, kind, outcome) {
159
+ if (kind !== "gate" || !["allow", "ask", "deny", "error"].includes(outcome)) throw new Error("Invalid automatic decision");
160
+ const state = writable(id);
161
+ state.attempts++;
162
+ state.gate.attempts++;
163
+ if (outcome === "error") {
164
+ state.failures++;
165
+ state.gate.failures++;
166
+ } else if (outcome === "allow") state.gate.allow++;
167
+ else if (outcome === "ask") state.gate.ask++;
168
+ else state.gate.deny++;
169
+ bump(sessionKey(id), state);
170
+ },
171
+ recordActual(id, outcome) {
172
+ if (!["allow", "deny", "error"].includes(outcome)) throw new Error("Invalid actual decision");
173
+ const state = writable(id);
174
+ const actual = state.gate.actual;
175
+ if (outcome === "allow") actual.allow++;
176
+ else if (outcome === "deny") actual.deny++;
177
+ else actual.error++;
178
+ bump(sessionKey(id), state);
179
+ },
180
+ recordCheck(id, outcome, trend) {
181
+ if (!["ok", "low", "error"].includes(outcome)) throw new Error("Invalid self-check outcome");
182
+ const state = writable(id);
183
+ const check = state.check;
184
+ state.attempts++;
185
+ check.attempts++;
186
+ if (outcome === "error") {
187
+ state.failures++;
188
+ check.failures++;
189
+ } else {
190
+ if (outcome === "low") check.low++;
191
+ if (trend && typeof trend === "object") {
192
+ check.run = advanceRun(check.run, { lowScore: outcome === "low", confident: trend.confident === true });
193
+ check.severity = severityOf(check.run, {
194
+ trendRun: typeof trend.trendRun === "number" ? trend.trendRun : void 0,
195
+ trendSevereRun: typeof trend.trendSevereRun === "number" ? trend.trendSevereRun : void 0
196
+ });
197
+ }
198
+ }
199
+ bump(sessionKey(id), state);
200
+ },
201
+ recordNarrow(id, outcome, dropped = 0, applied = outcome === "applied") {
202
+ if (!["ok", "applied", "error"].includes(outcome)) throw new Error("Invalid narrowing outcome");
203
+ const state = writable(id);
204
+ const narrow = state.narrow;
205
+ state.attempts++;
206
+ narrow.attempts++;
207
+ if (outcome === "error") {
208
+ state.failures++;
209
+ narrow.failures++;
210
+ } else if (outcome === "applied") {
211
+ if (applied) narrow.applied++;
212
+ if (Number.isSafeInteger(dropped) && dropped > 0) narrow.dropped += dropped;
213
+ }
214
+ bump(sessionKey(id), state);
215
+ },
216
+ // Task-completion checking. `ok`/`unsatisfied` are real evaluations; `error`
217
+ // is an unevaluated failure counted as a failure, never as "unsatisfied".
218
+ // The per-condition three-state tallies come from `detail`.
219
+ recordComplete(id, outcome, detail = {}) {
220
+ if (!["ok", "unsatisfied", "error"].includes(outcome)) throw new Error("Invalid task-completion outcome");
221
+ const state = writable(id);
222
+ const complete = state.complete;
223
+ state.attempts++;
224
+ complete.attempts++;
225
+ if (outcome === "error") {
226
+ state.failures++;
227
+ complete.failures++;
228
+ } else {
229
+ if (Number.isSafeInteger(detail.satisfied) && detail.satisfied > 0) complete.satisfied += detail.satisfied;
230
+ if (Number.isSafeInteger(detail.unsatisfied) && detail.unsatisfied > 0) complete.unsatisfied += detail.unsatisfied;
231
+ if (Number.isSafeInteger(detail.insufficient) && detail.insufficient > 0) complete.insufficient += detail.insufficient;
232
+ if (detail.steered === true) complete.steered++;
233
+ }
234
+ bump(sessionKey(id), state);
235
+ },
236
+ log(id, entry) {
237
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) throw new Error("Invalid log entry");
238
+ const kind = entry.kind;
239
+ if (typeof kind !== "string" || !KINDS.includes(kind)) throw new Error("Invalid log entry");
240
+ const record = { id: randomUUID(), at: Date.now(), kind, outcome: typeof entry.outcome === "string" ? entry.outcome.slice(0, 32) : "" };
241
+ const tool = safeTool(entry.tool);
242
+ if (tool) record.tool = tool;
243
+ if (typeof entry.suggestion === "string" && entry.suggestion) record.suggestion = entry.suggestion.slice(0, 16);
244
+ if (typeof entry.action === "string" && entry.action) record.action = entry.action.slice(0, 16);
245
+ if (typeof entry.reason === "string" && entry.reason) record.reason = entry.reason.slice(0, 32);
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 (typeof entry.mode === "string" && entry.mode) record.mode = entry.mode.slice(0, 16);
252
+ if (typeof entry.score === "number" && Number.isFinite(entry.score)) record.score = Math.round(entry.score * 100) / 100;
253
+ if (typeof entry.confidence === "number" && Number.isFinite(entry.confidence)) record.confidence = Math.round(entry.confidence * 100) / 100;
254
+ if (Number.isSafeInteger(entry.dropped) && entry.dropped >= 0) record.dropped = entry.dropped;
255
+ if (Number.isSafeInteger(entry.kept) && entry.kept >= 0) record.kept = entry.kept;
256
+ if (typeof entry.applied === "boolean") record.applied = entry.applied;
257
+ if (Number.isSafeInteger(entry.candidates) && entry.candidates >= 0) record.candidates = entry.candidates;
258
+ if (Number.isSafeInteger(entry.conditions) && entry.conditions >= 0) record.conditions = entry.conditions;
259
+ if (Number.isSafeInteger(entry.satisfied) && entry.satisfied >= 0) record.satisfied = entry.satisfied;
260
+ if (Number.isSafeInteger(entry.unsatisfied) && entry.unsatisfied >= 0) record.unsatisfied = entry.unsatisfied;
261
+ if (Number.isSafeInteger(entry.insufficient) && entry.insufficient >= 0) record.insufficient = entry.insufficient;
262
+ if (entry.steered === true) record.steered = true;
263
+ if (entry.lowConfidence === true) record.lowConfidence = true;
264
+ if (Array.isArray(entry.tools)) {
265
+ const names = entry.tools.filter((name) => typeof name === "string" && Boolean(name.trim())).slice(0, 12).map((name) => name.trim().slice(0, 64));
266
+ if (names.length > 0) record.tools = names;
267
+ }
268
+ const state = writable(id);
269
+ state.log.push(record);
270
+ if (state.log.length > maxLogEntries) state.log.splice(0, state.log.length - maxLogEntries);
271
+ if (sink) {
272
+ const full = { ...record, sessionId: sessionKey(id) };
273
+ if (Array.isArray(entry.tools)) {
274
+ const allNames = entry.tools.filter((name) => typeof name === "string" && Boolean(name.trim())).map((name) => name.trim());
275
+ if (allNames.length > 0) full.tools = allNames;
276
+ }
277
+ try {
278
+ sink(full);
279
+ } catch {
280
+ }
281
+ }
282
+ bump(sessionKey(id), state);
283
+ }
284
+ };
285
+ }
286
+ export {
287
+ createSessionState
288
+ };
@@ -0,0 +1,14 @@
1
+ export declare function stateDir(): string;
2
+ export declare function safeStateId(id: unknown): string | undefined;
3
+ export type StateStoreOptions = {
4
+ dir?: string;
5
+ flushIntervalMs?: number;
6
+ retentionDays?: number;
7
+ now?: () => number;
8
+ };
9
+ export declare function createStateStore({ dir, flushIntervalMs, retentionDays, now }?: StateStoreOptions): {
10
+ load(id: unknown): Promise<Record<string, unknown> | undefined>;
11
+ save(id: unknown, state: unknown): void;
12
+ flush: () => Promise<void>;
13
+ dispose(): Promise<void>;
14
+ };
@@ -0,0 +1,109 @@
1
+ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { isObject } from "./types.js";
6
+ function stateDir() {
7
+ return join(homedir(), ".config", "dsh-decision-layer", "state");
8
+ }
9
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
10
+ function safeStateId(id) {
11
+ return typeof id === "string" && SAFE_ID.test(id) && !id.includes("..") ? id : void 0;
12
+ }
13
+ function createStateStore({
14
+ dir = stateDir(),
15
+ flushIntervalMs = 2e3,
16
+ retentionDays = 30,
17
+ now = () => Date.now()
18
+ } = {}) {
19
+ const pending = /* @__PURE__ */ new Map();
20
+ let timer;
21
+ let flushing = false;
22
+ let lastPrune = 0;
23
+ const fileFor = (id) => join(dir, `${id}.json`);
24
+ const writeOne = async (id, state) => {
25
+ const temporary = join(dir, `${id}.${randomUUID()}.tmp`);
26
+ try {
27
+ await writeFile(temporary, `${JSON.stringify({ v: 1, at: now(), state })}
28
+ `, { mode: 384, flag: "wx" });
29
+ await rename(temporary, fileFor(id));
30
+ } finally {
31
+ await rm(temporary, { force: true }).catch(() => {
32
+ });
33
+ }
34
+ };
35
+ const prune = async () => {
36
+ let names;
37
+ try {
38
+ names = await readdir(dir);
39
+ } catch {
40
+ return;
41
+ }
42
+ const cutoff = now() - retentionDays * 24 * 60 * 60 * 1e3;
43
+ await Promise.all(names.map(async (name) => {
44
+ if (!name.endsWith(".json")) return;
45
+ try {
46
+ const info = await stat(join(dir, name));
47
+ if (info.mtimeMs < cutoff) await rm(join(dir, name), { force: true }).catch(() => {
48
+ });
49
+ } catch {
50
+ }
51
+ }));
52
+ };
53
+ const flush = async () => {
54
+ if (flushing || pending.size === 0) return;
55
+ flushing = true;
56
+ const batch = [...pending.entries()];
57
+ pending.clear();
58
+ try {
59
+ await mkdir(dir, { recursive: true, mode: 448 });
60
+ for (const [id, state] of batch) await writeOne(id, state).catch(() => {
61
+ });
62
+ if (now() - lastPrune > 60 * 60 * 1e3) {
63
+ lastPrune = now();
64
+ await prune();
65
+ }
66
+ } catch {
67
+ } finally {
68
+ flushing = false;
69
+ }
70
+ };
71
+ return {
72
+ // Read a session's persisted state, or undefined when there is no usable file.
73
+ async load(id) {
74
+ const safe = safeStateId(id);
75
+ if (!safe) return void 0;
76
+ try {
77
+ const parsed = JSON.parse(await readFile(fileFor(safe), "utf8"));
78
+ if (isObject(parsed) && isObject(parsed.state)) {
79
+ return parsed.state;
80
+ }
81
+ } catch {
82
+ }
83
+ return void 0;
84
+ },
85
+ // Register the latest live state for a session and schedule a debounced flush.
86
+ save(id, state) {
87
+ const safe = safeStateId(id);
88
+ if (!safe || state === null || typeof state !== "object" || Array.isArray(state)) return;
89
+ pending.set(safe, state);
90
+ if (!timer) timer = setTimeout(() => {
91
+ timer = void 0;
92
+ void flush();
93
+ }, flushIntervalMs);
94
+ },
95
+ flush,
96
+ dispose() {
97
+ if (timer) {
98
+ clearTimeout(timer);
99
+ timer = void 0;
100
+ }
101
+ return flush();
102
+ }
103
+ };
104
+ }
105
+ export {
106
+ createStateStore,
107
+ safeStateId,
108
+ stateDir
109
+ };
@@ -0,0 +1,13 @@
1
+ export type SteerTurn = {
2
+ agent?: {
3
+ session?: {
4
+ id?: string;
5
+ };
6
+ };
7
+ turn?: number;
8
+ };
9
+ export declare function createSteerOnce({ maxTurns }?: {
10
+ maxTurns?: number;
11
+ }): {
12
+ attempt(turn: SteerTurn | undefined, run: () => void): boolean;
13
+ };
@@ -0,0 +1,25 @@
1
+ function createSteerOnce({ maxTurns = 4096 } = {}) {
2
+ const steeredTurns = /* @__PURE__ */ new Set();
3
+ const keyOf = (turn) => {
4
+ const id = turn?.agent?.session?.id;
5
+ const index = turn?.turn;
6
+ return typeof id === "string" && Number.isInteger(index) ? `${id}:${index}` : void 0;
7
+ };
8
+ return {
9
+ attempt(turn, run) {
10
+ const key = keyOf(turn);
11
+ if (key === void 0 || steeredTurns.has(key)) return false;
12
+ try {
13
+ run();
14
+ } catch {
15
+ return false;
16
+ }
17
+ steeredTurns.add(key);
18
+ if (steeredTurns.size > maxTurns) steeredTurns.delete(steeredTurns.values().next().value);
19
+ return true;
20
+ }
21
+ };
22
+ }
23
+ export {
24
+ createSteerOnce
25
+ };
@@ -0,0 +1,29 @@
1
+ import type { AgentLike, CompleteSettings, EvaluateInput, Evaluator, SessionsFace } from './types.js';
2
+ export declare function extractConditions(userText: unknown): string[];
3
+ export declare function completionQuestions(conditions: unknown, evidence: unknown, answer: unknown): EvaluateInput;
4
+ export declare const DEFAULT_MIN_CONFIDENCE = 0.4;
5
+ export type CompletionTurn = {
6
+ agent?: AgentLike;
7
+ turn?: number;
8
+ signal?: AbortSignal;
9
+ output?: unknown;
10
+ userText?: unknown;
11
+ evidence?: unknown;
12
+ };
13
+ export type CompletionResult = {
14
+ evaluated: boolean;
15
+ steered: boolean;
16
+ satisfied?: number;
17
+ unsatisfied?: number;
18
+ insufficient?: number;
19
+ };
20
+ export type TaskCompletionOptions = {
21
+ evaluate?: Evaluator;
22
+ sessions?: SessionsFace;
23
+ steer?: (agent: AgentLike, text: string) => unknown;
24
+ settings?: CompleteSettings | (() => Promise<CompleteSettings | undefined>);
25
+ minConfidence?: number;
26
+ };
27
+ export declare function createTaskCompletion({ evaluate, sessions, steer, settings, minConfidence }?: TaskCompletionOptions): {
28
+ review(turn: CompletionTurn | undefined): Promise<CompletionResult>;
29
+ };