@moolam/edge-agent 1.0.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.
- package/LICENSE +202 -0
- package/README.md +54 -0
- package/dist/cognitive_bindings.d.ts +329 -0
- package/dist/cognitive_bindings.d.ts.map +1 -0
- package/dist/cognitive_bindings.js +1293 -0
- package/dist/cognitive_bindings.js.map +1 -0
- package/dist/coldstart.d.ts +95 -0
- package/dist/coldstart.d.ts.map +1 -0
- package/dist/coldstart.js +224 -0
- package/dist/coldstart.js.map +1 -0
- package/dist/edge_agent.d.ts +216 -0
- package/dist/edge_agent.d.ts.map +1 -0
- package/dist/edge_agent.js +530 -0
- package/dist/edge_agent.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/local_vector_db.d.ts +115 -0
- package/dist/local_vector_db.d.ts.map +1 -0
- package/dist/local_vector_db.js +297 -0
- package/dist/local_vector_db.js.map +1 -0
- package/dist/slm_runtime.d.ts +158 -0
- package/dist/slm_runtime.d.ts.map +1 -0
- package/dist/slm_runtime.js +247 -0
- package/dist/slm_runtime.js.map +1 -0
- package/dist/trajectory_capture.d.ts +83 -0
- package/dist/trajectory_capture.d.ts.map +1 -0
- package/dist/trajectory_capture.js +193 -0
- package/dist/trajectory_capture.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module cognitive_bindings
|
|
3
|
+
*
|
|
4
|
+
* ChatMessage[] → single SlmRuntime prompt string.
|
|
5
|
+
* SlmRuntime ↔ ModelInterface (generate / stream / embed)
|
|
6
|
+
* with deadlineMs (+ optional AbortSignal) passthrough; stream deltas
|
|
7
|
+
* (CK-03.2).
|
|
8
|
+
* On-device conformance SlmRuntime + ModelConformanceHarness
|
|
9
|
+
* factory; CK-03 + locality zero-egress proof in
|
|
10
|
+
* tests/model_adapter_conformance.test.mjs.
|
|
11
|
+
* LocalVectorDb ↔ MemoryInterface (remember/recall/forget);
|
|
12
|
+
* kind mapping incl. semantic; CK-02 harness factory.
|
|
13
|
+
* Kind-aware decay + correction pinning (see local_vector_db
|
|
14
|
+
* kindAwareDecayFactor / pinCorrectionsInWorkingSet); recall ordering tests.
|
|
15
|
+
* CreateEdgeCognitiveBindings factory (memory/model +
|
|
16
|
+
* edge reasoning/planning/tools/knowledge stubs).
|
|
17
|
+
* ConceptId→topicId remember wrap; coerce legacy SlmRuntime
|
|
18
|
+
* `descriptor` hosts so EdgeAgent.agentTurn can bind CognitiveCore.
|
|
19
|
+
*
|
|
20
|
+
* Invariants:
|
|
21
|
+
* - Identical inputs → identical prompt (B5 state-hash caching).
|
|
22
|
+
* - Empty messages → typed error (never call SlmRuntime).
|
|
23
|
+
* - Prompt / system budget exceeding contextWindow → typed overflow
|
|
24
|
+
* (never silent truncation).
|
|
25
|
+
* - Deadline abort propagates without hanging the turn.
|
|
26
|
+
* - Stream yields deltas, not cumulative text.
|
|
27
|
+
* - remember durable before resolve; recall subject-scoped.
|
|
28
|
+
*/
|
|
29
|
+
import { EPISODIC_HALF_LIFE_DAYS, LocalVectorDb, createLocalVectorMemoryDriver, } from "./local_vector_db.js";
|
|
30
|
+
/** Empty ChatMessage[] before any local generate. */
|
|
31
|
+
export const EDGE_PROMPT_OBLIGATION_EMPTY = "EDGE.PROMPT_EMPTY_MESSAGES";
|
|
32
|
+
/** Assembled prompt (or system slice) exceeds SlmModelCard.contextWindow. */
|
|
33
|
+
export const EDGE_PROMPT_OBLIGATION_OVERFLOW = "EDGE.PROMPT_CONTEXT_OVERFLOW";
|
|
34
|
+
/** Runtime role not in the ChatMessage contract. */
|
|
35
|
+
export const EDGE_PROMPT_OBLIGATION_INVALID_ROLE = "EDGE.PROMPT_INVALID_ROLE";
|
|
36
|
+
/** subjectId required for sovereignty / isolation. */
|
|
37
|
+
export const EDGE_PROMPT_OBLIGATION_SUBJECT = "EDGE.PROMPT_SUBJECT_REQUIRED";
|
|
38
|
+
/** Stable section markers — do not localize (determinism). */
|
|
39
|
+
export const EDGE_PROMPT_ROLE_MARKERS = Object.freeze({
|
|
40
|
+
system: "### system",
|
|
41
|
+
user: "### user",
|
|
42
|
+
assistant: "### assistant",
|
|
43
|
+
tool: "### tool",
|
|
44
|
+
});
|
|
45
|
+
/** Trailing cue inviting the next assistant turn (completion-style SLMs). */
|
|
46
|
+
export const EDGE_PROMPT_ASSISTANT_CUE = `${EDGE_PROMPT_ROLE_MARKERS.assistant}\n`;
|
|
47
|
+
const KNOWN_ROLES = new Set(["system", "user", "assistant", "tool"]);
|
|
48
|
+
/**
|
|
49
|
+
* Conservative token estimate: ceil(UTF-16 length / 4). Used only for
|
|
50
|
+
* context-window gating — not billed usage.
|
|
51
|
+
*/
|
|
52
|
+
export function estimatePromptTokens(text) {
|
|
53
|
+
if (text.length === 0)
|
|
54
|
+
return 0;
|
|
55
|
+
return Math.ceil(text.length / 4);
|
|
56
|
+
}
|
|
57
|
+
export class ChatPromptAssemblyError extends Error {
|
|
58
|
+
obligationId;
|
|
59
|
+
failureClass;
|
|
60
|
+
errorCode;
|
|
61
|
+
constructor(message, opts) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "ChatPromptAssemblyError";
|
|
64
|
+
this.obligationId = opts.obligationId;
|
|
65
|
+
this.failureClass = opts.failureClass ?? "validation";
|
|
66
|
+
this.errorCode = opts.errorCode ?? opts.obligationId;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function emitAssembly(options, partial) {
|
|
70
|
+
if (!options.emit)
|
|
71
|
+
return;
|
|
72
|
+
const event = {
|
|
73
|
+
event: "edge_agent.chat_prompt_assembly",
|
|
74
|
+
subjectId: options.subjectId,
|
|
75
|
+
outcome: partial.outcome,
|
|
76
|
+
messageCount: partial.messageCount,
|
|
77
|
+
systemCount: partial.systemCount,
|
|
78
|
+
toolCount: partial.toolCount,
|
|
79
|
+
estimatedTokens: partial.estimatedTokens,
|
|
80
|
+
systemEstimatedTokens: partial.systemEstimatedTokens,
|
|
81
|
+
contextWindow: options.contextWindow,
|
|
82
|
+
};
|
|
83
|
+
if (options.deviceId !== undefined) {
|
|
84
|
+
event.deviceId = options.deviceId;
|
|
85
|
+
}
|
|
86
|
+
options.emit(event);
|
|
87
|
+
}
|
|
88
|
+
function formatMessageBlock(message) {
|
|
89
|
+
const role = message.role;
|
|
90
|
+
if (!KNOWN_ROLES.has(role)) {
|
|
91
|
+
throw new ChatPromptAssemblyError(`unsupported ChatMessage role: ${String(role)}`, {
|
|
92
|
+
obligationId: EDGE_PROMPT_OBLIGATION_INVALID_ROLE,
|
|
93
|
+
failureClass: "validation",
|
|
94
|
+
errorCode: "INVALID_ROLE",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const content = typeof message.content === "string" ? message.content : String(message.content ?? "");
|
|
98
|
+
if (role === "tool") {
|
|
99
|
+
const id = typeof message.toolCallId === "string" && message.toolCallId.length > 0
|
|
100
|
+
? message.toolCallId
|
|
101
|
+
: "-";
|
|
102
|
+
return `${EDGE_PROMPT_ROLE_MARKERS.tool} id=${id}\n${content}`;
|
|
103
|
+
}
|
|
104
|
+
return `${EDGE_PROMPT_ROLE_MARKERS[role]}\n${content}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Map ChatMessage[] → one SlmRuntime prompt string.
|
|
108
|
+
*
|
|
109
|
+
* Deterministic for identical inputs. Never truncates on overflow.
|
|
110
|
+
*/
|
|
111
|
+
export function assembleChatMessagesToPrompt(messages, options) {
|
|
112
|
+
const subjectId = options.subjectId?.trim() ?? "";
|
|
113
|
+
if (!subjectId) {
|
|
114
|
+
emitAssembly(options, {
|
|
115
|
+
outcome: "subject_required",
|
|
116
|
+
messageCount: Array.isArray(messages) ? messages.length : 0,
|
|
117
|
+
systemCount: 0,
|
|
118
|
+
toolCount: 0,
|
|
119
|
+
estimatedTokens: 0,
|
|
120
|
+
systemEstimatedTokens: 0,
|
|
121
|
+
});
|
|
122
|
+
throw new ChatPromptAssemblyError("assembleChatMessagesToPrompt requires subjectId (subject isolation)", {
|
|
123
|
+
obligationId: EDGE_PROMPT_OBLIGATION_SUBJECT,
|
|
124
|
+
failureClass: "validation",
|
|
125
|
+
errorCode: "SUBJECT_REQUIRED",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const contextWindow = options.contextWindow;
|
|
129
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
|
|
130
|
+
throw new ChatPromptAssemblyError("contextWindow must be a positive number (SlmModelCard.contextWindow)", {
|
|
131
|
+
obligationId: EDGE_PROMPT_OBLIGATION_OVERFLOW,
|
|
132
|
+
failureClass: "validation",
|
|
133
|
+
errorCode: "CONTEXT_WINDOW_INVALID",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
137
|
+
emitAssembly({ ...options, subjectId }, {
|
|
138
|
+
outcome: "empty",
|
|
139
|
+
messageCount: 0,
|
|
140
|
+
systemCount: 0,
|
|
141
|
+
toolCount: 0,
|
|
142
|
+
estimatedTokens: 0,
|
|
143
|
+
systemEstimatedTokens: 0,
|
|
144
|
+
});
|
|
145
|
+
throw new ChatPromptAssemblyError("ChatMessage[] is empty — refusing SlmRuntime prompt assembly", {
|
|
146
|
+
obligationId: EDGE_PROMPT_OBLIGATION_EMPTY,
|
|
147
|
+
failureClass: "validation",
|
|
148
|
+
errorCode: "EMPTY_MESSAGES",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
let systemCount = 0;
|
|
152
|
+
let toolCount = 0;
|
|
153
|
+
const systemChunks = [];
|
|
154
|
+
const blocks = [];
|
|
155
|
+
try {
|
|
156
|
+
for (const message of messages) {
|
|
157
|
+
if (message.role === "system") {
|
|
158
|
+
systemCount += 1;
|
|
159
|
+
systemChunks.push(typeof message.content === "string"
|
|
160
|
+
? message.content
|
|
161
|
+
: String(message.content ?? ""));
|
|
162
|
+
}
|
|
163
|
+
else if (message.role === "tool") {
|
|
164
|
+
toolCount += 1;
|
|
165
|
+
}
|
|
166
|
+
blocks.push(formatMessageBlock(message));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
if (err instanceof ChatPromptAssemblyError) {
|
|
171
|
+
emitAssembly({ ...options, subjectId }, {
|
|
172
|
+
outcome: "invalid_role",
|
|
173
|
+
messageCount: messages.length,
|
|
174
|
+
systemCount,
|
|
175
|
+
toolCount,
|
|
176
|
+
estimatedTokens: 0,
|
|
177
|
+
systemEstimatedTokens: 0,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
const appendCue = options.appendAssistantCue !== false;
|
|
183
|
+
const last = messages[messages.length - 1];
|
|
184
|
+
const alreadyOpenAssistant = last.role === "assistant" &&
|
|
185
|
+
(typeof last.content !== "string" || last.content.length === 0);
|
|
186
|
+
let prompt = blocks.join("\n\n");
|
|
187
|
+
if (appendCue && !alreadyOpenAssistant) {
|
|
188
|
+
prompt = `${prompt}\n\n${EDGE_PROMPT_ASSISTANT_CUE}`;
|
|
189
|
+
}
|
|
190
|
+
const systemEstimatedTokens = estimatePromptTokens(systemChunks.join("\n"));
|
|
191
|
+
const estimatedTokens = estimatePromptTokens(prompt);
|
|
192
|
+
if (systemEstimatedTokens > contextWindow || estimatedTokens > contextWindow) {
|
|
193
|
+
emitAssembly({ ...options, subjectId }, {
|
|
194
|
+
outcome: "overflow",
|
|
195
|
+
messageCount: messages.length,
|
|
196
|
+
systemCount,
|
|
197
|
+
toolCount,
|
|
198
|
+
estimatedTokens,
|
|
199
|
+
systemEstimatedTokens,
|
|
200
|
+
});
|
|
201
|
+
throw new ChatPromptAssemblyError(systemEstimatedTokens > contextWindow
|
|
202
|
+
? `system messages estimate ${systemEstimatedTokens} tokens exceeds contextWindow ${contextWindow}`
|
|
203
|
+
: `assembled prompt estimate ${estimatedTokens} tokens exceeds contextWindow ${contextWindow}`, {
|
|
204
|
+
obligationId: EDGE_PROMPT_OBLIGATION_OVERFLOW,
|
|
205
|
+
failureClass: "cap",
|
|
206
|
+
errorCode: "CONTEXT_OVERFLOW",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
emitAssembly({ ...options, subjectId }, {
|
|
210
|
+
outcome: "ok",
|
|
211
|
+
messageCount: messages.length,
|
|
212
|
+
systemCount,
|
|
213
|
+
toolCount,
|
|
214
|
+
estimatedTokens,
|
|
215
|
+
systemEstimatedTokens,
|
|
216
|
+
});
|
|
217
|
+
return prompt;
|
|
218
|
+
}
|
|
219
|
+
/* ── SlmRuntime → ModelInterface ─────────────────────────── * ──
|
|
220
|
+
|
|
221
|
+
/** Generate/stream aborted by deadlineMs or AbortSignal. */
|
|
222
|
+
export const EDGE_MODEL_OBLIGATION_DEADLINE = "EDGE.MODEL_DEADLINE";
|
|
223
|
+
/** Embed dimension changed after first measurement (CK-03.1). */
|
|
224
|
+
export const EDGE_MODEL_OBLIGATION_EMBED_DIM = "EDGE.MODEL_EMBED_DIM";
|
|
225
|
+
/** SlmRuntime card / load state invalid at adapter construction. */
|
|
226
|
+
export const EDGE_MODEL_OBLIGATION_INIT = "EDGE.MODEL_INIT";
|
|
227
|
+
const DEFAULT_DEADLINE_MS = 30_000;
|
|
228
|
+
const DEFAULT_MAX_TOKENS = 512;
|
|
229
|
+
const DEFAULT_TEMPERATURE = 0.4;
|
|
230
|
+
export class SlmModelAdapterError extends Error {
|
|
231
|
+
obligationId;
|
|
232
|
+
failureClass;
|
|
233
|
+
errorCode;
|
|
234
|
+
constructor(message, opts) {
|
|
235
|
+
super(message);
|
|
236
|
+
this.name = "SlmModelAdapterError";
|
|
237
|
+
this.obligationId = opts.obligationId;
|
|
238
|
+
this.failureClass = opts.failureClass ?? "downstream";
|
|
239
|
+
this.errorCode = opts.errorCode ?? opts.obligationId;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function emitAdapter(options, locality, partial) {
|
|
243
|
+
if (!options.emit)
|
|
244
|
+
return;
|
|
245
|
+
const event = {
|
|
246
|
+
event: "edge_agent.slm_model_adapter",
|
|
247
|
+
subjectId: options.subjectId,
|
|
248
|
+
op: partial.op,
|
|
249
|
+
outcome: partial.outcome,
|
|
250
|
+
locality,
|
|
251
|
+
};
|
|
252
|
+
if (options.deviceId !== undefined)
|
|
253
|
+
event.deviceId = options.deviceId;
|
|
254
|
+
if (partial.finishReason !== undefined)
|
|
255
|
+
event.finishReason = partial.finishReason;
|
|
256
|
+
if (partial.deltaCount !== undefined)
|
|
257
|
+
event.deltaCount = partial.deltaCount;
|
|
258
|
+
if (partial.embedDim !== undefined)
|
|
259
|
+
event.embedDim = partial.embedDim;
|
|
260
|
+
options.emit(event);
|
|
261
|
+
}
|
|
262
|
+
function resolveDeadlineMs(options, defaults) {
|
|
263
|
+
const raw = options?.deadlineMs ?? defaults.defaultDeadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
264
|
+
if (!Number.isFinite(raw) || raw <= 0) {
|
|
265
|
+
throw new SlmModelAdapterError("deadlineMs must be a positive number", {
|
|
266
|
+
obligationId: EDGE_MODEL_OBLIGATION_DEADLINE,
|
|
267
|
+
failureClass: "validation",
|
|
268
|
+
errorCode: "DEADLINE_INVALID",
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return Math.min(Math.max(1, raw), 600_000);
|
|
272
|
+
}
|
|
273
|
+
function mapFinishReason(reason) {
|
|
274
|
+
if (reason === "length")
|
|
275
|
+
return "length";
|
|
276
|
+
if (reason === "deadline" || reason === "aborted")
|
|
277
|
+
return "deadline";
|
|
278
|
+
return "stop";
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Convert runtime stream frames into CK-03.2 deltas.
|
|
282
|
+
* Cumulative prefixes → suffix-only; already-delta chunks passthrough.
|
|
283
|
+
*/
|
|
284
|
+
export async function* toStreamDeltas(frames) {
|
|
285
|
+
let produced = "";
|
|
286
|
+
for await (const frame of frames) {
|
|
287
|
+
if (typeof frame !== "string" || frame.length === 0)
|
|
288
|
+
continue;
|
|
289
|
+
if (produced.length > 0 && frame.startsWith(produced)) {
|
|
290
|
+
const delta = frame.slice(produced.length);
|
|
291
|
+
if (delta.length === 0)
|
|
292
|
+
continue;
|
|
293
|
+
produced = frame;
|
|
294
|
+
yield delta;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
produced += frame;
|
|
298
|
+
yield frame;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function buildSlmParams(prompt, options, deadlineMs) {
|
|
303
|
+
const params = {
|
|
304
|
+
prompt,
|
|
305
|
+
maxTokens: typeof options?.maxTokens === "number" && options.maxTokens > 0
|
|
306
|
+
? Math.min(options.maxTokens, 8_192)
|
|
307
|
+
: DEFAULT_MAX_TOKENS,
|
|
308
|
+
temperature: typeof options?.temperature === "number" && Number.isFinite(options.temperature)
|
|
309
|
+
? options.temperature
|
|
310
|
+
: DEFAULT_TEMPERATURE,
|
|
311
|
+
deadlineMs,
|
|
312
|
+
};
|
|
313
|
+
if (options?.stopSequences && options.stopSequences.length > 0) {
|
|
314
|
+
params.stopSequences = options.stopSequences.slice(0, 16);
|
|
315
|
+
}
|
|
316
|
+
return params;
|
|
317
|
+
}
|
|
318
|
+
function raceDeadlineOrAbort(work, deadlineMs, signal) {
|
|
319
|
+
if (signal?.aborted) {
|
|
320
|
+
return Promise.resolve({ kind: "aborted" });
|
|
321
|
+
}
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
let settled = false;
|
|
324
|
+
const finish = (result) => {
|
|
325
|
+
if (settled)
|
|
326
|
+
return;
|
|
327
|
+
settled = true;
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
signal?.removeEventListener("abort", onAbort);
|
|
330
|
+
resolve(result);
|
|
331
|
+
};
|
|
332
|
+
const timer = setTimeout(() => finish({ kind: "deadline" }), deadlineMs);
|
|
333
|
+
const onAbort = () => finish({ kind: "aborted" });
|
|
334
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
335
|
+
work.then((value) => finish({ kind: "ok", value }), (err) => {
|
|
336
|
+
if (settled)
|
|
337
|
+
return;
|
|
338
|
+
settled = true;
|
|
339
|
+
clearTimeout(timer);
|
|
340
|
+
signal?.removeEventListener("abort", onAbort);
|
|
341
|
+
reject(err);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Wrap an SlmRuntime as ModelInterface: assemble ChatMessage[] → prompt,
|
|
347
|
+
* honor deadlineMs / AbortSignal, stream CK-03.2 deltas, stable embed dim.
|
|
348
|
+
*/
|
|
349
|
+
export function createSlmModelAdapter(runtime, options) {
|
|
350
|
+
const subjectId = options.subjectId?.trim() ?? "";
|
|
351
|
+
if (!subjectId) {
|
|
352
|
+
throw new SlmModelAdapterError("createSlmModelAdapter requires subjectId (subject isolation)", {
|
|
353
|
+
obligationId: EDGE_PROMPT_OBLIGATION_SUBJECT,
|
|
354
|
+
failureClass: "validation",
|
|
355
|
+
errorCode: "SUBJECT_REQUIRED",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (!runtime || typeof runtime.generate !== "function") {
|
|
359
|
+
emitAdapter({ ...options, subjectId }, options.locality ?? "on-device", { op: "init", outcome: "init_error" });
|
|
360
|
+
throw new SlmModelAdapterError("SlmRuntime is unavailable", {
|
|
361
|
+
obligationId: EDGE_MODEL_OBLIGATION_INIT,
|
|
362
|
+
failureClass: "config",
|
|
363
|
+
errorCode: "RUNTIME_UNAVAILABLE",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const card = runtime.card;
|
|
367
|
+
if (!card ||
|
|
368
|
+
typeof card.modelId !== "string" ||
|
|
369
|
+
card.modelId.trim().length === 0 ||
|
|
370
|
+
!Number.isFinite(card.contextWindow) ||
|
|
371
|
+
card.contextWindow <= 0) {
|
|
372
|
+
emitAdapter({ ...options, subjectId }, options.locality ?? "on-device", { op: "init", outcome: "init_error" });
|
|
373
|
+
throw new SlmModelAdapterError("SlmRuntime.card missing modelId or valid contextWindow", {
|
|
374
|
+
obligationId: EDGE_MODEL_OBLIGATION_INIT,
|
|
375
|
+
failureClass: "config",
|
|
376
|
+
errorCode: "CARD_INVALID",
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const locality = options.locality ?? "on-device";
|
|
380
|
+
const descriptor = {
|
|
381
|
+
modelId: card.modelId,
|
|
382
|
+
contextWindow: card.contextWindow,
|
|
383
|
+
locality,
|
|
384
|
+
modalities: ["text"],
|
|
385
|
+
};
|
|
386
|
+
const scoped = { ...options, subjectId };
|
|
387
|
+
let embedDim = null;
|
|
388
|
+
const assemble = (messages) => assembleChatMessagesToPrompt(messages, {
|
|
389
|
+
contextWindow: card.contextWindow,
|
|
390
|
+
subjectId,
|
|
391
|
+
...(options.deviceId !== undefined ? { deviceId: options.deviceId } : {}),
|
|
392
|
+
...(options.emit
|
|
393
|
+
? {
|
|
394
|
+
emit: (e) => {
|
|
395
|
+
options.emit?.(e);
|
|
396
|
+
},
|
|
397
|
+
}
|
|
398
|
+
: {}),
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
get descriptor() {
|
|
402
|
+
return descriptor;
|
|
403
|
+
},
|
|
404
|
+
async generate(messages, genOptions) {
|
|
405
|
+
let prompt;
|
|
406
|
+
try {
|
|
407
|
+
prompt = assemble(messages);
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
if (err instanceof ChatPromptAssemblyError) {
|
|
411
|
+
emitAdapter(scoped, locality, {
|
|
412
|
+
op: "generate",
|
|
413
|
+
outcome: err.obligationId === EDGE_PROMPT_OBLIGATION_EMPTY
|
|
414
|
+
? "empty"
|
|
415
|
+
: err.obligationId === EDGE_PROMPT_OBLIGATION_OVERFLOW
|
|
416
|
+
? "overflow"
|
|
417
|
+
: "error",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
emitAdapter(scoped, locality, { op: "generate", outcome: "error" });
|
|
422
|
+
}
|
|
423
|
+
throw err;
|
|
424
|
+
}
|
|
425
|
+
const deadlineMs = resolveDeadlineMs(genOptions, scoped);
|
|
426
|
+
if (scoped.signal?.aborted) {
|
|
427
|
+
emitAdapter(scoped, locality, {
|
|
428
|
+
op: "generate",
|
|
429
|
+
outcome: "aborted",
|
|
430
|
+
finishReason: "deadline",
|
|
431
|
+
});
|
|
432
|
+
return { text: "", finishReason: "deadline" };
|
|
433
|
+
}
|
|
434
|
+
const params = buildSlmParams(prompt, genOptions, deadlineMs);
|
|
435
|
+
try {
|
|
436
|
+
const raced = await raceDeadlineOrAbort(runtime.generate(params), deadlineMs, scoped.signal);
|
|
437
|
+
if (raced.kind === "deadline" || raced.kind === "aborted") {
|
|
438
|
+
emitAdapter(scoped, locality, {
|
|
439
|
+
op: "generate",
|
|
440
|
+
outcome: raced.kind === "aborted" ? "aborted" : "deadline",
|
|
441
|
+
finishReason: "deadline",
|
|
442
|
+
});
|
|
443
|
+
return { text: "", finishReason: "deadline" };
|
|
444
|
+
}
|
|
445
|
+
const finishReason = mapFinishReason(raced.value.finishReason);
|
|
446
|
+
emitAdapter(scoped, locality, {
|
|
447
|
+
op: "generate",
|
|
448
|
+
outcome: finishReason === "deadline" ? "deadline" : "ok",
|
|
449
|
+
finishReason,
|
|
450
|
+
});
|
|
451
|
+
return {
|
|
452
|
+
text: raced.value.text ?? "",
|
|
453
|
+
finishReason,
|
|
454
|
+
usage: {
|
|
455
|
+
inputTokens: estimatePromptTokens(prompt),
|
|
456
|
+
outputTokens: estimatePromptTokens(raced.value.text ?? ""),
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
emitAdapter(scoped, locality, { op: "generate", outcome: "error" });
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
async *generateStream(messages, genOptions) {
|
|
466
|
+
let prompt;
|
|
467
|
+
try {
|
|
468
|
+
prompt = assemble(messages);
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
if (err instanceof ChatPromptAssemblyError) {
|
|
472
|
+
emitAdapter(scoped, locality, {
|
|
473
|
+
op: "generateStream",
|
|
474
|
+
outcome: err.obligationId === EDGE_PROMPT_OBLIGATION_EMPTY
|
|
475
|
+
? "empty"
|
|
476
|
+
: err.obligationId === EDGE_PROMPT_OBLIGATION_OVERFLOW
|
|
477
|
+
? "overflow"
|
|
478
|
+
: "error",
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
emitAdapter(scoped, locality, {
|
|
483
|
+
op: "generateStream",
|
|
484
|
+
outcome: "error",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
const deadlineMs = resolveDeadlineMs(genOptions, scoped);
|
|
490
|
+
if (scoped.signal?.aborted) {
|
|
491
|
+
emitAdapter(scoped, locality, {
|
|
492
|
+
op: "generateStream",
|
|
493
|
+
outcome: "aborted",
|
|
494
|
+
finishReason: "deadline",
|
|
495
|
+
deltaCount: 0,
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const params = buildSlmParams(prompt, genOptions, deadlineMs);
|
|
500
|
+
const started = Date.now();
|
|
501
|
+
let deltaCount = 0;
|
|
502
|
+
try {
|
|
503
|
+
for await (const delta of toStreamDeltas(runtime.generateStream(params))) {
|
|
504
|
+
if (scoped.signal?.aborted) {
|
|
505
|
+
emitAdapter(scoped, locality, {
|
|
506
|
+
op: "generateStream",
|
|
507
|
+
outcome: "aborted",
|
|
508
|
+
finishReason: "deadline",
|
|
509
|
+
deltaCount,
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (Date.now() - started >= deadlineMs) {
|
|
514
|
+
emitAdapter(scoped, locality, {
|
|
515
|
+
op: "generateStream",
|
|
516
|
+
outcome: "deadline",
|
|
517
|
+
finishReason: "deadline",
|
|
518
|
+
deltaCount,
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
deltaCount += 1;
|
|
523
|
+
yield delta;
|
|
524
|
+
}
|
|
525
|
+
emitAdapter(scoped, locality, {
|
|
526
|
+
op: "generateStream",
|
|
527
|
+
outcome: "ok",
|
|
528
|
+
finishReason: "stop",
|
|
529
|
+
deltaCount,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
emitAdapter(scoped, locality, {
|
|
534
|
+
op: "generateStream",
|
|
535
|
+
outcome: "error",
|
|
536
|
+
deltaCount,
|
|
537
|
+
});
|
|
538
|
+
throw err;
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
async embed(text) {
|
|
542
|
+
try {
|
|
543
|
+
const vector = await runtime.embed(text);
|
|
544
|
+
const dim = vector.length;
|
|
545
|
+
if (embedDim === null) {
|
|
546
|
+
embedDim = dim;
|
|
547
|
+
}
|
|
548
|
+
else if (dim !== embedDim) {
|
|
549
|
+
emitAdapter(scoped, locality, {
|
|
550
|
+
op: "embed",
|
|
551
|
+
outcome: "embed_dim",
|
|
552
|
+
embedDim: dim,
|
|
553
|
+
});
|
|
554
|
+
throw new SlmModelAdapterError(`embed dimension ${dim} !== established ${embedDim}`, {
|
|
555
|
+
obligationId: EDGE_MODEL_OBLIGATION_EMBED_DIM,
|
|
556
|
+
failureClass: "contract",
|
|
557
|
+
errorCode: "EMBED_DIM_MISMATCH",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
emitAdapter(scoped, locality, {
|
|
561
|
+
op: "embed",
|
|
562
|
+
outcome: "ok",
|
|
563
|
+
embedDim: dim,
|
|
564
|
+
});
|
|
565
|
+
return vector;
|
|
566
|
+
}
|
|
567
|
+
catch (err) {
|
|
568
|
+
if (err instanceof SlmModelAdapterError)
|
|
569
|
+
throw err;
|
|
570
|
+
emitAdapter(scoped, locality, { op: "embed", outcome: "error" });
|
|
571
|
+
throw err;
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/* ── CK-03 harness surface (on-device, zero network) ──────── * ──
|
|
577
|
+
|
|
578
|
+
/** Stable embed width for on-device conformance runtime (matches CK-03 probes). */
|
|
579
|
+
export const EDGE_CONFORMANCE_EMBED_DIM = 8;
|
|
580
|
+
/**
|
|
581
|
+
* Deterministic assistant body derived from assembled prompt length — stream
|
|
582
|
+
* delta concatenation MUST equal this string (CK-03.2).
|
|
583
|
+
*/
|
|
584
|
+
export function conformanceSlmAssistantText(prompt) {
|
|
585
|
+
return `probe.ck03.edge.delta.${prompt.length}`;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Pure on-device SlmRuntime for CK-03 / locality proof — no network unless
|
|
589
|
+
* deliberately configured as a liar fixture.
|
|
590
|
+
*/
|
|
591
|
+
export function createOnDeviceConformanceSlmRuntime(options = {}) {
|
|
592
|
+
const card = {
|
|
593
|
+
modelId: "edge-ck03.conformance",
|
|
594
|
+
contextWindow: 4096,
|
|
595
|
+
quantization: "q4-test",
|
|
596
|
+
memoryFootprintMiB: 64,
|
|
597
|
+
languages: ["en"],
|
|
598
|
+
...options.card,
|
|
599
|
+
};
|
|
600
|
+
const egressUrl = options.egressUrl ?? "https://vendor.example/v1/infer";
|
|
601
|
+
return {
|
|
602
|
+
card,
|
|
603
|
+
async load() { },
|
|
604
|
+
async unload() { },
|
|
605
|
+
async generate(params) {
|
|
606
|
+
if (options.egressDuringGenerate) {
|
|
607
|
+
await fetch(egressUrl, { method: "POST", body: "{}" });
|
|
608
|
+
}
|
|
609
|
+
const text = conformanceSlmAssistantText(params.prompt);
|
|
610
|
+
return {
|
|
611
|
+
text,
|
|
612
|
+
tokensPerSecond: 120,
|
|
613
|
+
finishReason: "stop",
|
|
614
|
+
};
|
|
615
|
+
},
|
|
616
|
+
async *generateStream(params) {
|
|
617
|
+
if (options.egressDuringGenerate) {
|
|
618
|
+
await fetch(egressUrl, { method: "POST", body: "{}" });
|
|
619
|
+
}
|
|
620
|
+
const text = conformanceSlmAssistantText(params.prompt);
|
|
621
|
+
const mid = Math.max(1, Math.floor(text.length / 2));
|
|
622
|
+
yield text.slice(0, mid);
|
|
623
|
+
yield text.slice(mid);
|
|
624
|
+
},
|
|
625
|
+
async embed(text) {
|
|
626
|
+
const out = new Float32Array(EDGE_CONFORMANCE_EMBED_DIM);
|
|
627
|
+
let h = 0;
|
|
628
|
+
for (let i = 0; i < Math.min(text.length, 64); i++) {
|
|
629
|
+
h = (h * 31 + text.charCodeAt(i)) >>> 0;
|
|
630
|
+
}
|
|
631
|
+
for (let i = 0; i < EDGE_CONFORMANCE_EMBED_DIM; i++) {
|
|
632
|
+
out[i] = ((h + i * 17) % 1000) / 1000;
|
|
633
|
+
}
|
|
634
|
+
return out;
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Factory for `runConformance({ registry: createModelObligationsRegistry() })`.
|
|
640
|
+
* Honors runner FactoryContext.subjectId when provided.
|
|
641
|
+
*/
|
|
642
|
+
export function createSlmModelAdapterHarnessFactory(options = {}) {
|
|
643
|
+
return (ctx) => {
|
|
644
|
+
const subjectId = (ctx?.subjectId ?? options.subjectId ?? "").trim();
|
|
645
|
+
if (!subjectId) {
|
|
646
|
+
throw new SlmModelAdapterError("createSlmModelAdapterHarnessFactory requires subjectId", {
|
|
647
|
+
obligationId: EDGE_PROMPT_OBLIGATION_SUBJECT,
|
|
648
|
+
failureClass: "validation",
|
|
649
|
+
errorCode: "SUBJECT_REQUIRED",
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
let networkAllowed = true;
|
|
653
|
+
const runtime = options.createRuntime?.() ?? createOnDeviceConformanceSlmRuntime();
|
|
654
|
+
const model = createSlmModelAdapter(runtime, {
|
|
655
|
+
subjectId,
|
|
656
|
+
...(options.deviceId !== undefined
|
|
657
|
+
? { deviceId: options.deviceId }
|
|
658
|
+
: {}),
|
|
659
|
+
locality: options.locality ?? "on-device",
|
|
660
|
+
...(options.emit ? { emit: options.emit } : {}),
|
|
661
|
+
});
|
|
662
|
+
return {
|
|
663
|
+
model,
|
|
664
|
+
isNetworkAllowed: () => networkAllowed,
|
|
665
|
+
setNetworkAllowed: (allowed) => {
|
|
666
|
+
networkAllowed = allowed;
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/* ── LocalVectorDb → MemoryInterface ─────────────────────── * ──
|
|
672
|
+
|
|
673
|
+
/** Empty / missing subjectId on memory ops. */
|
|
674
|
+
export const EDGE_MEMORY_OBLIGATION_SUBJECT = "EDGE.MEMORY_SUBJECT_REQUIRED";
|
|
675
|
+
/** Embedding dimension mismatch (store vs query / write). */
|
|
676
|
+
export const EDGE_MEMORY_OBLIGATION_EMBED_DIM = "EDGE.MEMORY_EMBED_DIM";
|
|
677
|
+
/** Bounded recall result set (NFR). */
|
|
678
|
+
export const EDGE_MEMORY_RECALL_LIMIT = 64;
|
|
679
|
+
/** Bounded scan overshoot when kind-filtering post-search. */
|
|
680
|
+
export const EDGE_MEMORY_SCAN_LIMIT = 256;
|
|
681
|
+
const MEMORY_KINDS = new Set([
|
|
682
|
+
"correction",
|
|
683
|
+
"milestone",
|
|
684
|
+
"preference",
|
|
685
|
+
"episodic",
|
|
686
|
+
"semantic",
|
|
687
|
+
]);
|
|
688
|
+
export class LocalVectorMemoryError extends Error {
|
|
689
|
+
obligationId;
|
|
690
|
+
failureClass;
|
|
691
|
+
errorCode;
|
|
692
|
+
constructor(message, opts) {
|
|
693
|
+
super(message);
|
|
694
|
+
this.name = "LocalVectorMemoryError";
|
|
695
|
+
this.obligationId = opts.obligationId;
|
|
696
|
+
this.failureClass = opts.failureClass ?? "validation";
|
|
697
|
+
this.errorCode = opts.errorCode ?? opts.obligationId;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function emitMemory(options, partial) {
|
|
701
|
+
if (!options.emit)
|
|
702
|
+
return;
|
|
703
|
+
const event = {
|
|
704
|
+
event: "edge_agent.local_vector_memory",
|
|
705
|
+
subjectId: partial.subjectId,
|
|
706
|
+
op: partial.op,
|
|
707
|
+
outcome: partial.outcome,
|
|
708
|
+
};
|
|
709
|
+
if (options.deviceId !== undefined)
|
|
710
|
+
event.deviceId = options.deviceId;
|
|
711
|
+
if (partial.itemCount !== undefined)
|
|
712
|
+
event.itemCount = partial.itemCount;
|
|
713
|
+
if (partial.kind !== undefined)
|
|
714
|
+
event.kind = partial.kind;
|
|
715
|
+
if (partial.correctionCount !== undefined) {
|
|
716
|
+
event.correctionCount = partial.correctionCount;
|
|
717
|
+
}
|
|
718
|
+
if (partial.episodicCount !== undefined) {
|
|
719
|
+
event.episodicCount = partial.episodicCount;
|
|
720
|
+
}
|
|
721
|
+
options.emit(event);
|
|
722
|
+
}
|
|
723
|
+
function toVectorKind(kind) {
|
|
724
|
+
if (!MEMORY_KINDS.has(kind)) {
|
|
725
|
+
throw new LocalVectorMemoryError(`unsupported MemoryKind: ${String(kind)}`, {
|
|
726
|
+
obligationId: "EDGE.MEMORY_INVALID_KIND",
|
|
727
|
+
failureClass: "validation",
|
|
728
|
+
errorCode: "INVALID_KIND",
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return kind;
|
|
732
|
+
}
|
|
733
|
+
function toMemoryItem(record) {
|
|
734
|
+
return {
|
|
735
|
+
id: record.id,
|
|
736
|
+
subjectId: record.subjectId,
|
|
737
|
+
topicId: record.conceptId,
|
|
738
|
+
text: record.text,
|
|
739
|
+
kind: record.kind,
|
|
740
|
+
createdAt: record.createdAt,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function clampScore(score) {
|
|
744
|
+
if (!Number.isFinite(score))
|
|
745
|
+
return 0;
|
|
746
|
+
return Math.max(0, Math.min(1, score));
|
|
747
|
+
}
|
|
748
|
+
async function withSubjectMemoryLock(locks, subjectId, fn) {
|
|
749
|
+
const prev = locks.get(subjectId) ?? Promise.resolve();
|
|
750
|
+
let release;
|
|
751
|
+
const gate = new Promise((r) => {
|
|
752
|
+
release = r;
|
|
753
|
+
});
|
|
754
|
+
const next = prev.then(() => gate);
|
|
755
|
+
locks.set(subjectId, next);
|
|
756
|
+
await prev.catch(() => undefined);
|
|
757
|
+
try {
|
|
758
|
+
return await fn();
|
|
759
|
+
}
|
|
760
|
+
finally {
|
|
761
|
+
release();
|
|
762
|
+
if (locks.get(subjectId) === next)
|
|
763
|
+
locks.delete(subjectId);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Adapt LocalVectorDb to MemoryInterface.
|
|
768
|
+
* topicId ↔ conceptId; MemoryKind ↔ VectorMemoryKind (incl. semantic).
|
|
769
|
+
* remember embeds via injected embed() and awaits durable upsert before resolve.
|
|
770
|
+
*/
|
|
771
|
+
export function createLocalVectorMemoryAdapter(db, options) {
|
|
772
|
+
if (typeof options.embed !== "function") {
|
|
773
|
+
throw new LocalVectorMemoryError("createLocalVectorMemoryAdapter requires embed()", {
|
|
774
|
+
obligationId: "EDGE.MEMORY_EMBED_REQUIRED",
|
|
775
|
+
failureClass: "config",
|
|
776
|
+
errorCode: "EMBED_REQUIRED",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
const subjectLocks = new Map();
|
|
780
|
+
return {
|
|
781
|
+
async remember(item) {
|
|
782
|
+
const subjectId = item.subjectId?.trim() ?? "";
|
|
783
|
+
if (!subjectId) {
|
|
784
|
+
emitMemory(options, {
|
|
785
|
+
subjectId: "",
|
|
786
|
+
op: "remember",
|
|
787
|
+
outcome: "subject_required",
|
|
788
|
+
});
|
|
789
|
+
throw new LocalVectorMemoryError("MemoryItem.subjectId is required (subject isolation)", {
|
|
790
|
+
obligationId: EDGE_MEMORY_OBLIGATION_SUBJECT,
|
|
791
|
+
failureClass: "validation",
|
|
792
|
+
errorCode: "SUBJECT_REQUIRED",
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
const topicId = item.topicId?.trim() ?? "";
|
|
796
|
+
if (!topicId) {
|
|
797
|
+
throw new LocalVectorMemoryError("MemoryItem.topicId is required", {
|
|
798
|
+
obligationId: "EDGE.MEMORY_TOPIC_REQUIRED",
|
|
799
|
+
failureClass: "validation",
|
|
800
|
+
errorCode: "TOPIC_REQUIRED",
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return withSubjectMemoryLock(subjectLocks, subjectId, async () => {
|
|
804
|
+
try {
|
|
805
|
+
const kind = toVectorKind(item.kind);
|
|
806
|
+
const vector = await options.embed(item.text);
|
|
807
|
+
const id = `mem-${crypto.randomUUID()}`;
|
|
808
|
+
const createdAt = (item.createdAt?.trim() ||
|
|
809
|
+
new Date().toISOString());
|
|
810
|
+
// Durable upsert BEFORE resolve (CK-02.1).
|
|
811
|
+
await db.upsert({
|
|
812
|
+
id,
|
|
813
|
+
subjectId,
|
|
814
|
+
conceptId: topicId,
|
|
815
|
+
text: item.text,
|
|
816
|
+
vector,
|
|
817
|
+
kind,
|
|
818
|
+
createdAt,
|
|
819
|
+
});
|
|
820
|
+
const stored = {
|
|
821
|
+
id,
|
|
822
|
+
subjectId,
|
|
823
|
+
topicId,
|
|
824
|
+
text: item.text,
|
|
825
|
+
kind: item.kind,
|
|
826
|
+
createdAt,
|
|
827
|
+
...(item.relatedIds !== undefined
|
|
828
|
+
? { relatedIds: item.relatedIds }
|
|
829
|
+
: {}),
|
|
830
|
+
...(item.metadata !== undefined ? { metadata: item.metadata } : {}),
|
|
831
|
+
};
|
|
832
|
+
emitMemory(options, {
|
|
833
|
+
subjectId,
|
|
834
|
+
op: "remember",
|
|
835
|
+
outcome: "ok",
|
|
836
|
+
itemCount: 1,
|
|
837
|
+
kind: item.kind,
|
|
838
|
+
});
|
|
839
|
+
return stored;
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
if (err instanceof LocalVectorMemoryError)
|
|
843
|
+
throw err;
|
|
844
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
845
|
+
if (/dimension/i.test(message)) {
|
|
846
|
+
emitMemory(options, {
|
|
847
|
+
subjectId,
|
|
848
|
+
op: "remember",
|
|
849
|
+
outcome: "embed_dim",
|
|
850
|
+
});
|
|
851
|
+
throw new LocalVectorMemoryError(message, {
|
|
852
|
+
obligationId: EDGE_MEMORY_OBLIGATION_EMBED_DIM,
|
|
853
|
+
failureClass: "contract",
|
|
854
|
+
errorCode: "EMBED_DIM_MISMATCH",
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
emitMemory(options, {
|
|
858
|
+
subjectId,
|
|
859
|
+
op: "remember",
|
|
860
|
+
outcome: "error",
|
|
861
|
+
});
|
|
862
|
+
throw err;
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
},
|
|
866
|
+
async recall(query) {
|
|
867
|
+
const subjectId = query.subjectId?.trim() ?? "";
|
|
868
|
+
if (!subjectId) {
|
|
869
|
+
emitMemory(options, {
|
|
870
|
+
subjectId: "",
|
|
871
|
+
op: "recall",
|
|
872
|
+
outcome: "subject_required",
|
|
873
|
+
});
|
|
874
|
+
throw new LocalVectorMemoryError("MemoryQuery.subjectId is required (subject isolation)", {
|
|
875
|
+
obligationId: EDGE_MEMORY_OBLIGATION_SUBJECT,
|
|
876
|
+
failureClass: "validation",
|
|
877
|
+
errorCode: "SUBJECT_REQUIRED",
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
const limit = Math.min(Math.max(1, query.limit ?? 16), EDGE_MEMORY_RECALL_LIMIT);
|
|
881
|
+
try {
|
|
882
|
+
const vector = await options.embed(query.query);
|
|
883
|
+
const fetchLimit = query.kinds !== undefined
|
|
884
|
+
? Math.min(EDGE_MEMORY_SCAN_LIMIT, Math.max(limit * 4, limit))
|
|
885
|
+
: limit;
|
|
886
|
+
const hits = await db.search(subjectId, vector, {
|
|
887
|
+
...(query.topicId !== undefined
|
|
888
|
+
? { conceptId: query.topicId }
|
|
889
|
+
: {}),
|
|
890
|
+
limit: fetchLimit,
|
|
891
|
+
});
|
|
892
|
+
let mapped = hits.map((h) => ({
|
|
893
|
+
item: toMemoryItem(h.record),
|
|
894
|
+
score: clampScore(h.score),
|
|
895
|
+
}));
|
|
896
|
+
if (query.kinds !== undefined) {
|
|
897
|
+
const allowed = new Set(query.kinds);
|
|
898
|
+
mapped = mapped.filter((h) => allowed.has(h.item.kind));
|
|
899
|
+
}
|
|
900
|
+
const out = mapped.slice(0, limit);
|
|
901
|
+
emitMemory(options, {
|
|
902
|
+
subjectId,
|
|
903
|
+
op: "recall",
|
|
904
|
+
outcome: out.length === 0 ? "empty" : "ok",
|
|
905
|
+
itemCount: out.length,
|
|
906
|
+
correctionCount: out.filter((h) => h.item.kind === "correction")
|
|
907
|
+
.length,
|
|
908
|
+
episodicCount: out.filter((h) => h.item.kind === "episodic").length,
|
|
909
|
+
});
|
|
910
|
+
return out;
|
|
911
|
+
}
|
|
912
|
+
catch (err) {
|
|
913
|
+
if (err instanceof LocalVectorMemoryError)
|
|
914
|
+
throw err;
|
|
915
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
916
|
+
if (/dimension/i.test(message)) {
|
|
917
|
+
emitMemory(options, {
|
|
918
|
+
subjectId,
|
|
919
|
+
op: "recall",
|
|
920
|
+
outcome: "embed_dim",
|
|
921
|
+
});
|
|
922
|
+
throw new LocalVectorMemoryError(message, {
|
|
923
|
+
obligationId: EDGE_MEMORY_OBLIGATION_EMBED_DIM,
|
|
924
|
+
failureClass: "contract",
|
|
925
|
+
errorCode: "EMBED_DIM_MISMATCH",
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
emitMemory(options, {
|
|
929
|
+
subjectId,
|
|
930
|
+
op: "recall",
|
|
931
|
+
outcome: "error",
|
|
932
|
+
});
|
|
933
|
+
throw err;
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
async associate(_fromId, _toId, _relation) {
|
|
937
|
+
// Pure vector store — graph edges are a no-op (MemoryInterface contract).
|
|
938
|
+
emitMemory(options, {
|
|
939
|
+
subjectId: "*",
|
|
940
|
+
op: "associate",
|
|
941
|
+
outcome: "ok",
|
|
942
|
+
itemCount: 0,
|
|
943
|
+
});
|
|
944
|
+
},
|
|
945
|
+
async forget(id) {
|
|
946
|
+
const key = id?.trim() ?? "";
|
|
947
|
+
if (!key) {
|
|
948
|
+
throw new LocalVectorMemoryError("forget requires id", {
|
|
949
|
+
obligationId: "EDGE.MEMORY_ID_REQUIRED",
|
|
950
|
+
failureClass: "validation",
|
|
951
|
+
errorCode: "ID_REQUIRED",
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
await db.deleteById(key);
|
|
955
|
+
emitMemory(options, {
|
|
956
|
+
subjectId: "*",
|
|
957
|
+
op: "forget",
|
|
958
|
+
outcome: "ok",
|
|
959
|
+
itemCount: 1,
|
|
960
|
+
});
|
|
961
|
+
},
|
|
962
|
+
async compact(subjectId, olderThanDays) {
|
|
963
|
+
const sid = subjectId?.trim() ?? "";
|
|
964
|
+
if (!sid) {
|
|
965
|
+
throw new LocalVectorMemoryError("compact requires subjectId (subject isolation)", {
|
|
966
|
+
obligationId: EDGE_MEMORY_OBLIGATION_SUBJECT,
|
|
967
|
+
failureClass: "validation",
|
|
968
|
+
errorCode: "SUBJECT_REQUIRED",
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
const days = Number.isFinite(olderThanDays)
|
|
972
|
+
? Math.max(0, olderThanDays)
|
|
973
|
+
: 0;
|
|
974
|
+
const cutoffMs = (options.nowMs?.() ?? Date.now()) - days * 86_400_000;
|
|
975
|
+
const cutoff = `${String(Math.floor(cutoffMs)).padStart(15, "0")}:000000:compact`;
|
|
976
|
+
const n = await db.pruneEpisodicOlderThan(cutoff, sid);
|
|
977
|
+
emitMemory(options, {
|
|
978
|
+
subjectId: sid,
|
|
979
|
+
op: "compact",
|
|
980
|
+
outcome: "ok",
|
|
981
|
+
itemCount: n,
|
|
982
|
+
});
|
|
983
|
+
return n;
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Shared durable substrate + injectable clock for CK-02 probes.
|
|
989
|
+
* reinstantiate opens a fresh MemoryInterface over the same driver rows.
|
|
990
|
+
*/
|
|
991
|
+
export function createLocalVectorMemoryHarnessFactory(options = {}) {
|
|
992
|
+
const embedDim = options.embedDim ?? EDGE_CONFORMANCE_EMBED_DIM;
|
|
993
|
+
const halfLifeDays = options.episodicHalfLifeDays ?? EPISODIC_HALF_LIFE_DAYS;
|
|
994
|
+
return () => {
|
|
995
|
+
let now = 1_700_000_000_000;
|
|
996
|
+
const driver = createLocalVectorMemoryDriver();
|
|
997
|
+
const embed = async (text) => {
|
|
998
|
+
const out = new Float32Array(embedDim);
|
|
999
|
+
let h = 0;
|
|
1000
|
+
for (let i = 0; i < Math.min(text.length, 96); i++) {
|
|
1001
|
+
h = (h * 31 + text.charCodeAt(i)) >>> 0;
|
|
1002
|
+
}
|
|
1003
|
+
for (let i = 0; i < embedDim; i++) {
|
|
1004
|
+
out[i] = ((h + i * 17) % 1000) / 1000;
|
|
1005
|
+
}
|
|
1006
|
+
// Make probe.decay.* tokens cluster for relevance fairness.
|
|
1007
|
+
if (text.includes("probe.decay")) {
|
|
1008
|
+
out[0] = 0.91;
|
|
1009
|
+
out[1] = 0.87;
|
|
1010
|
+
}
|
|
1011
|
+
return out;
|
|
1012
|
+
};
|
|
1013
|
+
const openDb = () => new LocalVectorDb(driver, {
|
|
1014
|
+
episodicHalfLifeDays: halfLifeDays,
|
|
1015
|
+
nowMs: () => now,
|
|
1016
|
+
});
|
|
1017
|
+
const openMemory = (db) => createLocalVectorMemoryAdapter(db, {
|
|
1018
|
+
...(options.deviceId !== undefined
|
|
1019
|
+
? { deviceId: options.deviceId }
|
|
1020
|
+
: {}),
|
|
1021
|
+
embed,
|
|
1022
|
+
nowMs: () => now,
|
|
1023
|
+
episodicHalfLifeDays: halfLifeDays,
|
|
1024
|
+
...(options.emit ? { emit: options.emit } : {}),
|
|
1025
|
+
});
|
|
1026
|
+
const bootstrap = openDb();
|
|
1027
|
+
// In-memory driver treats CREATE as no-op; still honor the initialize seam.
|
|
1028
|
+
const initPromise = bootstrap.initialize();
|
|
1029
|
+
return {
|
|
1030
|
+
memory: openMemory(bootstrap),
|
|
1031
|
+
async reinstantiate() {
|
|
1032
|
+
await initPromise;
|
|
1033
|
+
const db = openDb();
|
|
1034
|
+
await db.initialize();
|
|
1035
|
+
return openMemory(db);
|
|
1036
|
+
},
|
|
1037
|
+
nowMs: () => now,
|
|
1038
|
+
setNowMs: (ms) => {
|
|
1039
|
+
now = ms;
|
|
1040
|
+
},
|
|
1041
|
+
};
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
/* ── CognitiveBindings factory for EdgeAgent ───────────── * ──
|
|
1045
|
+
|
|
1046
|
+
/** subjectId / deviceId missing when assembling edge bindings. */
|
|
1047
|
+
export const EDGE_BINDINGS_OBLIGATION_SUBJECT = "EDGE.BINDINGS_SUBJECT_REQUIRED";
|
|
1048
|
+
/**
|
|
1049
|
+
* Map edge conceptId (task-graph / mastery key) → MemoryInterface.topicId.
|
|
1050
|
+
* Empty / null → stable on-device default (never invent foreign ids).
|
|
1051
|
+
*/
|
|
1052
|
+
export function mapConceptIdToTopicId(conceptId) {
|
|
1053
|
+
const trimmed = conceptId?.trim() ?? "";
|
|
1054
|
+
return trimmed.length > 0 ? trimmed : "edge.general";
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Map CognitiveState.profile.track (task-graph vocabulary) → AgentProfile.domainId.
|
|
1058
|
+
*/
|
|
1059
|
+
export function mapTrackToDomainId(track) {
|
|
1060
|
+
const trimmed = track?.trim() ?? "";
|
|
1061
|
+
return trimmed.length > 0 ? `edge.${trimmed}` : "edge.offline";
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Coerce host/test runtimes that still expose `descriptor` (pre-card) into a
|
|
1065
|
+
* full {@link SlmRuntime} for {@link createSlmModelAdapter}. Never invents
|
|
1066
|
+
* network paths — only fills card / stream / unload gaps locally.
|
|
1067
|
+
*/
|
|
1068
|
+
export function coerceSlmRuntimeForBindings(runtime) {
|
|
1069
|
+
const r = runtime;
|
|
1070
|
+
const src = r.card ?? r.descriptor;
|
|
1071
|
+
const card = {
|
|
1072
|
+
modelId: src?.modelId?.trim() || "edge-slm",
|
|
1073
|
+
contextWindow: typeof src?.contextWindow === "number" && src.contextWindow > 0
|
|
1074
|
+
? src.contextWindow
|
|
1075
|
+
: 4096,
|
|
1076
|
+
quantization: src?.quantization?.trim() || "unknown",
|
|
1077
|
+
memoryFootprintMiB: typeof src?.memoryFootprintMiB === "number" &&
|
|
1078
|
+
Number.isFinite(src.memoryFootprintMiB)
|
|
1079
|
+
? src.memoryFootprintMiB
|
|
1080
|
+
: 0,
|
|
1081
|
+
languages: Array.isArray(src?.languages) && src.languages.length > 0
|
|
1082
|
+
? [...src.languages]
|
|
1083
|
+
: ["en"],
|
|
1084
|
+
};
|
|
1085
|
+
const generateStream = typeof r.generateStream === "function"
|
|
1086
|
+
? r.generateStream.bind(r)
|
|
1087
|
+
: async function* (params) {
|
|
1088
|
+
const result = await r.generate(params);
|
|
1089
|
+
if (result.text)
|
|
1090
|
+
yield result.text;
|
|
1091
|
+
};
|
|
1092
|
+
const unload = typeof r.unload === "function" ? r.unload.bind(r) : async () => { };
|
|
1093
|
+
return {
|
|
1094
|
+
card,
|
|
1095
|
+
load: r.load.bind(r),
|
|
1096
|
+
unload,
|
|
1097
|
+
generate: r.generate.bind(r),
|
|
1098
|
+
generateStream,
|
|
1099
|
+
embed: r.embed.bind(r),
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Prefer edge concept topicId on remember so CognitiveCore reflect episodes
|
|
1104
|
+
* land under the active concept (Core passes domainId by default).
|
|
1105
|
+
*/
|
|
1106
|
+
function wrapMemoryWithConceptTopic(memory, topicId) {
|
|
1107
|
+
return {
|
|
1108
|
+
remember(item) {
|
|
1109
|
+
const effective = topicId !== "edge.general" ? topicId : item.topicId;
|
|
1110
|
+
return memory.remember({ ...item, topicId: effective });
|
|
1111
|
+
},
|
|
1112
|
+
recall: (query) => memory.recall(query),
|
|
1113
|
+
associate: (fromId, toId, relation) => memory.associate(fromId, toId, relation),
|
|
1114
|
+
forget: (id) => memory.forget(id),
|
|
1115
|
+
compact: (subjectId, olderThanDays) => memory.compact(subjectId, olderThanDays),
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
export function createEdgeAgentProfile(input) {
|
|
1119
|
+
const language = input.language.trim() || "en";
|
|
1120
|
+
const domainId = mapTrackToDomainId(input.track);
|
|
1121
|
+
return {
|
|
1122
|
+
domainId,
|
|
1123
|
+
charter: input.charter?.trim() ||
|
|
1124
|
+
`You are an autonomous on-device cognitive agent for domain ${domainId}. Prefer local evidence; never claim external network access.`,
|
|
1125
|
+
refusals: input.refusals ?? [],
|
|
1126
|
+
languages: [language],
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function createEdgeReasoningStub() {
|
|
1130
|
+
return {
|
|
1131
|
+
async deliberate(request) {
|
|
1132
|
+
const constraints = (request.constraints ?? []).slice(0, 32);
|
|
1133
|
+
const evidenceCount = Math.min(request.evidence.length, 16);
|
|
1134
|
+
return {
|
|
1135
|
+
conclusion: evidenceCount > 0
|
|
1136
|
+
? `On-device conclusion grounded in ${evidenceCount} evidence item(s).`
|
|
1137
|
+
: "On-device conclusion with no retrieved evidence.",
|
|
1138
|
+
confidence: evidenceCount > 0 ? 0.72 : 0.55,
|
|
1139
|
+
steps: [
|
|
1140
|
+
{
|
|
1141
|
+
kind: "inference",
|
|
1142
|
+
statement: "Edge reasoning stub synthesized a local conclusion.",
|
|
1143
|
+
evidenceRefs: evidenceCount > 0
|
|
1144
|
+
? Array.from({ length: Math.min(evidenceCount, 4) }, (_, i) => i)
|
|
1145
|
+
: [],
|
|
1146
|
+
},
|
|
1147
|
+
],
|
|
1148
|
+
unresolvedConstraints: constraints,
|
|
1149
|
+
};
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function createEdgePlanningStub() {
|
|
1154
|
+
return {
|
|
1155
|
+
async compose(goals, _context) {
|
|
1156
|
+
const steps = goals.slice(0, 8).map((g, i) => ({
|
|
1157
|
+
stepId: `edge-step-${i + 1}`,
|
|
1158
|
+
goalId: g.goalId,
|
|
1159
|
+
action: g.description.slice(0, 120),
|
|
1160
|
+
dependsOn: g.prerequisites.slice(0, 8),
|
|
1161
|
+
status: "pending",
|
|
1162
|
+
}));
|
|
1163
|
+
return {
|
|
1164
|
+
planId: `edge-plan-${crypto.randomUUID().slice(0, 8)}`,
|
|
1165
|
+
steps,
|
|
1166
|
+
rationale: "edge-default-compose",
|
|
1167
|
+
};
|
|
1168
|
+
},
|
|
1169
|
+
async revise(plan, event) {
|
|
1170
|
+
return {
|
|
1171
|
+
...plan,
|
|
1172
|
+
rationale: `edge-revise:${event.severity}:${event.observation.slice(0, 64)}`,
|
|
1173
|
+
};
|
|
1174
|
+
},
|
|
1175
|
+
nextStep(plan) {
|
|
1176
|
+
return plan.steps.find((s) => s.status === "pending") ?? null;
|
|
1177
|
+
},
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function createEdgeToolsStub() {
|
|
1181
|
+
return {
|
|
1182
|
+
list: () => [],
|
|
1183
|
+
async invoke(invocation) {
|
|
1184
|
+
return {
|
|
1185
|
+
invocationId: invocation.invocationId,
|
|
1186
|
+
status: "error",
|
|
1187
|
+
output: { message: "no edge tools bound" },
|
|
1188
|
+
latencyMs: 0,
|
|
1189
|
+
};
|
|
1190
|
+
},
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function createEdgeKnowledgeStub() {
|
|
1194
|
+
return {
|
|
1195
|
+
sources: [
|
|
1196
|
+
{
|
|
1197
|
+
sourceId: "edge.bundled",
|
|
1198
|
+
title: "On-device bundled index",
|
|
1199
|
+
domain: "edge",
|
|
1200
|
+
locality: "bundled-offline",
|
|
1201
|
+
coverage: { from: "1970-01-01", to: "9999-12-31" },
|
|
1202
|
+
},
|
|
1203
|
+
],
|
|
1204
|
+
async retrieve() {
|
|
1205
|
+
// Offline-first: empty passages beat fabricated remote hits.
|
|
1206
|
+
return [];
|
|
1207
|
+
},
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Assemble CognitiveBindings from edge config + Slm/LocalVector adapters.
|
|
1212
|
+
* Does not call CognitiveCore.turn — wires the loop.
|
|
1213
|
+
*/
|
|
1214
|
+
export function createEdgeCognitiveBindings(options) {
|
|
1215
|
+
const subjectId = options.subjectId?.trim() ?? "";
|
|
1216
|
+
const deviceId = options.deviceId?.trim() ?? "";
|
|
1217
|
+
if (!subjectId || !deviceId) {
|
|
1218
|
+
options.emit?.({
|
|
1219
|
+
event: "edge_agent.cognitive_bindings",
|
|
1220
|
+
subjectId: subjectId || "",
|
|
1221
|
+
...(deviceId ? { deviceId } : {}),
|
|
1222
|
+
outcome: "subject_required",
|
|
1223
|
+
domainId: mapTrackToDomainId(options.track),
|
|
1224
|
+
hasActiveConcept: false,
|
|
1225
|
+
servedLocally: true,
|
|
1226
|
+
});
|
|
1227
|
+
throw new SlmModelAdapterError("createEdgeCognitiveBindings requires subjectId and deviceId", {
|
|
1228
|
+
obligationId: EDGE_BINDINGS_OBLIGATION_SUBJECT,
|
|
1229
|
+
failureClass: "validation",
|
|
1230
|
+
errorCode: "SUBJECT_REQUIRED",
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
if (!options.runtime || typeof options.runtime.generate !== "function") {
|
|
1234
|
+
throw new SlmModelAdapterError("SlmRuntime is required for edge bindings", {
|
|
1235
|
+
obligationId: EDGE_MODEL_OBLIGATION_INIT,
|
|
1236
|
+
failureClass: "config",
|
|
1237
|
+
errorCode: "RUNTIME_REQUIRED",
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
if (!options.vectorDb) {
|
|
1241
|
+
throw new LocalVectorMemoryError("LocalVectorDb is required for edge bindings", {
|
|
1242
|
+
obligationId: "EDGE.BINDINGS_MEMORY_REQUIRED",
|
|
1243
|
+
failureClass: "config",
|
|
1244
|
+
errorCode: "MEMORY_REQUIRED",
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
const profile = createEdgeAgentProfile({
|
|
1248
|
+
track: options.track,
|
|
1249
|
+
language: options.language,
|
|
1250
|
+
...(options.charter !== undefined ? { charter: options.charter } : {}),
|
|
1251
|
+
...(options.refusals !== undefined ? { refusals: options.refusals } : {}),
|
|
1252
|
+
});
|
|
1253
|
+
const topicId = mapConceptIdToTopicId(options.activeConceptId);
|
|
1254
|
+
const runtime = coerceSlmRuntimeForBindings(options.runtime);
|
|
1255
|
+
const model = createSlmModelAdapter(runtime, {
|
|
1256
|
+
subjectId,
|
|
1257
|
+
deviceId,
|
|
1258
|
+
locality: options.locality ?? "on-device",
|
|
1259
|
+
...(options.defaultDeadlineMs !== undefined
|
|
1260
|
+
? { defaultDeadlineMs: options.defaultDeadlineMs }
|
|
1261
|
+
: {}),
|
|
1262
|
+
...(options.emit ? { emit: options.emit } : {}),
|
|
1263
|
+
});
|
|
1264
|
+
const baseMemory = createLocalVectorMemoryAdapter(options.vectorDb, {
|
|
1265
|
+
deviceId,
|
|
1266
|
+
embed: (text) => runtime.embed(text),
|
|
1267
|
+
...(options.emit ? { emit: options.emit } : {}),
|
|
1268
|
+
});
|
|
1269
|
+
const memory = wrapMemoryWithConceptTopic(baseMemory, topicId);
|
|
1270
|
+
const bindings = {
|
|
1271
|
+
memory,
|
|
1272
|
+
model,
|
|
1273
|
+
reasoning: options.reasoning ?? createEdgeReasoningStub(),
|
|
1274
|
+
planning: options.planning ?? createEdgePlanningStub(),
|
|
1275
|
+
tools: options.tools ?? createEdgeToolsStub(),
|
|
1276
|
+
knowledge: options.knowledge ?? createEdgeKnowledgeStub(),
|
|
1277
|
+
...(options.speech ? { speech: options.speech } : {}),
|
|
1278
|
+
...(options.vision ? { vision: options.vision } : {}),
|
|
1279
|
+
};
|
|
1280
|
+
options.emit?.({
|
|
1281
|
+
event: "edge_agent.cognitive_bindings",
|
|
1282
|
+
subjectId,
|
|
1283
|
+
deviceId,
|
|
1284
|
+
outcome: "ok",
|
|
1285
|
+
domainId: profile.domainId,
|
|
1286
|
+
hasActiveConcept: topicId !== "edge.general",
|
|
1287
|
+
hasSpeech: Boolean(options.speech),
|
|
1288
|
+
hasVision: Boolean(options.vision),
|
|
1289
|
+
servedLocally: true,
|
|
1290
|
+
});
|
|
1291
|
+
return { bindings, profile, topicId };
|
|
1292
|
+
}
|
|
1293
|
+
//# sourceMappingURL=cognitive_bindings.js.map
|