@remnic/core 9.25.2 → 9.25.4

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.
@@ -41,7 +41,7 @@ import {
41
41
  throwIfRecallAborted,
42
42
  tokenizeRecallQuery,
43
43
  utcDateKeysForLocalDay
44
- } from "./chunk-LFHMUWA2.js";
44
+ } from "./chunk-FDAPXLRC.js";
45
45
  import "./chunk-4B77XSG5.js";
46
46
  import {
47
47
  formatCompressionGuidelinesForRecall
@@ -104,9 +104,9 @@ import "./chunk-PHRLJFWZ.js";
104
104
  import "./chunk-64NJRYU2.js";
105
105
  import "./chunk-K2TDK7GG.js";
106
106
  import "./chunk-LTHVM4XN.js";
107
- import "./chunk-GBAJCDTW.js";
107
+ import "./chunk-BUK2FXOL.js";
108
108
  import "./chunk-4RA3C3EV.js";
109
- import "./chunk-5GSUBCZM.js";
109
+ import "./chunk-TGTTESJS.js";
110
110
  import "./chunk-54V4BZWP.js";
111
111
  import "./chunk-2CGLBUW3.js";
112
112
  import "./chunk-WLZYGLJ4.js";
package/dist/schemas.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  buildProfileConsolidationResultSchema,
24
24
  parseMemoryActionEligibilityContext,
25
25
  parseMemoryActionType
26
- } from "./chunk-5GSUBCZM.js";
26
+ } from "./chunk-TGTTESJS.js";
27
27
  import "./chunk-PZ5AY32C.js";
28
28
  export {
29
29
  BehaviorLoopAdjustmentSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.25.2",
3
+ "version": "9.25.4",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3056,7 +3056,7 @@
3056
3056
  "core"
3057
3057
  ],
3058
3058
  "peerDependencies": {
3059
- "@remnic/coding-graph": "^9.25.2"
3059
+ "@remnic/coding-graph": "^9.25.4"
3060
3060
  },
