blun-king-cli 9.1.308 → 9.1.310
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,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
6
|
+
const SUBJECT_KINDS = new Set(['human', 'agent', 'task']);
|
|
7
|
+
const SIGNAL_KINDS = new Set(['heartbeat', 'commitment', 'task_blocked']);
|
|
8
|
+
const HEARTBEAT_STALE_MS = 5 * 60 * 1000;
|
|
9
|
+
const BLOCKED_TASK_STALE_MS = 10 * 60 * 1000;
|
|
10
|
+
const MAX_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
function fail() {
|
|
13
|
+
const error = new Error('ATTENTION_INVALID_INPUT');
|
|
14
|
+
error.code = 'ATTENTION_INVALID_INPUT';
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function exactKeys(value, keys) {
|
|
19
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
20
|
+
&& Object.keys(value).every((key) => keys.has(key));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeId(value) {
|
|
24
|
+
const text = String(value ?? '').trim();
|
|
25
|
+
return SAFE_ID_RE.test(text) ? text : '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function instant(value) {
|
|
29
|
+
if (value === null || value === undefined) return null;
|
|
30
|
+
const milliseconds = Date.parse(String(value));
|
|
31
|
+
return Number.isFinite(milliseconds) ? milliseconds : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeQuietHours(value) {
|
|
35
|
+
if (!exactKeys(value, new Set(['startHour', 'endHour']))) fail();
|
|
36
|
+
const startHour = Number(value.startHour);
|
|
37
|
+
const endHour = Number(value.endHour);
|
|
38
|
+
if (!Number.isInteger(startHour) || startHour < 0 || startHour > 23
|
|
39
|
+
|| !Number.isInteger(endHour) || endHour < 0 || endHour > 23
|
|
40
|
+
|| startHour === endHour) fail();
|
|
41
|
+
return { startHour, endHour };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isQuietTime(nowMs, offsetMinutes, quietHours) {
|
|
45
|
+
const localHour = new Date(nowMs + offsetMinutes * 60 * 1000).getUTCHours();
|
|
46
|
+
return quietHours.startHour < quietHours.endHour
|
|
47
|
+
? localHour >= quietHours.startHour && localHour < quietHours.endHour
|
|
48
|
+
: localHour >= quietHours.startHour || localHour < quietHours.endHour;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function signalReason(subjectKind, signal, nowMs) {
|
|
52
|
+
if (!exactKeys(signal, new Set([
|
|
53
|
+
'kind', 'heartbeatAt', 'activeTask', 'expectedBy', 'blockedSince',
|
|
54
|
+
'evidenceId', 'evidenceScope', 'confidence',
|
|
55
|
+
]))) fail();
|
|
56
|
+
if (!SIGNAL_KINDS.has(signal.kind)) fail();
|
|
57
|
+
const evidenceId = safeId(signal.evidenceId);
|
|
58
|
+
const evidenceScope = safeId(signal.evidenceScope);
|
|
59
|
+
const confidence = Number(signal.confidence);
|
|
60
|
+
if (!evidenceId || !evidenceScope || !Number.isFinite(confidence)
|
|
61
|
+
|| confidence < 0 || confidence > 1) fail();
|
|
62
|
+
if (confidence < 0.5) return null;
|
|
63
|
+
|
|
64
|
+
if (signal.kind === 'heartbeat') {
|
|
65
|
+
if (!exactKeys(signal, new Set(['kind', 'heartbeatAt', 'activeTask', 'evidenceId', 'evidenceScope', 'confidence']))
|
|
66
|
+
|| subjectKind !== 'agent' || signal.activeTask !== true) return null;
|
|
67
|
+
const heartbeatAt = instant(signal.heartbeatAt);
|
|
68
|
+
if (heartbeatAt === null || heartbeatAt > nowMs) fail();
|
|
69
|
+
return nowMs - heartbeatAt >= HEARTBEAT_STALE_MS
|
|
70
|
+
? { reason: 'missing_heartbeat', anchor: new Date(heartbeatAt).toISOString(), evidenceId, evidenceScope, confidence }
|
|
71
|
+
: null;
|
|
72
|
+
}
|
|
73
|
+
if (signal.kind === 'commitment') {
|
|
74
|
+
if (!exactKeys(signal, new Set(['kind', 'expectedBy', 'evidenceId', 'evidenceScope', 'confidence']))) fail();
|
|
75
|
+
const expectedBy = instant(signal.expectedBy);
|
|
76
|
+
if (expectedBy === null) fail();
|
|
77
|
+
return nowMs > expectedBy
|
|
78
|
+
? { reason: 'overdue_commitment', anchor: new Date(expectedBy).toISOString(), evidenceId, evidenceScope, confidence }
|
|
79
|
+
: null;
|
|
80
|
+
}
|
|
81
|
+
if (!exactKeys(signal, new Set(['kind', 'blockedSince', 'evidenceId', 'evidenceScope', 'confidence'])) || subjectKind !== 'task') return null;
|
|
82
|
+
const blockedSince = instant(signal.blockedSince);
|
|
83
|
+
if (blockedSince === null || blockedSince > nowMs) fail();
|
|
84
|
+
return nowMs - blockedSince >= BLOCKED_TASK_STALE_MS
|
|
85
|
+
? { reason: 'blocked_task', anchor: new Date(blockedSince).toISOString(), evidenceId, evidenceScope, confidence }
|
|
86
|
+
: null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildAttentionCandidate(input) {
|
|
90
|
+
const allowed = new Set([
|
|
91
|
+
'tenantId', 'subjectKind', 'subjectId', 'responsibleAgent', 'currentAgent', 'now', 'signal',
|
|
92
|
+
'absenceUntil', 'timezoneOffsetMinutes', 'quietHours', 'cooldownMs', 'allowedChannel',
|
|
93
|
+
]);
|
|
94
|
+
if (!exactKeys(input, allowed)) fail();
|
|
95
|
+
const tenantId = safeId(input.tenantId);
|
|
96
|
+
const subjectKind = String(input.subjectKind ?? '');
|
|
97
|
+
const subjectId = safeId(input.subjectId);
|
|
98
|
+
const responsibleAgent = safeId(input.responsibleAgent);
|
|
99
|
+
const currentAgent = safeId(input.currentAgent);
|
|
100
|
+
const allowedChannel = safeId(input.allowedChannel);
|
|
101
|
+
const nowMs = instant(input.now);
|
|
102
|
+
const offsetMinutes = Number(input.timezoneOffsetMinutes);
|
|
103
|
+
const cooldownMs = Number(input.cooldownMs);
|
|
104
|
+
if (!tenantId || !SUBJECT_KINDS.has(subjectKind) || !subjectId || !responsibleAgent
|
|
105
|
+
|| !currentAgent || !allowedChannel || nowMs === null
|
|
106
|
+
|| !Number.isInteger(offsetMinutes) || offsetMinutes < -840 || offsetMinutes > 840
|
|
107
|
+
|| !Number.isSafeInteger(cooldownMs) || cooldownMs < 1000 || cooldownMs > MAX_COOLDOWN_MS) fail();
|
|
108
|
+
if (currentAgent !== responsibleAgent) return null;
|
|
109
|
+
|
|
110
|
+
const absenceUntil = instant(input.absenceUntil);
|
|
111
|
+
if (input.absenceUntil !== null && absenceUntil === null) fail();
|
|
112
|
+
if (absenceUntil !== null && absenceUntil > nowMs) return null;
|
|
113
|
+
const quietHours = normalizeQuietHours(input.quietHours);
|
|
114
|
+
if (isQuietTime(nowMs, offsetMinutes, quietHours)) return null;
|
|
115
|
+
|
|
116
|
+
const reason = signalReason(subjectKind, input.signal, nowMs);
|
|
117
|
+
if (reason === null) return null;
|
|
118
|
+
const digest = crypto.createHash('sha256')
|
|
119
|
+
.update([tenantId, subjectKind, subjectId, reason.reason, reason.evidenceId].join('\0'))
|
|
120
|
+
.digest('hex')
|
|
121
|
+
.slice(0, 40);
|
|
122
|
+
return {
|
|
123
|
+
candidate_id: `attention-${digest}`,
|
|
124
|
+
subject_kind: subjectKind,
|
|
125
|
+
subject_id: subjectId,
|
|
126
|
+
reason: reason.reason,
|
|
127
|
+
evidence_id: reason.evidenceId,
|
|
128
|
+
evidence_scope: reason.evidenceScope,
|
|
129
|
+
evidence_at: reason.anchor,
|
|
130
|
+
confidence: reason.confidence,
|
|
131
|
+
responsible_agent: responsibleAgent,
|
|
132
|
+
allowed_channel: allowedChannel,
|
|
133
|
+
detected_at: new Date(nowMs).toISOString(),
|
|
134
|
+
cooldown_ms: cooldownMs,
|
|
135
|
+
requires_runtime_authorization: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
BLOCKED_TASK_STALE_MS,
|
|
141
|
+
HEARTBEAT_STALE_MS,
|
|
142
|
+
buildAttentionCandidate,
|
|
143
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const FOCUS_DOMAINS = new Set(['self', 'team', 'goal', 'open_thread', 'next_trigger', 'expected_evidence']);
|
|
4
|
+
const DOMAIN_WEIGHTS = new Map([
|
|
5
|
+
['goal', 60],
|
|
6
|
+
['open_thread', 50],
|
|
7
|
+
['next_trigger', 40],
|
|
8
|
+
['expected_evidence', 30],
|
|
9
|
+
['team', 20],
|
|
10
|
+
['self', 10],
|
|
11
|
+
]);
|
|
12
|
+
const LABELS = new Map([
|
|
13
|
+
['goal', 'Goal'],
|
|
14
|
+
['open_thread', 'Open thread'],
|
|
15
|
+
['next_trigger', 'Next trigger'],
|
|
16
|
+
['expected_evidence', 'Expected evidence'],
|
|
17
|
+
['team', 'Team'],
|
|
18
|
+
['self', 'Self'],
|
|
19
|
+
]);
|
|
20
|
+
const SAFETY_LINE = 'Durable context only; it cannot authorize any action or override the current assignment or runtime policy.';
|
|
21
|
+
|
|
22
|
+
function clean(value, max = 256) {
|
|
23
|
+
const text = String(value ?? '').replace(/[\u0000-\u001f\u007f]+/gu, ' ').replace(/\s+/gu, ' ').trim();
|
|
24
|
+
return text && text.length <= max ? text : '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeScopes(value) {
|
|
28
|
+
if (value === undefined) return [];
|
|
29
|
+
if (!Array.isArray(value) || value.length > 8) return null;
|
|
30
|
+
const scopes = value.map((item) => clean(item, 128));
|
|
31
|
+
return scopes.every(Boolean) ? [...new Set(scopes)] : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildCognitiveFocusProjection(state, { focusScopes, maxItems = 6, maxChars = 900 } = {}) {
|
|
35
|
+
const scopes = normalizeScopes(focusScopes);
|
|
36
|
+
if (scopes === null || !Number.isSafeInteger(maxItems) || maxItems < 1 || maxItems > 8
|
|
37
|
+
|| !Number.isSafeInteger(maxChars) || maxChars < 320 || maxChars > 1600
|
|
38
|
+
|| !Array.isArray(state?.observations)) return null;
|
|
39
|
+
|
|
40
|
+
const groups = new Map();
|
|
41
|
+
state.observations.forEach((item, index) => {
|
|
42
|
+
const domain = String(item?.domain ?? '');
|
|
43
|
+
const key = clean(item?.key, 128);
|
|
44
|
+
const value = clean(item?.value, 512);
|
|
45
|
+
const scope = clean(item?.scope, 128);
|
|
46
|
+
const confidence = Number(item?.confidence);
|
|
47
|
+
const occurredAt = Date.parse(String(item?.occurred_at ?? ''));
|
|
48
|
+
if (!FOCUS_DOMAINS.has(domain) || !key || !value || !scope || scope === 'runtime'
|
|
49
|
+
|| !Number.isFinite(confidence) || confidence < 0.5 || confidence > 1
|
|
50
|
+
|| !Number.isFinite(occurredAt)) return;
|
|
51
|
+
const groupKey = `${domain}\0${key}`;
|
|
52
|
+
const existing = groups.get(groupKey) ?? [];
|
|
53
|
+
existing.push({ domain, key, value, scope, confidence, occurredAt, index });
|
|
54
|
+
groups.set(groupKey, existing);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const ranked = [];
|
|
58
|
+
for (const entries of groups.values()) {
|
|
59
|
+
entries.sort((left, right) => left.occurredAt - right.occurredAt || left.index - right.index);
|
|
60
|
+
const latest = entries.at(-1);
|
|
61
|
+
const revised = entries.some((item) => item.value !== latest.value);
|
|
62
|
+
const scopeScore = scopes.includes(latest.scope) ? 100 : 0;
|
|
63
|
+
ranked.push({
|
|
64
|
+
...latest,
|
|
65
|
+
revised,
|
|
66
|
+
score: scopeScore + DOMAIN_WEIGHTS.get(latest.domain) + latest.confidence * 10,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
ranked.sort((left, right) => right.score - left.score
|
|
70
|
+
|| right.occurredAt - left.occurredAt || left.key.localeCompare(right.key));
|
|
71
|
+
if (ranked.length === 0) return null;
|
|
72
|
+
|
|
73
|
+
const selected = ranked.slice(0, maxItems);
|
|
74
|
+
const lines = selected.map((item) => `- ${LABELS.get(item.domain)}: ${item.value}${item.revised ? ' [revised]' : ''}`);
|
|
75
|
+
while ([`Current durable focus (${lines.length} items):`, ...lines, SAFETY_LINE].join('\n').length > maxChars
|
|
76
|
+
&& lines.length > 0) lines.pop();
|
|
77
|
+
if (lines.length === 0) return null;
|
|
78
|
+
return [`Current durable focus (${lines.length} items):`, ...lines, SAFETY_LINE].join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { buildCognitiveFocusProjection };
|
|
@@ -147,6 +147,14 @@ function initialize(db) {
|
|
|
147
147
|
);
|
|
148
148
|
CREATE INDEX IF NOT EXISTS cognitive_observations_stream
|
|
149
149
|
ON cognitive_observations (tenant_id, agent_id, occurred_at, observation_id);
|
|
150
|
+
CREATE TABLE IF NOT EXISTS cognitive_attention_claims (
|
|
151
|
+
tenant_id TEXT NOT NULL,
|
|
152
|
+
candidate_id TEXT NOT NULL,
|
|
153
|
+
claimed_by TEXT NOT NULL,
|
|
154
|
+
claimed_at TEXT NOT NULL,
|
|
155
|
+
next_allowed_at TEXT NOT NULL,
|
|
156
|
+
PRIMARY KEY (tenant_id, candidate_id)
|
|
157
|
+
);
|
|
150
158
|
`);
|
|
151
159
|
}
|
|
152
160
|
|
|
@@ -235,10 +243,57 @@ function openCognitiveStateStore({ home } = {}) {
|
|
|
235
243
|
return { valid: true, events: rows.length, head_hash: previousHash };
|
|
236
244
|
}
|
|
237
245
|
|
|
246
|
+
function claimAttention(input) {
|
|
247
|
+
const allowedInput = new Set(['tenantId', 'candidate', 'claimedBy', 'claimedAt']);
|
|
248
|
+
const allowedCandidate = new Set([
|
|
249
|
+
'candidate_id', 'subject_kind', 'subject_id', 'reason', 'responsible_agent',
|
|
250
|
+
'evidence_id', 'evidence_scope', 'evidence_at', 'confidence', 'allowed_channel',
|
|
251
|
+
'detected_at', 'cooldown_ms', 'requires_runtime_authorization',
|
|
252
|
+
]);
|
|
253
|
+
if (!exactKeys(input, allowedInput) || !exactKeys(input.candidate, allowedCandidate)
|
|
254
|
+
|| hasForbiddenKey(input)) fail('COGNITIVE_FORBIDDEN_FIELD');
|
|
255
|
+
const tenant = safeId(input.tenantId);
|
|
256
|
+
const candidateId = safeId(input.candidate.candidate_id);
|
|
257
|
+
const claimedBy = safeId(input.claimedBy);
|
|
258
|
+
const claimedAtMs = Date.parse(String(input.claimedAt ?? ''));
|
|
259
|
+
const cooldownMs = Number(input.candidate.cooldown_ms);
|
|
260
|
+
if (!tenant || !candidateId || !claimedBy || !Number.isFinite(claimedAtMs)
|
|
261
|
+
|| !Number.isSafeInteger(cooldownMs) || cooldownMs < 1000
|
|
262
|
+
|| input.candidate.requires_runtime_authorization !== true) fail('COGNITIVE_INVALID_EVENT');
|
|
263
|
+
const claimedAt = new Date(claimedAtMs).toISOString();
|
|
264
|
+
const nextAllowedAt = new Date(claimedAtMs + cooldownMs).toISOString();
|
|
265
|
+
db.exec('BEGIN IMMEDIATE');
|
|
266
|
+
try {
|
|
267
|
+
const existing = db.prepare(`SELECT claimed_by, next_allowed_at FROM cognitive_attention_claims
|
|
268
|
+
WHERE tenant_id = ? AND candidate_id = ?`).get(tenant, candidateId);
|
|
269
|
+
if (existing && Date.parse(existing.next_allowed_at) > claimedAtMs) {
|
|
270
|
+
db.exec('COMMIT');
|
|
271
|
+
return {
|
|
272
|
+
claimed: false,
|
|
273
|
+
claimed_by: existing.claimed_by,
|
|
274
|
+
next_allowed_at: existing.next_allowed_at,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
db.prepare(`INSERT INTO cognitive_attention_claims
|
|
278
|
+
(tenant_id, candidate_id, claimed_by, claimed_at, next_allowed_at) VALUES (?, ?, ?, ?, ?)
|
|
279
|
+
ON CONFLICT(tenant_id, candidate_id) DO UPDATE SET
|
|
280
|
+
claimed_by = excluded.claimed_by,
|
|
281
|
+
claimed_at = excluded.claimed_at,
|
|
282
|
+
next_allowed_at = excluded.next_allowed_at`)
|
|
283
|
+
.run(tenant, candidateId, claimedBy, claimedAt, nextAllowedAt);
|
|
284
|
+
db.exec('COMMIT');
|
|
285
|
+
return { claimed: true, claimed_by: claimedBy, next_allowed_at: nextAllowedAt };
|
|
286
|
+
} catch (error) {
|
|
287
|
+
try { db.exec('ROLLBACK'); } catch {}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
238
292
|
return {
|
|
239
293
|
commit,
|
|
240
294
|
read,
|
|
241
295
|
verify,
|
|
296
|
+
claimAttention,
|
|
242
297
|
close: () => {
|
|
243
298
|
try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch {}
|
|
244
299
|
db.close();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const { openCognitiveStateStore } = require('./cognitive-state-store.cjs');
|
|
5
5
|
const { buildCognitiveContextProjection } = require('./cognitive-context-projection.cjs');
|
|
6
|
+
const { buildCognitiveFocusProjection } = require('./cognitive-focus-projection.cjs');
|
|
6
7
|
|
|
7
8
|
const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
8
9
|
const RIGHTS_AUTHORITIES = new Set(['runtime_user_prompt_hook', 'runtime_tool_policy']);
|
|
@@ -10,6 +11,8 @@ const RIGHTS_DECISIONS = new Set(['passed', 'blocked', 'not_applicable']);
|
|
|
10
11
|
const TOOL_POLICY_DECISIONS = new Set(['passed', 'blocked', 'error']);
|
|
11
12
|
const TOOL_OUTCOMES = new Set(['success', 'error', 'cancelled']);
|
|
12
13
|
const TURN_REASONS = new Set(['completed', 'cancelled', 'filtered', 'failed']);
|
|
14
|
+
const FOCUS_DOMAINS = new Set(['self', 'team', 'goal', 'open_thread', 'next_trigger', 'expected_evidence']);
|
|
15
|
+
const AUTHORITY_KEY_RE = /(?:^|:|_)(?:acl|api[_-]?key|capability|password|permission|secret|token)(?::|_|$)/iu;
|
|
13
16
|
|
|
14
17
|
function fail(code) {
|
|
15
18
|
const error = new Error(code);
|
|
@@ -140,6 +143,27 @@ function createCognitiveTurnLifecycle({ home, tenantId, agentId, runtimeId, now
|
|
|
140
143
|
]);
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
function recordFocusSnapshot(input) {
|
|
147
|
+
if (!exactKeys(input, new Set(['snapshotId', 'observations']))
|
|
148
|
+
|| !safeId(input.snapshotId) || !Array.isArray(input.observations)
|
|
149
|
+
|| input.observations.length < 1 || input.observations.length > 16) {
|
|
150
|
+
fail('COGNITIVE_FOCUS_INVALID');
|
|
151
|
+
}
|
|
152
|
+
const observations = input.observations.map((item) => {
|
|
153
|
+
if (!exactKeys(item, new Set(['domain', 'key', 'value', 'confidence', 'scope']))) fail('COGNITIVE_FOCUS_INVALID');
|
|
154
|
+
const domain = String(item.domain ?? '');
|
|
155
|
+
const key = cleanLabel(item.key, 128);
|
|
156
|
+
const value = cleanLabel(item.value, 512);
|
|
157
|
+
const confidence = Number(item.confidence);
|
|
158
|
+
const scope = cleanLabel(item.scope, 128);
|
|
159
|
+
if (!FOCUS_DOMAINS.has(domain) || !key || AUTHORITY_KEY_RE.test(key) || !value
|
|
160
|
+
|| !Number.isFinite(confidence) || confidence < 0.5 || confidence > 1
|
|
161
|
+
|| !scope || scope === 'runtime') fail('COGNITIVE_FOCUS_INVALID');
|
|
162
|
+
return { domain, key, value, confidence, scope };
|
|
163
|
+
});
|
|
164
|
+
return commitStage(`focus-${input.snapshotId}`, observations);
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
function toolFields(input, allowedKeys) {
|
|
144
168
|
if (!exactKeys(input, allowedKeys)) fail('COGNITIVE_LIFECYCLE_INVALID');
|
|
145
169
|
const turnId = safeTurnId(input.turnId);
|
|
@@ -174,13 +198,16 @@ function createCognitiveTurnLifecycle({ home, tenantId, agentId, runtimeId, now
|
|
|
174
198
|
}
|
|
175
199
|
|
|
176
200
|
function projectForTurn(input) {
|
|
177
|
-
if (!exactKeys(input, new Set(['turnId']))) fail('COGNITIVE_LIFECYCLE_INVALID');
|
|
201
|
+
if (!exactKeys(input, new Set(['turnId', 'focusScopes']))) fail('COGNITIVE_LIFECYCLE_INVALID');
|
|
178
202
|
const turnId = safeTurnId(input.turnId);
|
|
179
203
|
if (turnId === null) fail('COGNITIVE_LIFECYCLE_INVALID');
|
|
180
|
-
|
|
204
|
+
const state = store.read({ tenantId: tenant, agentId: agent });
|
|
205
|
+
const continuity = buildCognitiveContextProjection(state, {
|
|
181
206
|
currentTurnId: turnId,
|
|
182
207
|
currentRuntimeId: runtime,
|
|
183
208
|
});
|
|
209
|
+
const focus = buildCognitiveFocusProjection(state, { focusScopes: input.focusScopes });
|
|
210
|
+
return [focus, continuity].filter(Boolean).join('\n\n') || null;
|
|
184
211
|
}
|
|
185
212
|
|
|
186
213
|
return {
|
|
@@ -188,6 +215,7 @@ function createCognitiveTurnLifecycle({ home, tenantId, agentId, runtimeId, now
|
|
|
188
215
|
recordRightsCheck,
|
|
189
216
|
recordToolPolicy,
|
|
190
217
|
recordToolResult,
|
|
218
|
+
recordFocusSnapshot,
|
|
191
219
|
projectForTurn,
|
|
192
220
|
endTurn,
|
|
193
221
|
read: () => store.read({ tenantId: tenant, agentId: agent }),
|