@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.
- package/dist/access-cli.js +3 -3
- package/dist/{chunk-GBAJCDTW.js → chunk-BUK2FXOL.js} +207 -227
- package/dist/chunk-BUK2FXOL.js.map +1 -0
- package/dist/{chunk-LFHMUWA2.js → chunk-FDAPXLRC.js} +3 -3
- package/dist/{chunk-5GSUBCZM.js → chunk-TGTTESJS.js} +2 -2
- package/dist/chunk-TGTTESJS.js.map +1 -0
- package/dist/extraction.js +6 -2
- package/dist/index.js +3 -3
- package/dist/orchestrator.js +3 -3
- package/dist/schemas.js +1 -1
- package/package.json +2 -2
- package/src/extraction-prompt-safety.test.ts +335 -0
- package/src/extraction.ts +258 -326
- package/src/schemas.ts +1 -1
- package/dist/chunk-5GSUBCZM.js.map +0 -1
- package/dist/chunk-GBAJCDTW.js.map +0 -1
- /package/dist/{chunk-LFHMUWA2.js.map → chunk-FDAPXLRC.js.map} +0 -0
package/dist/orchestrator.js
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
throwIfRecallAborted,
|
|
42
42
|
tokenizeRecallQuery,
|
|
43
43
|
utcDateKeysForLocalDay
|
|
44
|
-
} from "./chunk-
|
|
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-
|
|
107
|
+
import "./chunk-BUK2FXOL.js";
|
|
108
108
|
import "./chunk-4RA3C3EV.js";
|
|
109
|
-
import "./chunk-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "9.25.
|
|
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.
|
|
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
|
+
});
|