@remnic/core 9.25.3 → 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 +2 -2
- package/dist/{chunk-PXJ4A6IK.js → chunk-BUK2FXOL.js} +133 -105
- package/dist/chunk-BUK2FXOL.js.map +1 -0
- package/dist/{chunk-NBT7O25Z.js → chunk-FDAPXLRC.js} +2 -2
- package/dist/extraction.js +5 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestrator.js +2 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +2 -2
- package/src/extraction-prompt-safety.test.ts +179 -0
- package/src/extraction.ts +186 -205
- package/dist/chunk-PXJ4A6IK.js.map +0 -1
- /package/dist/{chunk-NBT7O25Z.js.map → chunk-FDAPXLRC.js.map} +0 -0
package/src/extraction.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { normalizeProcedureSteps } from "./procedural/procedure-types.js";
|
|
|
40
40
|
import { normalizeReasoningTrace } from "./reasoning-trace-types.js";
|
|
41
41
|
import { looksLikeMechanicalTelemetryTranscript } from "./telemetry-transcript.js";
|
|
42
42
|
import { buildFactProvenance, type ProvenanceTurnInput } from "./provenance.js";
|
|
43
|
+
import { isMemoryCategory } from "./write-envelope.js";
|
|
43
44
|
import { classifyExtractionThrownError, classifyFallbackParseFailure } from "./extraction-error-classification.js";
|
|
44
45
|
export { classifyExtractionThrownError, classifyFallbackParseFailure } from "./extraction-error-classification.js";
|
|
45
46
|
import { resolvePipelineProcessingCapabilities } from "./capabilities.js";
|
|
@@ -83,6 +84,10 @@ const EXTRACTION_RESPONSE_SHAPE = `{
|
|
|
83
84
|
"identityReflection": "<conversation-grounded agent reflection>",
|
|
84
85
|
"relationships": [{"source": "<normalized-name>", "target": "<normalized-name>", "label": "<source-grounded relationship>"}]
|
|
85
86
|
}`;
|
|
87
|
+
const EXTRACTION_RESPONSE_PLACEHOLDERS: Record<string, true> = {};
|
|
88
|
+
for (const placeholder of EXTRACTION_RESPONSE_SHAPE.match(/<[^<>\r\n]+>/g) ?? []) {
|
|
89
|
+
EXTRACTION_RESPONSE_PLACEHOLDERS[placeholder] = true;
|
|
90
|
+
}
|
|
86
91
|
const CONSOLIDATION_RESPONSE_SCHEMA = `{
|
|
87
92
|
"items": [
|
|
88
93
|
{
|
|
@@ -101,6 +106,46 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
101
106
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
function containsExtractionPlaceholder(value: unknown): boolean {
|
|
110
|
+
if (typeof value === "string") return EXTRACTION_RESPONSE_PLACEHOLDERS[value.trim()] === true;
|
|
111
|
+
if (Array.isArray(value)) return value.some(containsExtractionPlaceholder);
|
|
112
|
+
return isPlainRecord(value) && Object.values(value).some(containsExtractionPlaceholder);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractionText(value: unknown): string | undefined {
|
|
116
|
+
if (typeof value !== "string") return undefined;
|
|
117
|
+
const text = value.trim();
|
|
118
|
+
return text.length > 0 && !containsExtractionPlaceholder(text) ? text : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractionAttributes(value: unknown): Record<string, string> | undefined {
|
|
122
|
+
if (!isPlainRecord(value)) return undefined;
|
|
123
|
+
const attributes: Record<string, string> = {};
|
|
124
|
+
for (const [key, candidate] of Object.entries(value)) {
|
|
125
|
+
const normalizedKey = extractionText(key);
|
|
126
|
+
const normalizedValue = extractionText(candidate);
|
|
127
|
+
if (normalizedKey !== undefined && normalizedValue !== undefined) {
|
|
128
|
+
attributes[normalizedKey] = normalizedValue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return Object.keys(attributes).length > 0 ? attributes : undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractionEntityType(value: unknown): ExtractedEntityResult["type"] | undefined {
|
|
135
|
+
const type = extractionText(value);
|
|
136
|
+
if (
|
|
137
|
+
type === "person" ||
|
|
138
|
+
type === "project" ||
|
|
139
|
+
type === "tool" ||
|
|
140
|
+
type === "company" ||
|
|
141
|
+
type === "place" ||
|
|
142
|
+
type === "other"
|
|
143
|
+
) {
|
|
144
|
+
return type;
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
104
149
|
function normalizeQuestion(question: ExtractionQuestion): ExtractionQuestion {
|
|
105
150
|
const priority = Number.isFinite(question.priority)
|
|
106
151
|
? Math.max(0, Math.min(1, question.priority))
|
|
@@ -276,182 +321,168 @@ export class ExtractionEngine {
|
|
|
276
321
|
}
|
|
277
322
|
|
|
278
323
|
private normalizeExtractionResultPayload(parsed: any): ExtractionResult {
|
|
279
|
-
const entities = Array.isArray(parsed?.entities)
|
|
324
|
+
const entities: ExtractedEntityResult[] = Array.isArray(parsed?.entities)
|
|
280
325
|
? parsed.entities
|
|
281
|
-
.map((
|
|
282
|
-
.filter((
|
|
326
|
+
.map((candidate: unknown): ExtractedEntityResult | undefined => this.normalizeEntityUpdate(candidate))
|
|
327
|
+
.filter((entity: ExtractedEntityResult | undefined): entity is ExtractedEntityResult => (
|
|
328
|
+
entity !== undefined && entity.name.length > 0
|
|
329
|
+
))
|
|
283
330
|
: [];
|
|
284
331
|
|
|
285
332
|
const facts = Array.isArray(parsed?.facts)
|
|
286
333
|
? parsed.facts
|
|
287
|
-
.map((
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
structuredAttributes:
|
|
298
|
-
f?.structuredAttributes && typeof f.structuredAttributes === "object" && !Array.isArray(f.structuredAttributes)
|
|
299
|
-
? Object.fromEntries(
|
|
300
|
-
Object.entries(f.structuredAttributes)
|
|
301
|
-
.filter(([k, v]) => typeof k === "string" && typeof v === "string")
|
|
302
|
-
) as Record<string, string>
|
|
303
|
-
: undefined,
|
|
304
|
-
procedureSteps: Array.isArray(f?.procedureSteps)
|
|
334
|
+
.map((candidate: unknown) => {
|
|
335
|
+
const f = isPlainRecord(candidate) ? candidate : {};
|
|
336
|
+
const category = typeof f.category === "string" ? f.category.trim() : "fact";
|
|
337
|
+
const reasoningTraceInput = isPlainRecord(f.reasoningTrace)
|
|
338
|
+
? f.reasoningTrace
|
|
339
|
+
: isPlainRecord(f?.reasoning_trace)
|
|
340
|
+
? f.reasoning_trace
|
|
341
|
+
: undefined;
|
|
342
|
+
if (!isMemoryCategory(category)) return undefined;
|
|
343
|
+
const procedureSteps = Array.isArray(f.procedureSteps)
|
|
305
344
|
? normalizeProcedureSteps(f.procedureSteps)
|
|
306
|
-
: undefined
|
|
307
|
-
reasoningTrace
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
: f?.reasoning_trace && typeof f.reasoning_trace === "object" && !Array.isArray(f.reasoning_trace)
|
|
317
|
-
? f.reasoning_trace
|
|
318
|
-
: null;
|
|
319
|
-
return candidate ? normalizeReasoningTrace(candidate) ?? undefined : undefined;
|
|
320
|
-
})(),
|
|
321
|
-
// Issue #1575 PR 2: the LLM provides a verbatim supporting quote
|
|
322
|
-
// per fact. Optional/nullable per repo gotcha 6 (OpenAI Responses
|
|
323
|
-
// API emits null for absent optional fields). The post-parse
|
|
324
|
-
// validator (buildFactProvenance) locates this quote in the
|
|
325
|
-
// buffered turns and builds verified ProvenanceSource[] entries.
|
|
326
|
-
quote:
|
|
327
|
-
typeof f?.quote === "string" && f.quote.trim().length > 0
|
|
328
|
-
? f.quote
|
|
329
|
-
: undefined,
|
|
330
|
-
eventTime:
|
|
331
|
-
typeof f?.eventTime === "string" && f.eventTime.trim().length > 0
|
|
332
|
-
? f.eventTime.trim()
|
|
333
|
-
: typeof f?.event_time === "string" && f.event_time.trim().length > 0
|
|
334
|
-
? f.event_time.trim()
|
|
335
|
-
: undefined,
|
|
336
|
-
}))
|
|
337
|
-
.filter((f: any) => f.content.length > 0)
|
|
338
|
-
: [];
|
|
339
|
-
|
|
340
|
-
const questions = Array.isArray(parsed?.questions)
|
|
341
|
-
? parsed.questions
|
|
342
|
-
.map((q: any) => {
|
|
343
|
-
if (typeof q === "string") return { question: q, context: "", priority: 0.5 };
|
|
345
|
+
: undefined;
|
|
346
|
+
const reasoningTrace = reasoningTraceInput
|
|
347
|
+
? normalizeReasoningTrace(reasoningTraceInput) ?? undefined
|
|
348
|
+
: undefined;
|
|
349
|
+
if (
|
|
350
|
+
containsExtractionPlaceholder(procedureSteps) ||
|
|
351
|
+
containsExtractionPlaceholder(reasoningTrace)
|
|
352
|
+
) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
344
355
|
return {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
356
|
+
category,
|
|
357
|
+
content: extractionText(f.content) ?? extractionText(f.text) ?? "",
|
|
358
|
+
confidence: typeof f.confidence === "number" ? f.confidence : 0.7,
|
|
359
|
+
tags: Array.isArray(f.tags)
|
|
360
|
+
? f.tags.flatMap((tag: unknown) => {
|
|
361
|
+
const text = extractionText(tag);
|
|
362
|
+
return text === undefined ? [] : [text];
|
|
363
|
+
})
|
|
364
|
+
: [],
|
|
365
|
+
entityRef: extractionText(f.entityRef),
|
|
366
|
+
promptedByQuestion: extractionText(f.promptedByQuestion),
|
|
367
|
+
scope:
|
|
368
|
+
f.scope === "global" || f.scope === "project" ? f.scope : undefined,
|
|
369
|
+
structuredAttributes: extractionAttributes(f.structuredAttributes),
|
|
370
|
+
procedureSteps,
|
|
371
|
+
reasoningTrace,
|
|
372
|
+
quote: extractionText(f.quote),
|
|
373
|
+
eventTime: extractionText(f.eventTime) ?? extractionText(f.event_time),
|
|
348
374
|
};
|
|
349
375
|
})
|
|
350
|
-
.filter((
|
|
376
|
+
.filter((fact: ExtractedFactResult | undefined): fact is ExtractedFactResult => (
|
|
377
|
+
fact !== undefined && fact.content.length > 0
|
|
378
|
+
))
|
|
351
379
|
: [];
|
|
352
380
|
|
|
381
|
+
const questions: ExtractionQuestion[] = Array.isArray(parsed?.questions)
|
|
382
|
+
? parsed.questions.flatMap((candidate: unknown) => {
|
|
383
|
+
const record = isPlainRecord(candidate) ? candidate : undefined;
|
|
384
|
+
const question =
|
|
385
|
+
extractionText(record?.question) ??
|
|
386
|
+
extractionText(record?.text) ??
|
|
387
|
+
extractionText(candidate);
|
|
388
|
+
if (question === undefined) return [];
|
|
389
|
+
return [{
|
|
390
|
+
question,
|
|
391
|
+
context: extractionText(record?.context) ?? "",
|
|
392
|
+
priority: typeof record?.priority === "number" ? record.priority : 0.5,
|
|
393
|
+
}];
|
|
394
|
+
})
|
|
395
|
+
: [];
|
|
396
|
+
|
|
397
|
+
const profileUpdates = Array.isArray(parsed?.profileUpdates)
|
|
398
|
+
? parsed.profileUpdates.flatMap((candidate: unknown) => {
|
|
399
|
+
const update = extractionText(candidate);
|
|
400
|
+
return update === undefined ? [] : [update];
|
|
401
|
+
})
|
|
402
|
+
: [];
|
|
403
|
+
|
|
404
|
+
const relationships: ExtractedRelationshipResult[] | undefined = Array.isArray(parsed?.relationships)
|
|
405
|
+
? parsed.relationships.flatMap((candidate: unknown) => {
|
|
406
|
+
const relationship = isPlainRecord(candidate) ? candidate : undefined;
|
|
407
|
+
const source = extractionText(relationship?.source);
|
|
408
|
+
const target = extractionText(relationship?.target);
|
|
409
|
+
const label = extractionText(relationship?.label);
|
|
410
|
+
if (source === undefined || target === undefined || label === undefined) return [];
|
|
411
|
+
return [{
|
|
412
|
+
source,
|
|
413
|
+
target,
|
|
414
|
+
label,
|
|
415
|
+
promptedByQuestion: extractionText(relationship?.promptedByQuestion),
|
|
416
|
+
}];
|
|
417
|
+
})
|
|
418
|
+
: undefined;
|
|
419
|
+
|
|
353
420
|
return {
|
|
354
421
|
facts,
|
|
355
422
|
entities,
|
|
356
|
-
profileUpdates
|
|
357
|
-
? parsed.profileUpdates.filter((u: any) => typeof u === "string" && u.trim().length > 0)
|
|
358
|
-
: [],
|
|
423
|
+
profileUpdates,
|
|
359
424
|
questions,
|
|
360
|
-
identityReflection: parsed?.identityReflection
|
|
361
|
-
relationships
|
|
362
|
-
? parsed.relationships.filter(
|
|
363
|
-
(r: any) =>
|
|
364
|
-
typeof r?.source === "string" &&
|
|
365
|
-
typeof r?.target === "string" &&
|
|
366
|
-
typeof r?.label === "string",
|
|
367
|
-
)
|
|
368
|
-
.map((r: any) => ({
|
|
369
|
-
source: r.source,
|
|
370
|
-
target: r.target,
|
|
371
|
-
label: r.label,
|
|
372
|
-
promptedByQuestion:
|
|
373
|
-
typeof r?.promptedByQuestion === "string" ? r.promptedByQuestion : undefined,
|
|
374
|
-
}))
|
|
375
|
-
: undefined,
|
|
425
|
+
identityReflection: extractionText(parsed?.identityReflection),
|
|
426
|
+
relationships,
|
|
376
427
|
};
|
|
377
428
|
}
|
|
378
429
|
|
|
379
|
-
private normalizeEntityUpdate(entity:
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
.filter((fact: string) => fact.length > 0)
|
|
392
|
-
: [];
|
|
430
|
+
private normalizeEntityUpdate(entity: unknown): ExtractedEntityResult | undefined {
|
|
431
|
+
const record = isPlainRecord(entity) ? entity : {};
|
|
432
|
+
const rawUpdates = isPlainRecord(record.updates) ? record.updates : undefined;
|
|
433
|
+
const normalizedTexts = (value: unknown): string[] =>
|
|
434
|
+
Array.isArray(value)
|
|
435
|
+
? value.flatMap((candidate: unknown) => {
|
|
436
|
+
const text = extractionText(candidate);
|
|
437
|
+
return text === undefined ? [] : [text];
|
|
438
|
+
})
|
|
439
|
+
: [];
|
|
440
|
+
const directFacts = normalizedTexts(record.facts);
|
|
441
|
+
const updateFacts = normalizedTexts(rawUpdates?.facts);
|
|
393
442
|
const scalarUpdateFacts = rawUpdates
|
|
394
|
-
? Object.
|
|
395
|
-
.sort((
|
|
396
|
-
.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
400
|
-
return [`${key}: ${value.trim()}`];
|
|
443
|
+
? Object.entries(rawUpdates)
|
|
444
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
445
|
+
.flatMap(([key, value]) => {
|
|
446
|
+
if (["facts", "name", "promptedByQuestion", "structuredSections", "type"].includes(key)) {
|
|
447
|
+
return [];
|
|
401
448
|
}
|
|
449
|
+
const normalizedKey = extractionText(key);
|
|
450
|
+
if (normalizedKey === undefined) return [];
|
|
451
|
+
const normalizedValue = extractionText(value);
|
|
452
|
+
if (normalizedValue !== undefined) return [`${normalizedKey}: ${normalizedValue}`];
|
|
402
453
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
403
|
-
return [`${
|
|
454
|
+
return [`${normalizedKey}: ${String(value)}`];
|
|
404
455
|
}
|
|
405
456
|
return [];
|
|
406
457
|
})
|
|
407
458
|
: [];
|
|
408
|
-
const structuredSectionsSource = Array.isArray(
|
|
409
|
-
?
|
|
459
|
+
const structuredSectionsSource = Array.isArray(record.structuredSections)
|
|
460
|
+
? record.structuredSections
|
|
410
461
|
: Array.isArray(rawUpdates?.structuredSections)
|
|
411
462
|
? rawUpdates.structuredSections
|
|
412
463
|
: [];
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const type =
|
|
422
|
-
typeof entity?.type === "string" && entity.type.trim().length > 0
|
|
423
|
-
? entity.type.trim()
|
|
424
|
-
: typeof rawUpdates?.type === "string" && rawUpdates.type.trim().length > 0
|
|
425
|
-
? rawUpdates.type.trim()
|
|
426
|
-
: "other";
|
|
464
|
+
const structuredSections = structuredSectionsSource.flatMap((candidate: unknown) => {
|
|
465
|
+
const section = isPlainRecord(candidate) ? candidate : {};
|
|
466
|
+
const key = extractionText(section.key);
|
|
467
|
+
const title = extractionText(section.title);
|
|
468
|
+
const facts = normalizedTexts(section.facts);
|
|
469
|
+
if (key === undefined || title === undefined || facts.length === 0) return [];
|
|
470
|
+
return [{ key, title, facts }];
|
|
471
|
+
});
|
|
427
472
|
|
|
473
|
+
const rawType = record.type ?? rawUpdates?.type;
|
|
474
|
+
const type = rawType === undefined ? "other" : extractionEntityType(rawType);
|
|
475
|
+
if (type === undefined) return undefined;
|
|
428
476
|
return {
|
|
429
|
-
name
|
|
477
|
+
name:
|
|
478
|
+
extractionText(record.name) ??
|
|
479
|
+
extractionText(record.entityId) ??
|
|
480
|
+
extractionText(rawUpdates?.name) ??
|
|
481
|
+
"",
|
|
430
482
|
type,
|
|
431
483
|
facts: [...directFacts, ...updateFacts, ...scalarUpdateFacts],
|
|
432
|
-
structuredSections:
|
|
433
|
-
|
|
434
|
-
.map((section: any) => ({
|
|
435
|
-
key: typeof section?.key === "string" ? section.key.trim() : "",
|
|
436
|
-
title: typeof section?.title === "string" ? section.title.trim() : "",
|
|
437
|
-
facts: Array.isArray(section?.facts)
|
|
438
|
-
? section.facts.filter((fact: any) => typeof fact === "string")
|
|
439
|
-
.map((fact: string) => fact.trim())
|
|
440
|
-
.filter((fact: string) => fact.length > 0)
|
|
441
|
-
: [],
|
|
442
|
-
}))
|
|
443
|
-
.filter((section: any) => (
|
|
444
|
-
section.key.length > 0 &&
|
|
445
|
-
section.title.length > 0 &&
|
|
446
|
-
section.facts.length > 0
|
|
447
|
-
))
|
|
448
|
-
: undefined,
|
|
449
|
-
promptedByQuestion:
|
|
450
|
-
typeof entity?.promptedByQuestion === "string"
|
|
451
|
-
? entity.promptedByQuestion
|
|
452
|
-
: typeof rawUpdates?.promptedByQuestion === "string"
|
|
453
|
-
? rawUpdates.promptedByQuestion
|
|
454
|
-
: undefined,
|
|
484
|
+
structuredSections: structuredSections.length > 0 ? structuredSections : undefined,
|
|
485
|
+
promptedByQuestion: extractionText(record.promptedByQuestion) ?? extractionText(rawUpdates?.promptedByQuestion),
|
|
455
486
|
};
|
|
456
487
|
}
|
|
457
488
|
|
|
@@ -615,8 +646,10 @@ export class ExtractionEngine {
|
|
|
615
646
|
)
|
|
616
647
|
.filter((update) => update.length > 0);
|
|
617
648
|
const entityUpdates = (Array.isArray(result.entityUpdates) ? result.entityUpdates : [])
|
|
618
|
-
.map((entity:
|
|
619
|
-
.filter((entity: ExtractedEntityResult)
|
|
649
|
+
.map((entity: unknown): ExtractedEntityResult | undefined => this.normalizeEntityUpdate(entity))
|
|
650
|
+
.filter((entity: ExtractedEntityResult | undefined): entity is ExtractedEntityResult => (
|
|
651
|
+
entity !== undefined && entity.name.length > 0
|
|
652
|
+
));
|
|
620
653
|
return { items, profileUpdates, entityUpdates };
|
|
621
654
|
}
|
|
622
655
|
|
|
@@ -1310,37 +1343,8 @@ export class ExtractionEngine {
|
|
|
1310
1343
|
log.debug(
|
|
1311
1344
|
`extracted ${result.facts.length} facts, ${result.entities.length} entities, ${(result.questions ?? []).length} questions via fallback (${detailed.modelUsed})`,
|
|
1312
1345
|
);
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
// ExtractedFact contract only exposes camelCase. Collapse each fact's
|
|
1316
|
-
// reasoningTrace through normalizeReasoningTrace before passing it on so
|
|
1317
|
-
// gateway output matches the shape local/direct-client paths produce.
|
|
1318
|
-
const normalizedFacts = result.facts.map((f: any) => {
|
|
1319
|
-
if (!f) return f;
|
|
1320
|
-
// Gateway tolerance: collapse snake_case event_time → camelCase
|
|
1321
|
-
// eventTime so the gateway path matches the local/direct/proactive
|
|
1322
|
-
// normalization (#1578 r3 — cursor bugbot).
|
|
1323
|
-
const eventTime =
|
|
1324
|
-
typeof f.eventTime === "string" && f.eventTime.trim().length > 0
|
|
1325
|
-
? f.eventTime.trim()
|
|
1326
|
-
: typeof f.event_time === "string" && f.event_time.trim().length > 0
|
|
1327
|
-
? f.event_time.trim()
|
|
1328
|
-
: undefined;
|
|
1329
|
-
if (!f.reasoningTrace && eventTime === undefined) return f;
|
|
1330
|
-
return {
|
|
1331
|
-
...f,
|
|
1332
|
-
...(f.reasoningTrace
|
|
1333
|
-
? { reasoningTrace: normalizeReasoningTrace(f.reasoningTrace) ?? undefined }
|
|
1334
|
-
: {}),
|
|
1335
|
-
...(eventTime !== undefined ? { eventTime } : {}),
|
|
1336
|
-
};
|
|
1337
|
-
});
|
|
1338
|
-
const sanitized = this.sanitizeExtractionResult({
|
|
1339
|
-
...result,
|
|
1340
|
-
facts: normalizedFacts,
|
|
1341
|
-
questions: result.questions ?? [],
|
|
1342
|
-
identityReflection: result.identityReflection ?? undefined,
|
|
1343
|
-
} as ExtractionResult, messageTimestamp);
|
|
1346
|
+
const normalized = this.normalizeExtractionResultPayload(result);
|
|
1347
|
+
const sanitized = this.sanitizeExtractionResult(normalized, messageTimestamp);
|
|
1344
1348
|
const finalResult = await this.applyProactiveQuestionPass(conversation, sanitized);
|
|
1345
1349
|
return this.attachProvenanceToResult(finalResult, boundedTurns);
|
|
1346
1350
|
}
|
|
@@ -1567,29 +1571,6 @@ ${truncatedConversation}`;
|
|
|
1567
1571
|
* Local LLMs sometimes hit token limits mid-JSON. This tries to salvage valid facts.
|
|
1568
1572
|
*/
|
|
1569
1573
|
private extractPartialFacts(jsonStr: string): ExtractionResult {
|
|
1570
|
-
const allowedCategories = new Set([
|
|
1571
|
-
"fact",
|
|
1572
|
-
"preference",
|
|
1573
|
-
"correction",
|
|
1574
|
-
"entity",
|
|
1575
|
-
"decision",
|
|
1576
|
-
"relationship",
|
|
1577
|
-
"principle",
|
|
1578
|
-
"commitment",
|
|
1579
|
-
"moment",
|
|
1580
|
-
"skill",
|
|
1581
|
-
"rule",
|
|
1582
|
-
"procedure",
|
|
1583
|
-
"reasoning_trace",
|
|
1584
|
-
]);
|
|
1585
|
-
const allowedEntityTypes = new Set([
|
|
1586
|
-
"person",
|
|
1587
|
-
"project",
|
|
1588
|
-
"tool",
|
|
1589
|
-
"company",
|
|
1590
|
-
"place",
|
|
1591
|
-
"other",
|
|
1592
|
-
]);
|
|
1593
1574
|
|
|
1594
1575
|
const facts: ExtractionResult["facts"] = [];
|
|
1595
1576
|
const entities: ExtractionResult["entities"] = [];
|
|
@@ -1599,8 +1580,8 @@ ${truncatedConversation}`;
|
|
|
1599
1580
|
const factRegex = /\{\s*"category"\s*:\s*"([^"]+)"\s*,\s*"content"\s*:\s*"([^"]+)"\s*,\s*"confidence"\s*:\s*([0-9.]+)/g;
|
|
1600
1581
|
let match;
|
|
1601
1582
|
while ((match = factRegex.exec(jsonStr)) !== null) {
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1583
|
+
const category = match[1]?.trim() ?? "";
|
|
1584
|
+
if (!isMemoryCategory(category)) continue;
|
|
1604
1585
|
facts.push({
|
|
1605
1586
|
category,
|
|
1606
1587
|
content: match[2].replace(/\\n/g, '\n').replace(/\\"/g, '"'),
|
|
@@ -1612,8 +1593,8 @@ ${truncatedConversation}`;
|
|
|
1612
1593
|
// Find all complete entity objects
|
|
1613
1594
|
const entityRegex = /\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"type"\s*:\s*"([^"]+)"/g;
|
|
1614
1595
|
while ((match = entityRegex.exec(jsonStr)) !== null) {
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1596
|
+
const type = extractionEntityType(match[2]);
|
|
1597
|
+
if (type === undefined) continue;
|
|
1617
1598
|
entities.push({
|
|
1618
1599
|
name: match[1],
|
|
1619
1600
|
type,
|
|
@@ -1624,7 +1605,7 @@ ${truncatedConversation}`;
|
|
|
1624
1605
|
// Ignore regex errors
|
|
1625
1606
|
}
|
|
1626
1607
|
|
|
1627
|
-
return { facts, entities, profileUpdates: [], questions: [] };
|
|
1608
|
+
return this.normalizeExtractionResultPayload({ facts, entities, profileUpdates: [], questions: [] });
|
|
1628
1609
|
}
|
|
1629
1610
|
|
|
1630
1611
|
/**
|