dsh-decision-layer 0.4.0

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.
@@ -0,0 +1,116 @@
1
+ function sessionKey(value) {
2
+ if (typeof value !== 'string' || !value.trim() || value.length > 512) throw new Error('Invalid session ID');
3
+ return value.trim();
4
+ }
5
+
6
+ function emptyState() {
7
+ return { enabled: true, attempts: 0, failures: 0,
8
+ gate: { attempts: 0, failures: 0, ask: 0, deny: 0, allow: 0, actual: { allow: 0, deny: 0, error: 0 } },
9
+ check: { attempts: 0, failures: 0, low: 0 },
10
+ narrow: { attempts: 0, failures: 0, applied: 0, dropped: 0 }, log: [] };
11
+ }
12
+
13
+ function safeTool(value) {
14
+ return typeof value === 'string' && value.trim() ? value.trim().slice(0, 128) : undefined;
15
+ }
16
+
17
+ export function createSessionState({ maxSessions = 1024, maxLogEntries = 50, onLog } = {}) {
18
+ if (!Number.isSafeInteger(maxSessions) || maxSessions < 1) throw new Error('Invalid session limit');
19
+ if (!Number.isSafeInteger(maxLogEntries) || maxLogEntries < 1) throw new Error('Invalid log limit');
20
+ const sessions = new Map();
21
+ const sink = typeof onLog === 'function' ? onLog : undefined;
22
+ const writable = id => {
23
+ const key = sessionKey(id);
24
+ let state = sessions.get(key);
25
+ if (!state) {
26
+ if (sessions.size >= maxSessions) throw new Error('Session state limit reached');
27
+ state = emptyState();
28
+ sessions.set(key, state);
29
+ }
30
+ return state;
31
+ };
32
+ return {
33
+ setEnabled(id, enabled) {
34
+ if (typeof enabled !== 'boolean') throw new Error('Invalid enabled value');
35
+ const key = sessionKey(id);
36
+ if (!sessions.has(key) && sessions.size >= maxSessions && !enabled) return;
37
+ writable(key).enabled = enabled;
38
+ },
39
+ snapshot(id) {
40
+ const stored = sessions.get(sessionKey(id));
41
+ const overflow = !stored && sessions.size >= maxSessions;
42
+ const state = stored ?? emptyState();
43
+ const value = { enabled: overflow ? false : state.enabled, hasAutomaticDecisions: state.attempts > 0, attempts: state.attempts, failures: state.failures };
44
+ if (overflow) value.capacityExceeded = true;
45
+ if (state.gate.attempts > 0) value.gate = { ...state.gate, actual: { ...state.gate.actual } };
46
+ if (state.check.attempts > 0) value.check = { ...state.check };
47
+ if (state.narrow.attempts > 0) value.narrow = { ...state.narrow };
48
+ if (state.log.length > 0) value.log = state.log.map(entry => ({ ...entry }));
49
+ return value;
50
+ },
51
+ record(id, kind, outcome) {
52
+ if (kind !== 'gate' || !['allow', 'ask', 'deny', 'error'].includes(outcome)) throw new Error('Invalid automatic decision');
53
+ const state = writable(id);
54
+ state.attempts++;
55
+ state.gate.attempts++;
56
+ if (outcome === 'error') { state.failures++; state.gate.failures++; }
57
+ else state.gate[outcome]++;
58
+ },
59
+ recordActual(id, outcome) {
60
+ if (!['allow', 'deny', 'error'].includes(outcome)) throw new Error('Invalid actual decision');
61
+ writable(id).gate.actual[outcome]++;
62
+ },
63
+ recordCheck(id, outcome) {
64
+ if (!['ok', 'low', 'error'].includes(outcome)) throw new Error('Invalid self-check outcome');
65
+ const state = writable(id);
66
+ state.attempts++;
67
+ state.check.attempts++;
68
+ if (outcome === 'error') { state.failures++; state.check.failures++; }
69
+ else if (outcome === 'low') state.check.low++;
70
+ },
71
+ recordNarrow(id, outcome, dropped = 0) {
72
+ if (!['ok', 'applied', 'error'].includes(outcome)) throw new Error('Invalid narrowing outcome');
73
+ const state = writable(id);
74
+ state.attempts++;
75
+ state.narrow.attempts++;
76
+ if (outcome === 'error') { state.failures++; state.narrow.failures++; }
77
+ else if (outcome === 'applied') {
78
+ state.narrow.applied++;
79
+ if (Number.isSafeInteger(dropped) && dropped > 0) state.narrow.dropped += dropped;
80
+ }
81
+ },
82
+ log(id, entry) {
83
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) throw new Error('Invalid log entry');
84
+ if (!['gate', 'check', 'narrow'].includes(entry.kind)) throw new Error('Invalid log entry');
85
+ const record = { at: Date.now(), kind: entry.kind, outcome: typeof entry.outcome === 'string' ? entry.outcome.slice(0, 32) : '' };
86
+ const tool = safeTool(entry.tool);
87
+ if (tool) record.tool = tool;
88
+ if (typeof entry.suggestion === 'string' && entry.suggestion) record.suggestion = entry.suggestion.slice(0, 16);
89
+ if (typeof entry.action === 'string' && entry.action) record.action = entry.action.slice(0, 16);
90
+ if (typeof entry.reason === 'string' && entry.reason) record.reason = entry.reason.slice(0, 32);
91
+ if (typeof entry.mode === 'string' && entry.mode) record.mode = entry.mode.slice(0, 16);
92
+ if (typeof entry.score === 'number' && Number.isFinite(entry.score)) record.score = Math.round(entry.score * 100) / 100;
93
+ if (typeof entry.confidence === 'number' && Number.isFinite(entry.confidence)) record.confidence = Math.round(entry.confidence * 100) / 100;
94
+ if (Number.isSafeInteger(entry.dropped) && entry.dropped >= 0) record.dropped = entry.dropped;
95
+ if (Number.isSafeInteger(entry.kept) && entry.kept >= 0) record.kept = entry.kept;
96
+ if (Number.isSafeInteger(entry.candidates) && entry.candidates >= 0) record.candidates = entry.candidates;
97
+ if (Array.isArray(entry.tools)) {
98
+ const names = entry.tools.filter(name => typeof name === 'string' && name.trim()).slice(0, 12).map(name => name.trim().slice(0, 64));
99
+ if (names.length > 0) record.tools = names;
100
+ }
101
+ const state = writable(id);
102
+ state.log.push(record);
103
+ if (state.log.length > maxLogEntries) state.log.splice(0, state.log.length - maxLogEntries);
104
+ // Full, untruncated record for the research log sink (all dropped tool
105
+ // names, keyed by session id). The in-memory panel record above stays capped.
106
+ if (sink) {
107
+ const full = { ...record, sessionId: sessionKey(id) };
108
+ if (Array.isArray(entry.tools)) {
109
+ const allNames = entry.tools.filter(name => typeof name === 'string' && name.trim()).map(name => name.trim());
110
+ if (allNames.length > 0) full.tools = allNames;
111
+ }
112
+ try { sink(full); } catch { /* persistence is best effort */ }
113
+ }
114
+ },
115
+ };
116
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: decision-layer
3
+ description: 显式发起一次结构化裁决,适用于有明确候选项或评分标准的判断;不用于开放式创作。
4
+ ---
5
+
6
+ # 手动裁决
7
+
8
+ 当用户要求手动裁决时,用 `decision_evaluate` 提交最少的任务上下文和一组结构化问题。此调用只展示判断结果,不控制 agent 工具、审批或回合流程,也不计入自动裁决总数。
9
+
10
+ - `state` 是与判断相关的文字、对象或数组,不放密钥或不必要的完整对话。
11
+ - `questions` 以稳定 ID 作为键;每个问题必须有 `type` 与完整的 `instructions`。
12
+ - `choice` 的 `criteria` 是至少两个候选 ID 到描述的映射;`score` 的 `criteria` 是从低到高的 2–10 个等级;`noul` 用于明确的二元条件。
13
+ - 解释结果时如实呈现概率、分数及模型名。裁决建议不替代用户选择,也不授予权限。服务不可用时说明事实,不杜撰结果。
14
+
15
+ 需要验证配置时,可调用 `decision_check_connection`;它会向已配置的后端发送一条测试请求,但不会增加自动裁决计数。不要请用户在对话里粘贴 Key。