dsh-decision-layer 0.7.0 → 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 (61) hide show
  1. package/README.en.md +33 -19
  2. package/README.md +33 -21
  3. package/docs/en/DETAIL.md +20 -0
  4. package/docs/zh/DETAIL.md +20 -0
  5. package/lib/client.js +185 -35
  6. package/lib/host/backend.d.ts +15 -0
  7. package/lib/host/backend.js +149 -0
  8. package/lib/host/config-store.d.ts +12 -0
  9. package/lib/host/config-store.js +254 -0
  10. package/lib/host/danger-gate.d.ts +45 -0
  11. package/lib/host/danger-gate.js +196 -0
  12. package/lib/host/excerpt.d.ts +25 -0
  13. package/lib/host/excerpt.js +62 -0
  14. package/lib/host/index.d.ts +6 -0
  15. package/lib/host/index.js +290 -0
  16. package/lib/host/log-analyze.d.ts +121 -0
  17. package/lib/host/log-analyze.js +254 -0
  18. package/lib/host/log-read.d.ts +4 -0
  19. package/lib/host/log-read.js +34 -0
  20. package/lib/host/log-store.d.ts +15 -0
  21. package/lib/host/log-store.js +98 -0
  22. package/lib/host/loop-guard.d.ts +10 -0
  23. package/lib/host/loop-guard.js +52 -0
  24. package/lib/host/manager-api.d.ts +20 -0
  25. package/lib/host/manager-api.js +174 -0
  26. package/lib/host/narrowing.d.ts +48 -0
  27. package/lib/host/narrowing.js +213 -0
  28. package/lib/host/quality-trend.d.ts +14 -0
  29. package/lib/host/quality-trend.js +22 -0
  30. package/lib/host/self-check.d.ts +27 -0
  31. package/lib/host/self-check.js +104 -0
  32. package/lib/host/session-state.d.ts +19 -0
  33. package/lib/host/session-state.js +288 -0
  34. package/lib/host/state-store.d.ts +14 -0
  35. package/lib/host/state-store.js +109 -0
  36. package/lib/host/steer-once.d.ts +13 -0
  37. package/lib/host/steer-once.js +25 -0
  38. package/lib/host/task-completion.d.ts +29 -0
  39. package/lib/host/task-completion.js +148 -0
  40. package/lib/host/types.d.ts +343 -0
  41. package/lib/host/types.js +11 -0
  42. package/locale/en.json +2 -1
  43. package/locale/zh.json +2 -1
  44. package/package.json +19 -10
  45. package/service/.gitkeep +0 -0
  46. package/service/backend.mjs +0 -127
  47. package/service/config-store.mjs +0 -311
  48. package/service/danger-gate.mjs +0 -182
  49. package/service/dsh.mjs +0 -282
  50. package/service/log-analyze.mjs +0 -256
  51. package/service/log-read.mjs +0 -27
  52. package/service/log-store.mjs +0 -82
  53. package/service/loop-guard.mjs +0 -51
  54. package/service/manager-api.mjs +0 -137
  55. package/service/narrowing.mjs +0 -210
  56. package/service/quality-trend.mjs +0 -29
  57. package/service/self-check.mjs +0 -131
  58. package/service/session-state.mjs +0 -257
  59. package/service/state-store.mjs +0 -100
  60. package/service/steer-once.mjs +0 -25
  61. package/service/task-completion.mjs +0 -163