3061
3061
  "peerDependenciesMeta": {
3062
3062
  "@remnic/coding-graph": {
@@ -0,0 +1,335 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseConfig } from "./config.js";
5
+ import { ExtractionEngine } from "./extraction.js";
6
+ import { ExtractionResultSchema } from "./schemas.js";
7
+ import type { BufferTurn, ExtractionResult } from "./types.js";
8
+
9
+ const SOURCE_TURN: BufferTurn = {
10
+ role: "user",
11
+ content: "Moonlight's theme color is green, not blue.",
12
+ timestamp: "2026-07-25T12:00:00.000Z",
13
+ };
14
+
15
+ const TAG_SOURCE_TURN: BufferTurn = {
16
+ ...SOURCE_TURN,
17
+ content: "The page uses <main> as the primary landmark.",
18
+ };
19
+
20
+ const SOURCE_GROUNDED_EXTRACTION = {
21
+ facts: [{
22
+ category: "fact",
23
+ content: SOURCE_TURN.content,
24
+ confidence: 0.95,
25
+ tags: ["theme"],
26
+ }],
27
+ profileUpdates: [],
28
+ entities: [],
29
+ questions: [],
30
+ };
31
+ const STRUCTURAL_PLACEHOLDER_EXTRACTION = {
32
+ facts: [{
33
+ category: "<category>",
34
+ content: "<source-grounded statement>",
35
+ confidence: 0,
36
+ tags: ["<tag>"],
37
+ entityRef: "<optional normalized-name>",
38
+ promptedByQuestion: "<optional source-grounded question>",
39
+ quote: "<optional exact contiguous source span>",
40
+ },
41
+ {
42
+ category: "<category>",
43
+ content: SOURCE_TURN.content,
44
+ confidence: 0.95,
45
+ tags: ["theme"],
46
+ },
47
+ {
48
+ category: "procedure",
49
+ content: SOURCE_TURN.content,
50
+ confidence: 0.95,
51
+ tags: ["theme"],
52
+ procedureSteps: [
53
+ { order: 1, intent: "<step>" },
54
+ { order: 2, intent: "<step>" },
55
+ ],
56
+ },
57
+ {
58
+ category: "reasoning_trace",
59
+ content: SOURCE_TURN.content,
60
+ confidence: 0.95,
61
+ tags: ["theme"],
62
+ reasoningTrace: {
63
+ steps: [
64
+ { order: 1, description: "<step>" },
65
+ { order: 2, description: "<step>" },
66
+ ],
67
+ finalAnswer: "<answer>",
68
+ },
69
+ }],
70
+ profileUpdates: ["<source-grounded profile update>"],
71
+ entities: [{
72
+ name: "<normalized-name>",
73
+ type: "<entity-type>",
74
+ facts: ["<source-grounded statement>"],
75
+ promptedByQuestion: "<optional source-grounded question>",
76
+ },
77
+ {
78
+ name: "Moonlight",
79
+ type: "<entity-type>",
80
+ facts: [SOURCE_TURN.content],
81
+ }],
82
+ questions: [{
83
+ question: "<source-grounded unresolved question>",
84
+ context: "<source-grounded context>",
85
+ priority: 0,
86
+ }],
87
+ identityReflection: "<conversation-grounded agent reflection>",
88
+ relationships: [{
89
+ source: "<normalized-name>",
90
+ target: "<normalized-name>",
91
+ label: "<source-grounded relationship>",
92
+ }],
93
+ };
94
+
95
+ const GATEWAY_PLACEHOLDER_EXTRACTION = {
96
+ ...STRUCTURAL_PLACEHOLDER_EXTRACTION,
97
+ facts: [{
98
+ ...STRUCTURAL_PLACEHOLDER_EXTRACTION.facts[0],
99
+ category: "fact",
100
+ }],
101
+ entities: [{
102
+ ...STRUCTURAL_PLACEHOLDER_EXTRACTION.entities[0],
103
+ type: "other",
104
+ }],
105
+ };
106
+
107
+ type ChatMessage = { role: string; content: string };
108
+
109
+ type LocalLlmFixture = {
110
+ chatCompletion(messages: ChatMessage[]): Promise<{ content: string }>;
111
+ };
112
+
113
+ type GatewayFixture = {
114
+ parseWithSchemaDetailed(messages: ChatMessage[]): Promise<unknown>;
115
+ };
116
+
117
+ type DirectClientFixture = {
118
+ chat: {
119
+ completions: {
120
+ create(request: { messages: ChatMessage[] }): Promise<unknown>;
121
+ };
122
+ };
123
+ };
124
+
125
+ function assertSafeExtractionPrompt(prompt: string): void {
126
+ assert.match(prompt, /Questions are optional/i);
127
+ assert.match(prompt, /return an empty array/i);
128
+ assert.match(prompt, /\[context user\].*reference context only|reference context only.*\[context user\]/is);
129
+ assert.match(prompt, /may resolve references or complete a question-and-answer pair/i);
130
+ assert.match(prompt, /Explicit \(0\.95-1\.0\).*Speculative \(0\.00-0\.39\)/is);
131
+ assert.match(prompt, /operational noise/i);
132
+ assert.doesNotMatch(prompt, /dark mode|cloud provider|staging environment/i);
133
+ assert.doesNotMatch(prompt, /React over Vue|rate limiting at 1000/i);
134
+ }
135
+
136
+ function assertStructuralResponseShape(prompt: string): void {
137
+ assert.match(prompt, /"structuredSections"/);
138
+ assert.match(prompt, /"procedureSteps": \[\{"order": 1, "intent": "<step>"\}, \{"order": 2, "intent": "<step>"\}\]/);
139
+ assert.match(prompt, /"reasoningTrace":\s*\{\s*"steps": \[\{"order": 1, "description": "<step>"\}, \{"order": 2, "description": "<step>"\}\]/s);
140
+ assert.match(prompt, /"quote"/);
141
+ assert.match(prompt, /"scope"/);
142
+ assert.match(prompt, /"eventTime"/);
143
+ assert.match(prompt, /"identityReflection"/);
144
+ }
145
+
146
+ function assertSourceGroundedResult(result: ExtractionResult): void {
147
+ assert.equal(result.facts[0]?.content, SOURCE_TURN.content);
148
+ assert.equal(result.facts[0]?.category, "fact");
149
+ assert.deepEqual(result.questions, []);
150
+ }
151
+
152
+ test("local extraction prompt uses placeholders and accepts an empty question list", async () => {
153
+ const engine = new ExtractionEngine(parseConfig({
154
+ localLlmEnabled: true,
155
+ localLlmModel: "fixture-local",
156
+ localLlmFallback: false,
157
+ }));
158
+ let calls = 0;
159
+ let prompt = "";
160
+
161
+ const localLlm: LocalLlmFixture = {
162
+ async chatCompletion(messages: ChatMessage[]) {
163
+ calls += 1;
164
+ prompt = messages[1]?.content ?? "";
165
+ return { content: JSON.stringify(SOURCE_GROUNDED_EXTRACTION) };
166
+ },
167
+ };
168
+ const modelRegistry = {
169
+ calculateContextSizes: () => ({ maxInputChars: 8_000, maxOutputTokens: 1_000, description: "fixture" }),
170
+ };
171
+ assert.equal(Reflect.set(engine, "localLlm", localLlm), true);
172
+ assert.equal(Reflect.set(engine, "modelRegistry", modelRegistry), true);
173
+
174
+ const result = await engine.extract([SOURCE_TURN]);
175
+
176
+ assert.equal(calls, 1);
177
+ assertSourceGroundedResult(result);
178
+ assertSafeExtractionPrompt(prompt);
179
+ assertStructuralResponseShape(prompt);
180
+ assert.doesNotMatch(prompt, /^- rule:/m);
181
+ });
182
+
183
+ test("local extraction discards literal structural placeholders", async () => {
184
+ const engine = new ExtractionEngine(parseConfig({
185
+ localLlmEnabled: true,
186
+ localLlmModel: "fixture-local",
187
+ localLlmFallback: false,
188
+ }));
189
+ const localLlm: LocalLlmFixture = {
190
+ async chatCompletion() {
191
+ return { content: JSON.stringify(STRUCTURAL_PLACEHOLDER_EXTRACTION) };
192
+ },
193
+ };
194
+ const modelRegistry = {
195
+ calculateContextSizes: () => ({ maxInputChars: 8_000, maxOutputTokens: 1_000, description: "fixture" }),
196
+ };
197
+ assert.equal(Reflect.set(engine, "localLlm", localLlm), true);
198
+ assert.equal(Reflect.set(engine, "modelRegistry", modelRegistry), true);
199
+
200
+ const result = await engine.extract([SOURCE_TURN]);
201
+
202
+ assert.deepEqual(result.facts, []);
203
+ assert.deepEqual(result.profileUpdates, []);
204
+ assert.deepEqual(result.entities, []);
205
+ assert.deepEqual(result.questions, []);
206
+ assert.deepEqual(result.relationships, []);
207
+ assert.equal(result.identityReflection, undefined);
208
+ });
209
+
210
+ test("local extraction preserves source text that looks like a tag", async () => {
211
+ const engine = new ExtractionEngine(parseConfig({
212
+ localLlmEnabled: true,
213
+ localLlmModel: "fixture-local",
214
+ localLlmFallback: false,
215
+ }));
216
+ const localLlm: LocalLlmFixture = {
217
+ async chatCompletion() {
218
+ return {
219
+ content: JSON.stringify({
220
+ facts: [{ category: "fact", content: "<main>", confidence: 0.95, tags: [] }],
221
+ profileUpdates: [],
222
+ entities: [],
223
+ questions: [],
224
+ }),
225
+ };
226
+ },
227
+ };
228
+ const modelRegistry = {
229
+ calculateContextSizes: () => ({ maxInputChars: 8_000, maxOutputTokens: 1_000, description: "fixture" }),
230
+ };
231
+ assert.equal(Reflect.set(engine, "localLlm", localLlm), true);
232
+ assert.equal(Reflect.set(engine, "modelRegistry", modelRegistry), true);
233
+
234
+ const result = await engine.extract([TAG_SOURCE_TURN]);
235
+
236
+ assert.equal(result.facts[0]?.content, "<main>");
237
+ });
238
+
239
+ test("local truncated recovery discards literal structural placeholders", async () => {
240
+ const engine = new ExtractionEngine(parseConfig({
241
+ localLlmEnabled: true,
242
+ localLlmModel: "fixture-local",
243
+ localLlmFallback: false,
244
+ }));
245
+ const localLlm: LocalLlmFixture = {
246
+ async chatCompletion() {
247
+ return {
248
+ content: "{\"facts\":[{\"category\":\"<category>\",\"content\":\"<source-grounded statement>\",\"confidence\":0.7}",
249
+ };
250
+ },
251
+ };
252
+ const modelRegistry = {
253
+ calculateContextSizes: () => ({ maxInputChars: 8_000, maxOutputTokens: 1_000, description: "fixture" }),
254
+ };
255
+ assert.equal(Reflect.set(engine, "localLlm", localLlm), true);
256
+ assert.equal(Reflect.set(engine, "modelRegistry", modelRegistry), true);
257
+
258
+ const result = await engine.extract([SOURCE_TURN]);
259
+
260
+ assert.deepEqual(result.facts, []);
261
+ });
262
+
263
+ test("gateway extraction discards literal structural placeholders", async () => {
264
+ const engine = new ExtractionEngine(parseConfig({ modelSource: "gateway" }));
265
+ const fallbackLlm: GatewayFixture = {
266
+ async parseWithSchemaDetailed() {
267
+ return { modelUsed: "fixture-gateway", result: GATEWAY_PLACEHOLDER_EXTRACTION };
268
+ },
269
+ };
270
+ assert.equal(Reflect.set(engine, "fallbackLlm", fallbackLlm), true);
271
+
272
+ const result = await engine.extract([SOURCE_TURN]);
273
+
274
+ assert.deepEqual(result.facts, []);
275
+ assert.deepEqual(result.profileUpdates, []);
276
+ assert.deepEqual(result.entities, []);
277
+ assert.deepEqual(result.questions, []);
278
+ assert.deepEqual(result.relationships, []);
279
+ assert.equal(result.identityReflection, undefined);
280
+ });
281
+
282
+ test("gateway extraction prompt uses placeholders and accepts an empty question list", async () => {
283
+ const engine = new ExtractionEngine(parseConfig({ modelSource: "gateway" }));
284
+ let calls = 0;
285
+ let prompt = "";
286
+
287
+ const fallbackLlm: GatewayFixture = {
288
+ async parseWithSchemaDetailed(messages: ChatMessage[]) {
289
+ calls += 1;
290
+ prompt = messages[0]?.content ?? "";
291
+ return { modelUsed: "fixture-gateway", result: SOURCE_GROUNDED_EXTRACTION };
292
+ },
293
+ };
294
+ assert.equal(Reflect.set(engine, "fallbackLlm", fallbackLlm), true);
295
+
296
+ const result = await engine.extract([SOURCE_TURN]);
297
+
298
+ assert.equal(calls, 1);
299
+ assertSourceGroundedResult(result);
300
+ assertSafeExtractionPrompt(prompt);
301
+ });
302
+
303
+ test("direct extraction prompt uses placeholders and accepts an empty question list", async () => {
304
+ const engine = new ExtractionEngine(parseConfig({ openaiApiKey: "fixture-key" }));
305
+ let calls = 0;
306
+ let prompt = "";
307
+
308
+ const client: DirectClientFixture = {
309
+ chat: {
310
+ completions: {
311
+ async create(request: { messages: ChatMessage[] }) {
312
+ calls += 1;
313
+ prompt = request.messages[0]?.content ?? "";
314
+ return { choices: [{ message: { content: JSON.stringify(SOURCE_GROUNDED_EXTRACTION) } }] };
315
+ },
316
+ },
317
+ },
318
+ };
319
+ assert.equal(Reflect.set(engine, "client", client), true);
320
+
321
+ const result = await engine.extract([SOURCE_TURN]);
322
+
323
+ assert.equal(calls, 1);
324
+ assertSourceGroundedResult(result);
325
+ assertStructuralResponseShape(prompt);
326
+ assertSafeExtractionPrompt(prompt);
327
+ });
328
+
329
+ test("extraction schema permits empty optional question output", () => {
330
+ const parsed = ExtractionResultSchema.safeParse(SOURCE_GROUNDED_EXTRACTION);
331
+
332
+ assert.equal(parsed.success, true);
333
+ assert.deepEqual(parsed.data?.questions, []);
334
+ assert.match(ExtractionResultSchema.shape.questions.description ?? "", /zero to three/i);
335
+ });