@@ -0,0 +1,290 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
+ import { resolveConfig, featureEnabled } from "./config-store.js";
4
+ import { createBackend } from "./backend.js";
5
+ import { createManagerRoutes } from "./manager-api.js";
6
+ import { createSessionState } from "./session-state.js";
7
+ import { createDangerGate, normalizeDangerRules } from "./danger-gate.js";
8
+ import { createSelfCheck } from "./self-check.js";
9
+ import { createNarrowing } from "./narrowing.js";
10
+ import { createTaskCompletion } from "./task-completion.js";
11
+ import { createLoopGuard } from "./loop-guard.js";
12
+ import { createLogStore } from "./log-store.js";
13
+ import { createStateStore } from "./state-store.js";
14
+ import { analyzeSessionLogs, reconstructSessionSeed, listSessionDecisions } from "./log-analyze.js";
15
+ import { isObject } from "./types.js";
16
+ const name = "dsh-decision-layer";
17
+ const inject = ["tools", "skills"];
18
+ const asMessage = (value) => isObject(value) ? value : void 0;
19
+ const textOf = (content) => Array.isArray(content) ? content.filter((block) => isObject(block) && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("") : "";
20
+ const derivedMessages = (agent) => {
21
+ const messages = agent?.session?.deriveMessages?.();
22
+ return Array.isArray(messages) ? messages : void 0;
23
+ };
24
+ function lastAssistantText(agent) {
25
+ try {
26
+ const messages = derivedMessages(agent);
27
+ if (!messages) return "";
28
+ for (let i = messages.length - 1; i >= 0; i--) {
29
+ const message = asMessage(messages[i]);
30
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
31
+ return textOf(message.content);
32
+ }
33
+ return "";
34
+ } catch {
35
+ return "";
36
+ }
37
+ }
38
+ function stepTaskText(messages) {
39
+ try {
40
+ if (!Array.isArray(messages)) return "";
41
+ return messages.map(asMessage).filter((message) => message?.role === "user" && Array.isArray(message.content)).flatMap((message) => message.content.filter((block) => isObject(block) && block.type === "text" && typeof block.text === "string").map((block) => block.text)).join("\n").slice(0, 8 * 1024);
42
+ } catch {
43
+ return "";
44
+ }
45
+ }
46
+ function turnUserText(agent) {
47
+ try {
48
+ const messages = derivedMessages(agent);
49
+ if (!messages) return "";
50
+ const texts = [];
51
+ let collecting = false;
52
+ for (let i = messages.length - 1; i >= 0; i--) {
53
+ const message = asMessage(messages[i]);
54
+ if (message?.role === "user" && Array.isArray(message.content)) {
55
+ collecting = true;
56
+ texts.unshift(message.content.filter((block) => isObject(block) && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n"));
57
+ } else if (collecting) break;
58
+ }
59
+ return texts.join("\n").slice(0, 8 * 1024);
60
+ } catch {
61
+ return "";
62
+ }
63
+ }
64
+ function turnToolEvidence(agent) {
65
+ try {
66
+ const messages = derivedMessages(agent);
67
+ if (!messages) return [];
68
+ const evidence = [];
69
+ for (const item of messages) {
70
+ const message = asMessage(item);
71
+ if (message?.role !== "tool" || !Array.isArray(message.content)) continue;
72
+ const tool = typeof message.name === "string" ? message.name : typeof message.toolName === "string" ? message.toolName : "";
73
+ if (!tool) continue;
74
+ evidence.push({ tool: tool.slice(0, 128), outcome: message.isError === true ? "error" : "ok" });
75
+ if (evidence.length >= 40) break;
76
+ }
77
+ return evidence;
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+ function agentTools(ctx, agent) {
83
+ try {
84
+ const schemas = typeof ctx.tools?.schemas === "function" ? ctx.tools.schemas(agent) : void 0;
85
+ if (!Array.isArray(schemas)) return [];
86
+ return schemas.filter((schema) => isObject(schema) && typeof schema.name === "string").map((schema) => ({ name: schema.name, description: typeof schema.description === "string" ? schema.description : "" }));
87
+ } catch {
88
+ return [];
89
+ }
90
+ }
91
+ async function apply(ctx, options = {}) {
92
+ const path = options.configPath;
93
+ const backend = createBackend({ config: () => resolveConfig({ path }) });
94
+ let logStore;
95
+ const stateStore = createStateStore();
96
+ const sessions = createSessionState({
97
+ onLog: (entry) => logStore?.append(entry),
98
+ // Restore a resumed session from its durable snapshot; if it has none yet but
99
+ // has persisted JSONL decision history (e.g. a session that predates this
100
+ // persistence, or whose snapshot was pruned), reconstruct a seed from the
101
+ // logs instead. Logs are read across all ports and filtered by the globally
102
+ // unique session id, so a changed instance port never orphans the history.
103
+ loadState: async (id) => await stateStore.load(id) ?? reconstructSessionSeed(id, {
104
+ trendConfig: (await resolveConfig({ path })).checkSettings
105
+ }).catch(() => void 0),
106
+ persistState: (id, state) => stateStore.save(id, state)
107
+ });
108
+ if (typeof ctx.effect === "function") ctx.effect(() => () => {
109
+ void stateStore.dispose();
110
+ });
111
+ const gate = createDangerGate({
112
+ evaluate: (input, request) => backend.evaluate(input, request),
113
+ sessions,
114
+ rules: async () => normalizeDangerRules((await resolveConfig({ path })).dangerRules),
115
+ // Live config-backed floor (default 0.6): a `deny` only blocks once the model
116
+ // is at least this confident. Read per call so an edit takes effect at once.
117
+ minConfidence: async () => (await resolveConfig({ path })).gateSettings?.minConfidence,
118
+ // Read lazily (only when a dangerous call is recorded) so ordinary tool calls
119
+ // never pay for deriveMessages().
120
+ taskText: (agent) => turnUserText(agent)
121
+ });
122
+ const loopGuard = createLoopGuard({ sessions });
123
+ const featureOn = async (key) => {
124
+ try {
125
+ return featureEnabled((await resolveConfig({ path })).features, key);
126
+ } catch {
127
+ return true;
128
+ }
129
+ };
130
+ if (typeof ctx.on === "function") {
131
+ if (typeof ctx.tools.guard === "function") {
132
+ ctx.tools.guard((exec) => loopGuard.check(exec));
133
+ ctx.tools.guard((exec) => gate.guardReason(exec));
134
+ }
135
+ const install = () => {
136
+ const disposePre = ctx.on("tools/pre-execute", async (exec, next) => {
137
+ if (!await featureOn("gate")) return next();
138
+ return gate.preExecute(exec, next);
139
+ });
140
+ const disposePost = ctx.on("tools/post-execute", async (exec, result, next) => {
141
+ gate.observeResult(exec, result);
142
+ return next();
143
+ });
144
+ return () => {
145
+ disposePre?.();
146
+ disposePost?.();
147
+ };
148
+ };
149
+ if (typeof ctx.effect === "function") ctx.effect(install);
150
+ else install();
151
+ const selfCheck = createSelfCheck({
152
+ evaluate: (input, request) => backend.evaluate(input, request),
153
+ sessions,
154
+ settings: async () => (await resolveConfig({ path })).checkSettings,
155
+ steer: (agent, text) => {
156
+ if (typeof agent.steer !== "function") throw new Error("Agent cannot be steered");
157
+ return agent.steer(createUserMessage({ content: [{ type: "text", text }], source: { kind: "user" } }));
158
+ }
159
+ });
160
+ const installCheck = () => ctx.on("agent/turn-stopping", async (payload) => {
161
+ if (!await featureOn("check")) return;
162
+ return selfCheck.review({
163
+ agent: payload.agent,
164
+ turn: payload.turn,
165
+ signal: payload.signal,
166
+ output: lastAssistantText(payload.agent),
167
+ userText: turnUserText(payload.agent)
168
+ });
169
+ });
170
+ if (typeof ctx.effect === "function") ctx.effect(installCheck);
171
+ else installCheck();
172
+ const taskCompletion = createTaskCompletion({
173
+ evaluate: (input, request) => backend.evaluate(input, request),
174
+ sessions,
175
+ settings: async () => (await resolveConfig({ path })).completeSettings,
176
+ steer: (agent, text) => {
177
+ if (typeof agent.steer !== "function") throw new Error("Agent cannot be steered");
178
+ return agent.steer(createUserMessage({ content: [{ type: "text", text }], source: { kind: "user" } }));
179
+ }
180
+ });
181
+ const installCompletion = () => ctx.on("agent/turn-stopping", async (payload) => {
182
+ if (!await featureOn("complete")) return;
183
+ return taskCompletion.review({
184
+ agent: payload.agent,
185
+ turn: payload.turn,
186
+ signal: payload.signal,
187
+ output: lastAssistantText(payload.agent),
188
+ userText: turnUserText(payload.agent),
189
+ evidence: turnToolEvidence(payload.agent)
190
+ });
191
+ });
192
+ if (typeof ctx.effect === "function") ctx.effect(installCompletion);
193
+ else installCompletion();
194
+ const narrowing = createNarrowing({
195
+ evaluate: (input, request) => backend.evaluate(input, request),
196
+ sessions,
197
+ settings: async () => (await resolveConfig({ path })).narrowSettings
198
+ });
199
+ const narrowedTurns = /* @__PURE__ */ new Map();
200
+ const installNarrowingHook = () => ctx.on("agent/pre-step", async (payload, next) => {
201
+ const agent = payload?.agent;
202
+ const id = agent?.session?.id;
203
+ const turn = payload?.turn;
204
+ if (typeof id === "string") await sessions.ensureLoaded(id);
205
+ if (typeof id === "string" && Number.isInteger(turn)) {
206
+ const state = narrowedTurns.get(id);
207
+ if (!state || state.turn !== turn) {
208
+ state?.dispose?.();
209
+ narrowedTurns.delete(id);
210
+ if (await featureOn("narrow")) {
211
+ const entry = { turn, dispose: void 0 };
212
+ narrowedTurns.set(id, entry);
213
+ if (narrowedTurns.size > 1024) {
214
+ const oldest = narrowedTurns.keys().next().value;
215
+ if (oldest !== id) {
216
+ narrowedTurns.get(oldest)?.dispose?.();
217
+ narrowedTurns.delete(oldest);
218
+ }
219
+ }
220
+ try {
221
+ const result = await narrowing.review({
222
+ agent,
223
+ turn,
224
+ signal: payload.signal,
225
+ state: stepTaskText(payload.messages),
226
+ tools: agentTools(ctx, agent)
227
+ });
228
+ if (result.mode === "enforce" && result.applied && typeof agent?.ctx?.tools?.restrict === "function") {
229
+ entry.dispose = agent.ctx.tools.restrict({ allow: result.keep });
230
+ }
231
+ } catch {
232
+ }
233
+ }
234
+ }
235
+ }
236
+ return next();
237
+ });
238
+ if (typeof ctx.effect === "function") ctx.effect(installNarrowingHook);
239
+ else installNarrowingHook();
240
+ }
241
+ const rawSkill = await readFile(new URL("../../skills/decision-layer/SKILL.md", import.meta.url), "utf8");
242
+ const content = rawSkill.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
243
+ ctx.skills.register({ name: "decision-layer", description: "\u663E\u5F0F\u53D1\u8D77\u7ED3\u6784\u5316\u88C1\u51B3\uFF0C\u8F85\u52A9\u660E\u786E\u8FB9\u754C\u7684\u9009\u62E9\u4E0E\u8BC4\u5206\u3002", content, source: "bundled" });
244
+ const output = { schema: { type: "object", additionalProperties: true }, render: (_args, value) => [{ type: "text", text: JSON.stringify(value) }] };
245
+ ctx.tools.register({
246
+ name: "decision_evaluate",
247
+ description: "\u5BF9 state \u548C questions \u6267\u884C\u4E00\u6B21\u663E\u5F0F\u7ED3\u6784\u5316\u88C1\u51B3\uFF0C\u4E0D\u4FEE\u6539 agent \u884C\u4E3A\u3002",
248
+ parameters: { type: "object", required: ["state", "questions"], properties: {
249
+ state: { oneOf: [{ type: "string" }, { type: "object", additionalProperties: true }, { type: "array", items: {} }] },
250
+ questions: { type: "object", additionalProperties: { type: "object", additionalProperties: true } }
251
+ }, additionalProperties: false },
252
+ output,
253
+ timeoutMs: 25e3,
254
+ execute: (args, exec) => backend.evaluate(args, { signal: exec.signal })
255
+ });
256
+ ctx.tools.register({
257
+ name: "decision_check_connection",
258
+ description: "\u5411\u88C1\u51B3\u7AEF\u70B9\u53D1\u9001\u4E00\u6B21\u8F7B\u91CF\u8FDE\u901A\u6D4B\u8BD5\uFF0C\u4E0D\u589E\u52A0\u81EA\u52A8\u88C1\u51B3\u6307\u6807\u3002",
259
+ parameters: { type: "object", properties: {}, additionalProperties: false },
260
+ output,
261
+ timeoutMs: 25e3,
262
+ execute: async (_args, exec) => {
263
+ const result = await backend.evaluate({ state: "Connection test", questions: {
264
+ connected: { type: "noul", instructions: "Is this a connection test?" }
265
+ } }, { signal: exec.signal });
266
+ return { connected: true, model: result.model, usage: result.usage };
267
+ }
268
+ });
269
+ ctx.inject(["webServer"], (web) => {
270
+ logStore = createLogStore({ port: web.webServer.port });
271
+ const port = web.webServer.port;
272
+ const analyze = async (sessionId) => analyzeSessionLogs(sessionId, {
273
+ port,
274
+ trendConfig: (await resolveConfig({ path })).checkSettings
275
+ });
276
+ const listRows = (sessionId, { page, pageSize } = {}) => listSessionDecisions(sessionId, { port, page, pageSize });
277
+ const unregister = createManagerRoutes({ path, sessions, backend, logStore, analyze, listRows, bindHost: web.webServer.host }).map((route) => web.webServer.register(route));
278
+ return () => {
279
+ unregister.forEach((dispose) => dispose());
280
+ const store = logStore;
281
+ logStore = void 0;
282
+ void store?.dispose();
283
+ };
284
+ });
285
+ }
286
+ export {
287
+ apply,
288
+ inject,
289
+ name
290
+ };
@@ -0,0 +1,121 @@
1
+ import type { LogRow, SessionSeed, TrendConfig } from './types.js';
2
+ export type PersistedRecord = Record<string, unknown>;
3
+ export type Annotation = {
4
+ rating?: unknown;
5
+ at: number;
6
+ };
7
+ export type TrendBacktest = {
8
+ warnHits: number;
9
+ severeHits: number;
10
+ maxRun: number;
11
+ };
12
+ export declare function foldSession(records: Iterable<unknown>): {
13
+ decisions: Map<string, PersistedRecord>;
14
+ annotations: Map<string, Annotation>;
15
+ };
16
+ export type Profile = {
17
+ gate: {
18
+ attempts: number;
19
+ allow: number;
20
+ ask: number;
21
+ deny: number;
22
+ error: number;
23
+ };
24
+ check: {
25
+ attempts: number;
26
+ ok: number;
27
+ low: number;
28
+ error: number;
29
+ scoreSum: number;
30
+ scoreCount: number;
31
+ confidence: {
32
+ lt40: number;
33
+ lt60: number;
34
+ lt80: number;
35
+ gte80: number;
36
+ };
37
+ };
38
+ narrow: {
39
+ attempts: number;
40
+ applied: number;
41
+ error: number;
42
+ droppedSum: number;
43
+ tooManyCandidates: number;
44
+ };
45
+ complete: {
46
+ attempts: number;
47
+ satisfied: number;
48
+ unsatisfied: number;
49
+ insufficient: number;
50
+ error: number;
51
+ steered: number;
52
+ };
53
+ };
54
+ export declare function buildProfile(decisions: ReadonlyMap<string, PersistedRecord> | Iterable<PersistedRecord>): Profile;
55
+ export declare function backtestTrend(checkRecords: Iterable<unknown>, { trendMinConfidence, trendRun, trendSevereRun, lowScoreThreshold }?: TrendConfig): TrendBacktest;
56
+ export declare function annotationSummary(decisions: ReadonlyMap<string, PersistedRecord>, annotations: ReadonlyMap<string, Annotation>): {
57
+ summary: {
58
+ rated: number;
59
+ good: number;
60
+ bad: number;
61
+ unsure: number;
62
+ };
63
+ ratings: Record<string, unknown>;
64
+ };
65
+ export declare function analyzeSession(records: Iterable<unknown>, trendConfig?: TrendConfig): {
66
+ totalDecisions: number;
67
+ profile: Profile;
68
+ trendBacktest: TrendBacktest;
69
+ annotations: {
70
+ rated: number;
71
+ good: number;
72
+ bad: number;
73
+ unsure: number;
74
+ };
75
+ ratings: Record<string, unknown>;
76
+ };
77
+ export type ReadStream = (options: {
78
+ dir?: string;
79
+ port?: number;
80
+ }) => AsyncIterable<unknown>;
81
+ export declare function analyzeSessionLogs(sessionId: unknown, { dir, port, trendConfig, readStream }?: {
82
+ dir?: string;
83
+ port?: number;
84
+ trendConfig?: TrendConfig;
85
+ readStream?: ReadStream;
86
+ }): Promise<{
87
+ totalDecisions: number;
88
+ profile: Profile;
89
+ trendBacktest: TrendBacktest;
90
+ annotations: {
91
+ rated: number;
92
+ good: number;
93
+ bad: number;
94
+ unsure: number;
95
+ };
96
+ ratings: Record<string, unknown>;
97
+ }>;
98
+ export declare function buildSeed(decisions: ReadonlyMap<string, PersistedRecord>, { maxLog, trendConfig }?: {
99
+ maxLog?: number;
100
+ trendConfig?: TrendConfig;
101
+ }): SessionSeed;
102
+ export declare function reconstructSessionSeed(sessionId: unknown, { dir, port, trendConfig, maxLog, readStream }?: {
103
+ dir?: string;
104
+ port?: number;
105
+ trendConfig?: TrendConfig;
106
+ maxLog?: number;
107
+ readStream?: ReadStream;
108
+ }): Promise<SessionSeed | undefined>;
109
+ export declare function listSessionDecisions(sessionId: unknown, { dir, port, page, pageSize, readStream }?: {
110
+ dir?: string;
111
+ port?: number;
112
+ page?: number;
113
+ pageSize?: number;
114
+ readStream?: ReadStream;
115
+ }): Promise<{
116
+ entries: LogRow[];
117
+ total: number;
118
+ page: number;
119
+ pageSize: number;
120
+ pages: number;
121
+ }>;
@@ -0,0 +1,254 @@
1
+ import { createReadableStream } from "./log-read.js";
2
+ import { advanceRun, severityOf } from "./quality-trend.js";
3
+ import { narrowApplied } from "./narrowing.js";
4
+ import { isObject } from "./types.js";
5
+ const DECISION_KINDS = /* @__PURE__ */ new Set(["gate", "check", "narrow", "complete"]);
6
+ const atOf = (record) => {
7
+ const value = record.at;
8
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
9
+ };
10
+ function foldSession(records) {
11
+ const decisions = /* @__PURE__ */ new Map();
12
+ const annotations = /* @__PURE__ */ new Map();
13
+ for (const record of records) {
14
+ if (!isObject(record)) continue;
15
+ if (record.kind === "annotation") {
16
+ const target = typeof record.target === "string" ? record.target : void 0;
17
+ if (!target) continue;
18
+ const prev = annotations.get(target);
19
+ if (!prev || Number.isFinite(record.at) && record.at >= (prev.at ?? 0)) {
20
+ annotations.set(target, { rating: record.rating, at: Number.isFinite(record.at) ? record.at : 0 });
21
+ }
22
+ continue;
23
+ }
24
+ if (DECISION_KINDS.has(record.kind) && typeof record.id === "string") {
25
+ decisions.set(record.id, record);
26
+ }
27
+ }
28
+ return { decisions, annotations };
29
+ }
30
+ function confidenceBucket(value) {
31
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
32
+ if (value < 0.4) return "lt40";
33
+ if (value < 0.6) return "lt60";
34
+ if (value < 0.8) return "lt80";
35
+ return "gte80";
36
+ }
37
+ function emptyProfile() {
38
+ return {
39
+ gate: { attempts: 0, allow: 0, ask: 0, deny: 0, error: 0 },
40
+ check: { attempts: 0, ok: 0, low: 0, error: 0, scoreSum: 0, scoreCount: 0, confidence: { lt40: 0, lt60: 0, lt80: 0, gte80: 0 } },
41
+ narrow: { attempts: 0, applied: 0, error: 0, droppedSum: 0, tooManyCandidates: 0 },
42
+ complete: { attempts: 0, satisfied: 0, unsatisfied: 0, insufficient: 0, error: 0, steered: 0 }
43
+ };
44
+ }
45
+ function buildProfile(decisions) {
46
+ const profile = emptyProfile();
47
+ const rows = decisions instanceof Map ? [...decisions.values()] : [...decisions];
48
+ for (const record of rows) {
49
+ const kind = record.kind;
50
+ switch (kind) {
51
+ case "gate": {
52
+ const bucket = profile.gate;
53
+ bucket.attempts++;
54
+ if (record.outcome === "error") bucket.error++;
55
+ else if (record.outcome === "allow") bucket.allow++;
56
+ else if (record.outcome === "ask") bucket.ask++;
57
+ else if (record.outcome === "deny") bucket.deny++;
58
+ break;
59
+ }
60
+ case "check": {
61
+ const bucket = profile.check;
62
+ bucket.attempts++;
63
+ if (record.outcome === "error") bucket.error++;
64
+ else if (record.outcome === "low") bucket.low++;
65
+ else bucket.ok++;
66
+ if (typeof record.score === "number" && Number.isFinite(record.score)) {
67
+ bucket.scoreSum += record.score;
68
+ bucket.scoreCount++;
69
+ }
70
+ const cb = confidenceBucket(record.confidence);
71
+ if (cb) bucket.confidence[cb]++;
72
+ break;
73
+ }
74
+ case "narrow": {
75
+ const bucket = profile.narrow;
76
+ bucket.attempts++;
77
+ if (record.outcome === "error") bucket.error++;
78
+ else if (narrowApplied(record)) bucket.applied++;
79
+ if (Number.isSafeInteger(record.dropped) && record.dropped > 0) bucket.droppedSum += record.dropped;
80
+ if (record.reason === "too-many-candidates") bucket.tooManyCandidates++;
81
+ break;
82
+ }
83
+ case "complete": {
84
+ const bucket = profile.complete;
85
+ bucket.attempts++;
86
+ if (record.outcome === "error") bucket.error++;
87
+ if (Number.isSafeInteger(record.satisfied)) bucket.satisfied += record.satisfied;
88
+ if (Number.isSafeInteger(record.unsatisfied)) bucket.unsatisfied += record.unsatisfied;
89
+ if (Number.isSafeInteger(record.insufficient)) bucket.insufficient += record.insufficient;
90
+ if (record.steered === true) bucket.steered++;
91
+ break;
92
+ }
93
+ default:
94
+ break;
95
+ }
96
+ }
97
+ return profile;
98
+ }
99
+ function backtestTrend(checkRecords, { trendMinConfidence = 0.6, trendRun = 3, trendSevereRun = 5, lowScoreThreshold = 1 } = {}) {
100
+ const ordered = [...checkRecords].filter((record) => isObject(record) && record.kind === "check").sort((a, b) => atOf(a) - atOf(b));
101
+ let run = 0, warnHits = 0, severeHits = 0, maxRun = 0;
102
+ for (const record of ordered) {
103
+ if (record.outcome === "error") continue;
104
+ const isLow = record.outcome === "low" || typeof record.score === "number" && record.score <= lowScoreThreshold;
105
+ if (!isLow) {
106
+ run = 0;
107
+ continue;
108
+ }
109
+ const confident = typeof record.confidence === "number" && Number.isFinite(record.confidence) && record.confidence >= trendMinConfidence;
110
+ if (!confident) continue;
111
+ run++;
112
+ maxRun = Math.max(maxRun, run);
113
+ if (run === trendSevereRun) severeHits++;
114
+ else if (run === trendRun) warnHits++;
115
+ }
116
+ return { warnHits, severeHits, maxRun };
117
+ }
118
+ function annotationSummary(decisions, annotations) {
119
+ const summary = { rated: 0, good: 0, bad: 0, unsure: 0 };
120
+ const ratings = /* @__PURE__ */ Object.create(null);
121
+ for (const [id] of decisions) {
122
+ const annotation = annotations.get(id);
123
+ if (!annotation || typeof annotation.rating !== "string") continue;
124
+ summary.rated++;
125
+ if (annotation.rating === "good") summary.good++;
126
+ else if (annotation.rating === "bad") summary.bad++;
127
+ else if (annotation.rating === "unsure") summary.unsure++;
128
+ ratings[id] = annotation.rating;
129
+ }
130
+ return { summary, ratings };
131
+ }
132
+ function analyzeSession(records, trendConfig) {
133
+ const { decisions, annotations } = foldSession(records);
134
+ const annotation = annotationSummary(decisions, annotations);
135
+ return {
136
+ totalDecisions: decisions.size,
137
+ profile: buildProfile(decisions),
138
+ trendBacktest: backtestTrend([...decisions.values()], trendConfig),
139
+ annotations: annotation.summary,
140
+ ratings: annotation.ratings
141
+ };
142
+ }
143
+ async function analyzeSessionLogs(sessionId, { dir, port, trendConfig, readStream = createReadableStream } = {}) {
144
+ if (typeof sessionId !== "string" || !sessionId.trim()) throw new Error("Invalid session id");
145
+ const records = [];
146
+ for await (const record of readStream({ dir, port })) {
147
+ if (!isObject(record)) continue;
148
+ if (record.sessionId === sessionId) records.push(record);
149
+ }
150
+ return analyzeSession(records, trendConfig);
151
+ }
152
+ function buildSeed(decisions, { maxLog = 50, trendConfig = {} } = {}) {
153
+ const ordered = [...decisions.values()].filter((record) => DECISION_KINDS.has(record.kind)).sort((a, b) => atOf(a) - atOf(b));
154
+ const seed = {
155
+ attempts: 0,
156
+ failures: 0,
157
+ gate: { attempts: 0, failures: 0, ask: 0, deny: 0, allow: 0, actual: { allow: 0, deny: 0, error: 0 } },
158
+ check: { attempts: 0, failures: 0, low: 0, run: 0, severity: "normal" },
159
+ narrow: { attempts: 0, failures: 0, applied: 0, dropped: 0 },
160
+ complete: { attempts: 0, failures: 0, unsatisfied: 0, satisfied: 0, insufficient: 0, steered: 0 },
161
+ log: []
162
+ };
163
+ const minConfidence = typeof trendConfig.trendMinConfidence === "number" ? trendConfig.trendMinConfidence : 0.6;
164
+ const thresholds = { trendRun: trendConfig.trendRun ?? 3, trendSevereRun: trendConfig.trendSevereRun ?? 5 };
165
+ let run = 0;
166
+ for (const record of ordered) {
167
+ seed.attempts++;
168
+ const isError = record.outcome === "error";
169
+ if (isError) seed.failures++;
170
+ if (record.kind === "gate") {
171
+ seed.gate.attempts++;
172
+ if (isError) seed.gate.failures++;
173
+ if (record.outcome === "allow") seed.gate.allow++;
174
+ else if (record.outcome === "ask") seed.gate.ask++;
175
+ else if (record.outcome === "deny") seed.gate.deny++;
176
+ } else if (record.kind === "check") {
177
+ seed.check.attempts++;
178
+ if (isError) seed.check.failures++;
179
+ if (!isError) {
180
+ if (record.outcome === "low") seed.check.low++;
181
+ const confident = typeof record.confidence === "number" && Number.isFinite(record.confidence) && record.confidence >= minConfidence;
182
+ run = advanceRun(run, { lowScore: record.outcome === "low", confident });
183
+ }
184
+ } else if (record.kind === "narrow") {
185
+ seed.narrow.attempts++;
186
+ if (isError) seed.narrow.failures++;
187
+ if (record.outcome === "applied") {
188
+ if (narrowApplied(record)) seed.narrow.applied++;
189
+ if (Number.isSafeInteger(record.dropped) && record.dropped > 0) seed.narrow.dropped += record.dropped;
190
+ }
191
+ } else if (record.kind === "complete") {
192
+ seed.complete.attempts++;
193
+ if (isError) seed.complete.failures++;
194
+ if (!isError) {
195
+ if (Number.isSafeInteger(record.satisfied)) seed.complete.satisfied += record.satisfied;
196
+ if (Number.isSafeInteger(record.unsatisfied)) seed.complete.unsatisfied += record.unsatisfied;
197
+ if (Number.isSafeInteger(record.insufficient)) seed.complete.insufficient += record.insufficient;
198
+ if (record.steered === true) seed.complete.steered++;
199
+ }
200
+ }
201
+ }
202
+ seed.check.run = run;
203
+ seed.check.severity = severityOf(run, thresholds);
204
+ seed.log = ordered.slice(-maxLog).map((record) => {
205
+ const rest = { ...record };
206
+ delete rest.sessionId;
207
+ return rest;
208
+ });
209
+ return seed;
210
+ }
211
+ async function reconstructSessionSeed(sessionId, { dir, port, trendConfig, maxLog, readStream = createReadableStream } = {}) {
212
+ if (typeof sessionId !== "string" || !sessionId.trim()) return void 0;
213
+ const records = [];
214
+ for await (const record of readStream({ dir, port })) {
215
+ if (!isObject(record)) continue;
216
+ if (record.sessionId === sessionId) records.push(record);
217
+ }
218
+ if (records.length === 0) return void 0;
219
+ const { decisions } = foldSession(records);
220
+ if (decisions.size === 0) return void 0;
221
+ return buildSeed(decisions, { maxLog, trendConfig });
222
+ }
223
+ async function listSessionDecisions(sessionId, { dir, port, page = 1, pageSize = 100, readStream = createReadableStream } = {}) {
224
+ if (typeof sessionId !== "string" || !sessionId.trim()) throw new Error("Invalid session id");
225
+ const size = Number.isSafeInteger(pageSize) && pageSize > 0 && pageSize <= 500 ? pageSize : 100;
226
+ const records = [];
227
+ for await (const record of readStream({ dir, port })) {
228
+ if (!isObject(record)) continue;
229
+ if (record.sessionId === sessionId) records.push(record);
230
+ }
231
+ const { decisions } = foldSession(records);
232
+ const rows = [...decisions.values()].sort((a, b) => atOf(b) - atOf(a));
233
+ const total = rows.length;
234
+ const pages = Math.max(1, Math.ceil(total / size));
235
+ const current = Math.min(Math.max(1, Number.isSafeInteger(page) ? page : 1), pages);
236
+ const start = (current - 1) * size;
237
+ const entries = rows.slice(start, start + size).map((record) => {
238
+ const rest = { ...record };
239
+ delete rest.sessionId;
240
+ return rest;
241
+ });
242
+ return { entries, total, page: current, pageSize: size, pages };
243
+ }
244
+ export {
245
+ analyzeSession,
246
+ analyzeSessionLogs,
247
+ annotationSummary,
248
+ backtestTrend,
249
+ buildProfile,
250
+ buildSeed,
251
+ foldSession,
252
+ listSessionDecisions,
253
+ reconstructSessionSeed
254
+ };
@@ -0,0 +1,4 @@
1
+ export declare function createReadableStream({ dir, port }?: {
2
+ dir?: string;
3
+ port?: number;
4
+ }): AsyncGenerator<unknown>;