@remnic/core 9.6.30 → 9.6.31
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-admin-ops-surface.d.ts +1 -0
- package/dist/access-boundary.d.ts +1 -0
- package/dist/access-cli.js +9 -9
- package/dist/access-http.d.ts +1 -0
- package/dist/access-identity-continuity-surface.d.ts +1 -0
- package/dist/access-lcm-surface.d.ts +1 -0
- package/dist/access-mcp.d.ts +1 -0
- package/dist/access-observe-write-surface.d.ts +1 -0
- package/dist/access-operations.d.ts +1 -0
- package/dist/access-recall-surface.d.ts +1 -0
- package/dist/access-service.d.ts +1 -0
- package/dist/access-surface-catalog.d.ts +1 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/causal-behavior.d.ts +1 -0
- package/dist/{chunk-XGSOXFD4.js → chunk-6TCXIRP5.js} +2 -2
- package/dist/{chunk-7O2JL5RY.js → chunk-DEOYTFA6.js} +2 -2
- package/dist/{chunk-R26WO46Z.js → chunk-DGFU5MSX.js} +16 -1
- package/dist/chunk-DGFU5MSX.js.map +1 -0
- package/dist/{chunk-BFU7H32D.js → chunk-OH7QLI6W.js} +2 -2
- package/dist/{chunk-IRMLYBMU.js → chunk-T67LUYCR.js} +2 -2
- package/dist/{chunk-77UPM7P3.js → chunk-TVIVY7KU.js} +160 -61
- package/dist/chunk-TVIVY7KU.js.map +1 -0
- package/dist/{chunk-KCYE2MZM.js → chunk-UXPUUL3G.js} +29 -8
- package/dist/chunk-UXPUUL3G.js.map +1 -0
- package/dist/{chunk-3VSTTB74.js → chunk-WSOEYOHN.js} +7 -7
- package/dist/{chunk-5AJIDEFT.js → chunk-XFTPUXCE.js} +11 -7
- package/dist/chunk-XFTPUXCE.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/compounding/preference-consolidator.d.ts +1 -0
- package/dist/event-order-recall.d.ts +1 -0
- package/dist/event-order-recall.js +2 -2
- package/dist/evidence-pack.d.ts +16 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/explicit-capture.d.ts +1 -0
- package/dist/explicit-cue-recall.d.ts +16 -1
- package/dist/explicit-cue-recall.js +2 -2
- package/dist/focused-list-recall.d.ts +1 -0
- package/dist/focused-list-recall.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +9 -9
- package/dist/lcm/engine.d.ts +3 -0
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +5 -3
- package/dist/lcm/recall.d.ts +15 -1
- package/dist/lcm/recall.js +5 -3
- package/dist/lcm/tools.d.ts +1 -0
- package/dist/mcp-memory-inspector-app.d.ts +1 -0
- package/dist/orchestrator.d.ts +1 -0
- package/dist/orchestrator.js +9 -9
- package/dist/response-guidance-recall.d.ts +1 -0
- package/dist/response-guidance-recall.js +2 -2
- package/dist/targeted-fact-recall.d.ts +1 -0
- package/dist/targeted-fact-recall.js +2 -2
- package/package.json +2 -2
- package/src/evidence-pack.test.ts +66 -1
- package/src/evidence-pack.ts +39 -0
- package/src/explicit-cue-recall.ts +224 -67
- package/src/index.ts +3 -0
- package/src/lcm/engine.ts +17 -5
- package/src/lcm/index.ts +6 -1
- package/src/lcm/recall.ts +48 -5
- package/src/lcm-engine.test.ts +46 -0
- package/src/lcm-recall.test.ts +48 -1
- package/src/recall-trace-receipts.test.ts +380 -0
- package/dist/chunk-5AJIDEFT.js.map +0 -1
- package/dist/chunk-77UPM7P3.js.map +0 -1
- package/dist/chunk-KCYE2MZM.js.map +0 -1
- package/dist/chunk-R26WO46Z.js.map +0 -1
- /package/dist/{chunk-XGSOXFD4.js.map → chunk-6TCXIRP5.js.map} +0 -0
- /package/dist/{chunk-7O2JL5RY.js.map → chunk-DEOYTFA6.js.map} +0 -0
- /package/dist/{chunk-BFU7H32D.js.map → chunk-OH7QLI6W.js.map} +0 -0
- /package/dist/{chunk-IRMLYBMU.js.map → chunk-T67LUYCR.js.map} +0 -0
- /package/dist/{chunk-3VSTTB74.js.map → chunk-WSOEYOHN.js.map} +0 -0
package/src/lcm/engine.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { openLcmDatabase, ensureLcmStateDir } from "./schema.js";
|
|
|
3
3
|
import { LcmArchive, type LcmStructuredRecallMatch } from "./archive.js";
|
|
4
4
|
import { LcmDag } from "./dag.js";
|
|
5
5
|
import { LcmSummarizer, type SummarizeFn } from "./summarizer.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
assembleCompressedHistoryWithTrace,
|
|
8
|
+
type LcmRecallConfig,
|
|
9
|
+
type LcmRecallWithTraceResult,
|
|
10
|
+
} from "./recall.js";
|
|
7
11
|
import { LcmWorkQueue, type LcmObserveMessage } from "./queue.js";
|
|
8
12
|
import type { PluginConfig } from "../types.js";
|
|
9
13
|
import { log } from "../logger.js";
|
|
@@ -336,17 +340,25 @@ export class LcmEngine {
|
|
|
336
340
|
sessionId: string,
|
|
337
341
|
budgetChars: number,
|
|
338
342
|
): Promise<string> {
|
|
339
|
-
|
|
343
|
+
return (await this.assembleRecallWithTrace(sessionId, budgetChars)).text;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Build compressed recall plus content-free selected-summary receipts. */
|
|
347
|
+
async assembleRecallWithTrace(
|
|
348
|
+
sessionId: string,
|
|
349
|
+
budgetChars: number,
|
|
350
|
+
): Promise<LcmRecallWithTraceResult> {
|
|
351
|
+
if (!this.config.enabled) return { text: "", selectedSummaries: [] };
|
|
340
352
|
const normalizedSessionId = normalizeLcmSessionId(sessionId);
|
|
341
|
-
if (!normalizedSessionId) return "";
|
|
353
|
+
if (!normalizedSessionId) return { text: "", selectedSummaries: [] };
|
|
342
354
|
await this.ensureInitialized();
|
|
343
355
|
|
|
344
356
|
const effectiveBudget = Math.ceil(
|
|
345
357
|
budgetChars * this.config.recallBudgetShare,
|
|
346
358
|
);
|
|
347
|
-
if (effectiveBudget <= 0) return "";
|
|
359
|
+
if (effectiveBudget <= 0) return { text: "", selectedSummaries: [] };
|
|
348
360
|
|
|
349
|
-
return
|
|
361
|
+
return assembleCompressedHistoryWithTrace(this.dag!, this.archive!, normalizedSessionId, {
|
|
350
362
|
freshTailTurns: this.config.freshTailTurns,
|
|
351
363
|
budgetChars: effectiveBudget,
|
|
352
364
|
});
|
package/src/lcm/index.ts
CHANGED
|
@@ -14,6 +14,11 @@ export {
|
|
|
14
14
|
export { LcmArchive, estimateTokens } from "./archive.js";
|
|
15
15
|
export { LcmDag, type SummaryNode } from "./dag.js";
|
|
16
16
|
export { LcmSummarizer, type SummarizeFn } from "./summarizer.js";
|
|
17
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
assembleCompressedHistory,
|
|
19
|
+
assembleCompressedHistoryWithTrace,
|
|
20
|
+
type LcmRecallWithTraceResult,
|
|
21
|
+
type LcmSummarySelectionReceipt,
|
|
22
|
+
} from "./recall.js";
|
|
18
23
|
export { registerLcmTools } from "./tools.js";
|
|
19
24
|
export { openLcmDatabase, ensureLcmStateDir } from "./schema.js";
|
package/src/lcm/recall.ts
CHANGED
|
@@ -7,6 +7,21 @@ export interface LcmRecallConfig {
|
|
|
7
7
|
budgetChars: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface LcmSummarySelectionReceipt {
|
|
11
|
+
id: string;
|
|
12
|
+
depth: number;
|
|
13
|
+
msgStart: number;
|
|
14
|
+
msgEnd: number;
|
|
15
|
+
/** Half-open offsets within the returned compressed-history string. */
|
|
16
|
+
entryStart: number;
|
|
17
|
+
entryEnd: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LcmRecallWithTraceResult {
|
|
21
|
+
text: string;
|
|
22
|
+
selectedSummaries: LcmSummarySelectionReceipt[];
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
/**
|
|
11
26
|
* Assemble a compressed session history section from the summary DAG.
|
|
12
27
|
*
|
|
@@ -21,14 +36,24 @@ export function assembleCompressedHistory(
|
|
|
21
36
|
sessionId: string,
|
|
22
37
|
config: LcmRecallConfig,
|
|
23
38
|
): string {
|
|
39
|
+
return assembleCompressedHistoryWithTrace(dag, archive, sessionId, config).text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function assembleCompressedHistoryWithTrace(
|
|
43
|
+
dag: LcmDag,
|
|
44
|
+
archive: LcmArchive,
|
|
45
|
+
sessionId: string,
|
|
46
|
+
config: LcmRecallConfig,
|
|
47
|
+
): LcmRecallWithTraceResult {
|
|
24
48
|
const maxTurn = archive.getMaxTurnIndex(sessionId);
|
|
25
|
-
if (maxTurn < 0) return "";
|
|
49
|
+
if (maxTurn < 0) return { text: "", selectedSummaries: [] };
|
|
26
50
|
|
|
27
51
|
const allNodes = dag.getAllNodes(sessionId);
|
|
28
|
-
if (allNodes.length === 0) return "";
|
|
52
|
+
if (allNodes.length === 0) return { text: "", selectedSummaries: [] };
|
|
29
53
|
|
|
30
54
|
const freshTailStart = Math.max(0, maxTurn - config.freshTailTurns + 1);
|
|
31
55
|
const sections: string[] = [];
|
|
56
|
+
const selectedNodes: SummaryNode[] = [];
|
|
32
57
|
let usedChars = 0;
|
|
33
58
|
|
|
34
59
|
// Collect nodes covering the "old" portion (before fresh tail)
|
|
@@ -39,6 +64,7 @@ export function assembleCompressedHistory(
|
|
|
39
64
|
const entry = `**${label}** (depth ${node.depth}):\n${node.summary_text}`;
|
|
40
65
|
if (usedChars + entry.length > config.budgetChars) break;
|
|
41
66
|
sections.push(entry);
|
|
67
|
+
selectedNodes.push(node);
|
|
42
68
|
usedChars += entry.length;
|
|
43
69
|
}
|
|
44
70
|
}
|
|
@@ -58,12 +84,29 @@ export function assembleCompressedHistory(
|
|
|
58
84
|
const entry = `**${label}**:\n${node.summary_text}`;
|
|
59
85
|
if (usedChars + entry.length > config.budgetChars) break;
|
|
60
86
|
sections.push(entry);
|
|
87
|
+
selectedNodes.push(node);
|
|
61
88
|
usedChars += entry.length;
|
|
62
89
|
}
|
|
63
90
|
|
|
64
|
-
if (sections.length === 0) return "";
|
|
65
|
-
|
|
66
|
-
|
|
91
|
+
if (sections.length === 0) return { text: "", selectedSummaries: [] };
|
|
92
|
+
|
|
93
|
+
const prefix = "## Session History (Compressed)\n\n";
|
|
94
|
+
const text = `${prefix}${sections.join("\n\n")}`;
|
|
95
|
+
let entryStart = prefix.length;
|
|
96
|
+
const selectedSummaries = selectedNodes.map((node, index) => {
|
|
97
|
+
const entryEnd = entryStart + sections[index]!.length;
|
|
98
|
+
const receipt: LcmSummarySelectionReceipt = {
|
|
99
|
+
id: node.id,
|
|
100
|
+
depth: node.depth,
|
|
101
|
+
msgStart: node.msg_start,
|
|
102
|
+
msgEnd: node.msg_end,
|
|
103
|
+
entryStart,
|
|
104
|
+
entryEnd,
|
|
105
|
+
};
|
|
106
|
+
entryStart = entryEnd + 2;
|
|
107
|
+
return receipt;
|
|
108
|
+
});
|
|
109
|
+
return { text, selectedSummaries };
|
|
67
110
|
}
|
|
68
111
|
|
|
69
112
|
/**
|
package/src/lcm-engine.test.ts
CHANGED
|
@@ -676,6 +676,52 @@ test("expandContext preserves first, middle, and last row identity when truncate
|
|
|
676
676
|
}
|
|
677
677
|
});
|
|
678
678
|
|
|
679
|
+
test("assembleRecallWithTrace preserves text and reports selected summary metadata", async () => {
|
|
680
|
+
const memoryDir = await mkdtemp(
|
|
681
|
+
path.join(os.tmpdir(), "engram-lcm-summary-receipts-"),
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
const engine = new LcmEngine(
|
|
686
|
+
createPluginConfig(memoryDir),
|
|
687
|
+
async () => "private summary body",
|
|
688
|
+
);
|
|
689
|
+
await engine.observeMessages(
|
|
690
|
+
"private-session",
|
|
691
|
+
Array.from({ length: 8 }, (_, index) => ({
|
|
692
|
+
role: index % 2 === 0 ? "user" : "assistant",
|
|
693
|
+
content: `private message ${index}`,
|
|
694
|
+
})),
|
|
695
|
+
);
|
|
696
|
+
await engine.waitForSessionObserveIdle("private-session");
|
|
697
|
+
|
|
698
|
+
const baseline = await engine.assembleRecall("private-session", 10_000);
|
|
699
|
+
const traced = await engine.assembleRecallWithTrace(
|
|
700
|
+
"private-session",
|
|
701
|
+
10_000,
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
assert.equal(traced.text, baseline);
|
|
705
|
+
assert.ok(traced.selectedSummaries.length > 0);
|
|
706
|
+
assert.ok(
|
|
707
|
+
traced.selectedSummaries.every((receipt) =>
|
|
708
|
+
receipt.entryStart < receipt.entryEnd &&
|
|
709
|
+
receipt.msgStart <= receipt.msgEnd
|
|
710
|
+
),
|
|
711
|
+
);
|
|
712
|
+
assert.equal(
|
|
713
|
+
JSON.stringify(traced.selectedSummaries).includes("private summary body"),
|
|
714
|
+
false,
|
|
715
|
+
);
|
|
716
|
+
assert.equal(
|
|
717
|
+
JSON.stringify(traced.selectedSummaries).includes("private-session"),
|
|
718
|
+
false,
|
|
719
|
+
);
|
|
720
|
+
} finally {
|
|
721
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
679
725
|
test("close prevents deferred observe work from reinitializing the engine", async () => {
|
|
680
726
|
const memoryDir = await mkdtemp(
|
|
681
727
|
path.join(os.tmpdir(), "engram-lcm-engine-close-"),
|
package/src/lcm-recall.test.ts
CHANGED
|
@@ -6,7 +6,10 @@ import test from "node:test";
|
|
|
6
6
|
|
|
7
7
|
import { LcmArchive } from "./lcm/archive.js";
|
|
8
8
|
import { LcmDag } from "./lcm/dag.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
assembleCompressedHistory,
|
|
11
|
+
assembleCompressedHistoryWithTrace,
|
|
12
|
+
} from "./lcm/recall.js";
|
|
10
13
|
import { ensureLcmStateDir, openLcmDatabase } from "./lcm/schema.js";
|
|
11
14
|
|
|
12
15
|
test("assembleCompressedHistory excludes old summaries that straddle the fresh tail", async () => {
|
|
@@ -58,12 +61,56 @@ test("assembleCompressedHistory excludes old summaries that straddle the fresh t
|
|
|
58
61
|
freshTailTurns: 16,
|
|
59
62
|
budgetChars: 10_000,
|
|
60
63
|
});
|
|
64
|
+
const traced = assembleCompressedHistoryWithTrace(
|
|
65
|
+
dag,
|
|
66
|
+
archive,
|
|
67
|
+
sessionId,
|
|
68
|
+
{ freshTailTurns: 16, budgetChars: 10_000 },
|
|
69
|
+
);
|
|
61
70
|
|
|
71
|
+
assert.equal(traced.text, section);
|
|
62
72
|
assert.doesNotMatch(section, /parent summary 0-31/);
|
|
63
73
|
assert.match(section, /leaf summary 0-7/);
|
|
64
74
|
assert.match(section, /leaf summary 8-15/);
|
|
65
75
|
assert.match(section, /leaf summary 16-23/);
|
|
66
76
|
assert.match(section, /leaf summary 24-31/);
|
|
77
|
+
assert.deepEqual(
|
|
78
|
+
traced.selectedSummaries.map(({ id, depth, msgStart, msgEnd }) => ({
|
|
79
|
+
id,
|
|
80
|
+
depth,
|
|
81
|
+
msgStart,
|
|
82
|
+
msgEnd,
|
|
83
|
+
})),
|
|
84
|
+
[
|
|
85
|
+
{ id: "leaf-0-7", depth: 0, msgStart: 0, msgEnd: 7 },
|
|
86
|
+
{ id: "leaf-8-15", depth: 0, msgStart: 8, msgEnd: 15 },
|
|
87
|
+
{ id: "leaf-16-23", depth: 0, msgStart: 16, msgEnd: 23 },
|
|
88
|
+
{ id: "leaf-24-31", depth: 0, msgStart: 24, msgEnd: 31 },
|
|
89
|
+
],
|
|
90
|
+
);
|
|
91
|
+
for (const receipt of traced.selectedSummaries) {
|
|
92
|
+
assert.ok(receipt.entryStart < receipt.entryEnd);
|
|
93
|
+
const expectedSummary = receipt.id.replace("leaf-", "leaf summary ");
|
|
94
|
+
assert.equal(
|
|
95
|
+
traced.text.slice(receipt.entryStart, receipt.entryEnd).includes(expectedSummary),
|
|
96
|
+
true,
|
|
97
|
+
);
|
|
98
|
+
assert.equal("summary" in receipt, false);
|
|
99
|
+
assert.equal("sessionId" in receipt, false);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const first = traced.selectedSummaries[0]!;
|
|
103
|
+
const firstEntryBudget = first.entryEnd - first.entryStart;
|
|
104
|
+
const limited = assembleCompressedHistoryWithTrace(
|
|
105
|
+
dag,
|
|
106
|
+
archive,
|
|
107
|
+
sessionId,
|
|
108
|
+
{ freshTailTurns: 16, budgetChars: firstEntryBudget },
|
|
109
|
+
);
|
|
110
|
+
assert.deepEqual(
|
|
111
|
+
limited.selectedSummaries.map((receipt) => receipt.id),
|
|
112
|
+
["leaf-0-7"],
|
|
113
|
+
);
|
|
67
114
|
} finally {
|
|
68
115
|
db.close();
|
|
69
116
|
await rm(memoryDir, { recursive: true, force: true });
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildExplicitCueRecallSection,
|
|
6
|
+
buildTrajectoryAnalysisRecallSection,
|
|
7
|
+
type ExplicitCueRecallEngine,
|
|
8
|
+
type TrajectoryAnalysisLineReceipt,
|
|
9
|
+
} from "./explicit-cue-recall.js";
|
|
10
|
+
import type { EvidencePackSelectionReceipt } from "./evidence-pack.js";
|
|
11
|
+
|
|
12
|
+
interface ReceiptRow {
|
|
13
|
+
id?: number;
|
|
14
|
+
session_id: string;
|
|
15
|
+
turn_index: number;
|
|
16
|
+
role: string;
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class ReceiptEngine implements ExplicitCueRecallEngine {
|
|
21
|
+
constructor(private readonly rows: ReceiptRow[]) {}
|
|
22
|
+
|
|
23
|
+
async expandContext(
|
|
24
|
+
sessionId: string,
|
|
25
|
+
fromTurn: number,
|
|
26
|
+
toTurn: number,
|
|
27
|
+
): Promise<ReceiptRow[]> {
|
|
28
|
+
return this.rows.filter((row) =>
|
|
29
|
+
row.session_id === sessionId &&
|
|
30
|
+
row.turn_index >= fromTurn &&
|
|
31
|
+
row.turn_index <= toTurn
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async searchContextFull(): Promise<Array<ReceiptRow & { score: number }>> {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getStats(): Promise<{ totalMessages: number; maxTurnIndex?: number }> {
|
|
40
|
+
return {
|
|
41
|
+
totalMessages: this.rows.length,
|
|
42
|
+
maxTurnIndex: this.rows.length > 0
|
|
43
|
+
? Math.max(...this.rows.map((row) => row.turn_index))
|
|
44
|
+
: undefined,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("explicit-cue receipts identify only blocks that survive selection", async () => {
|
|
50
|
+
const engine = new ReceiptEngine([
|
|
51
|
+
{
|
|
52
|
+
id: 101,
|
|
53
|
+
session_id: "s",
|
|
54
|
+
turn_index: 7,
|
|
55
|
+
role: "user",
|
|
56
|
+
content: "first selected row",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 102,
|
|
60
|
+
session_id: "s",
|
|
61
|
+
turn_index: 7,
|
|
62
|
+
role: "assistant",
|
|
63
|
+
content: "second row that the deliberately narrow section budget rejects",
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
const receipts: EvidencePackSelectionReceipt[] = [];
|
|
67
|
+
const options = {
|
|
68
|
+
engine,
|
|
69
|
+
sessionId: "s",
|
|
70
|
+
query: "Review turn 7",
|
|
71
|
+
maxChars:
|
|
72
|
+
"## Explicit Cue Evidence".length +
|
|
73
|
+
2 +
|
|
74
|
+
"[s, turn 7, user]: first selected row".length,
|
|
75
|
+
maxItemChars: 200,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const baseline = await buildExplicitCueRecallSection(options);
|
|
79
|
+
const traced = await buildExplicitCueRecallSection({
|
|
80
|
+
...options,
|
|
81
|
+
onEvidenceSelected: (receipt) => receipts.push(receipt),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.equal(traced, baseline);
|
|
85
|
+
assert.deepEqual(receipts.map((receipt) => receipt.item.archiveRowId), [101]);
|
|
86
|
+
assert.equal(
|
|
87
|
+
traced.slice(receipts[0]!.blockStart, receipts[0]!.blockEnd).includes(
|
|
88
|
+
"first selected row",
|
|
89
|
+
),
|
|
90
|
+
true,
|
|
91
|
+
);
|
|
92
|
+
assert.equal(JSON.stringify(receipts).includes("first selected row"), false);
|
|
93
|
+
assert.equal(JSON.stringify(receipts).includes("second row"), false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("trajectory receipts retain row lineage only for emitted clipped lines", async () => {
|
|
97
|
+
const engine = new ReceiptEngine([
|
|
98
|
+
{
|
|
99
|
+
id: 201,
|
|
100
|
+
session_id: "ama",
|
|
101
|
+
turn_index: 1,
|
|
102
|
+
role: "user",
|
|
103
|
+
content: "[Action 1]: move right",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 202,
|
|
107
|
+
session_id: "ama",
|
|
108
|
+
turn_index: 1,
|
|
109
|
+
role: "assistant",
|
|
110
|
+
content: "[Observation 1]: reached the wall",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 203,
|
|
114
|
+
session_id: "ama",
|
|
115
|
+
turn_index: 2,
|
|
116
|
+
role: "user",
|
|
117
|
+
content: "[Action 2]: move left",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 204,
|
|
121
|
+
session_id: "ama",
|
|
122
|
+
turn_index: 2,
|
|
123
|
+
role: "assistant",
|
|
124
|
+
content: "[Observation 2]: reached the door",
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
const receipts: TrajectoryAnalysisLineReceipt[] = [];
|
|
128
|
+
const options = {
|
|
129
|
+
engine,
|
|
130
|
+
sessionId: "ama",
|
|
131
|
+
query:
|
|
132
|
+
"What will be the resulting state at action 1? Provide the full observation.",
|
|
133
|
+
maxChars: 4_000,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const baseline = await buildTrajectoryAnalysisRecallSection(options);
|
|
137
|
+
const traced = await buildTrajectoryAnalysisRecallSection({
|
|
138
|
+
...options,
|
|
139
|
+
onLineSelected: (receipt) => receipts.push(receipt),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
assert.equal(traced, baseline);
|
|
143
|
+
const actionOne = receipts.find((receipt) =>
|
|
144
|
+
traced.slice(receipt.lineStart, receipt.lineEnd).startsWith("[Action 1]")
|
|
145
|
+
);
|
|
146
|
+
const observationOne = receipts.find((receipt) =>
|
|
147
|
+
traced.slice(receipt.lineStart, receipt.lineEnd).startsWith("[Observation 1]")
|
|
148
|
+
);
|
|
149
|
+
assert.ok(actionOne);
|
|
150
|
+
assert.ok(observationOne);
|
|
151
|
+
assert.equal(actionOne.lineageStatus, "exact");
|
|
152
|
+
assert.equal(observationOne.lineageStatus, "exact");
|
|
153
|
+
assert.deepEqual(actionOne.actionArchiveRowIds, [201]);
|
|
154
|
+
assert.deepEqual(actionOne.observationArchiveRowIds, []);
|
|
155
|
+
assert.deepEqual(observationOne.actionArchiveRowIds, []);
|
|
156
|
+
assert.deepEqual(observationOne.observationArchiveRowIds, [202]);
|
|
157
|
+
assert.equal(actionOne.actionArchiveRowIds.includes(203), false);
|
|
158
|
+
assert.equal(observationOne.observationArchiveRowIds.includes(204), false);
|
|
159
|
+
|
|
160
|
+
const clippedReceipts: TrajectoryAnalysisLineReceipt[] = [];
|
|
161
|
+
const clipped = await buildTrajectoryAnalysisRecallSection({
|
|
162
|
+
...options,
|
|
163
|
+
maxChars: actionOne.lineStart + 8,
|
|
164
|
+
onLineSelected: (receipt) => clippedReceipts.push(receipt),
|
|
165
|
+
});
|
|
166
|
+
const partialAction = clippedReceipts.at(-1)!;
|
|
167
|
+
assert.equal(partialAction.lineStart, actionOne.lineStart);
|
|
168
|
+
assert.equal(partialAction.lineEnd, clipped.length);
|
|
169
|
+
assert.equal(partialAction.lineageStatus, "exact");
|
|
170
|
+
assert.deepEqual(partialAction.actionArchiveRowIds, [201]);
|
|
171
|
+
assert.equal(partialAction.actionArchiveRowIds.includes(203), false);
|
|
172
|
+
assert.equal(JSON.stringify(receipts).includes("move right"), false);
|
|
173
|
+
assert.equal(JSON.stringify(receipts).includes("reached the wall"), false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("spatial derived-line receipts mark lineage unavailable without guessing siblings", async () => {
|
|
177
|
+
const engine = new ReceiptEngine([
|
|
178
|
+
{
|
|
179
|
+
id: 301,
|
|
180
|
+
session_id: "ama",
|
|
181
|
+
turn_index: 20,
|
|
182
|
+
role: "user",
|
|
183
|
+
content: "[Action 20]: right",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 302,
|
|
187
|
+
session_id: "ama",
|
|
188
|
+
turn_index: 20,
|
|
189
|
+
role: "assistant",
|
|
190
|
+
content: [
|
|
191
|
+
"[Observation 20]: Active rules:",
|
|
192
|
+
"baba is you",
|
|
193
|
+
"",
|
|
194
|
+
"Objects on the map:",
|
|
195
|
+
"rule `win` 3 step to the left",
|
|
196
|
+
].join("\n"),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 303,
|
|
200
|
+
session_id: "ama",
|
|
201
|
+
turn_index: 21,
|
|
202
|
+
role: "user",
|
|
203
|
+
content: "[Action 21]: left",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 304,
|
|
207
|
+
session_id: "ama",
|
|
208
|
+
turn_index: 21,
|
|
209
|
+
role: "assistant",
|
|
210
|
+
content: [
|
|
211
|
+
"[Observation 21]: Active rules:",
|
|
212
|
+
"baba is you",
|
|
213
|
+
"",
|
|
214
|
+
"Objects on the map:",
|
|
215
|
+
"rule `win` 2 step to the left",
|
|
216
|
+
].join("\n"),
|
|
217
|
+
},
|
|
218
|
+
]);
|
|
219
|
+
const receipts: TrajectoryAnalysisLineReceipt[] = [];
|
|
220
|
+
const options = {
|
|
221
|
+
engine,
|
|
222
|
+
sessionId: "ama",
|
|
223
|
+
query:
|
|
224
|
+
"Between steps 20 and 21, the rule 'win' block's relative position changed from 3 step to the left to 2 step to the left. What was the actual movement?",
|
|
225
|
+
maxChars: 4_000,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const baseline = await buildTrajectoryAnalysisRecallSection(options);
|
|
229
|
+
const traced = await buildTrajectoryAnalysisRecallSection({
|
|
230
|
+
...options,
|
|
231
|
+
onLineSelected: (receipt) => receipts.push(receipt),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
assert.equal(traced, baseline);
|
|
235
|
+
const heading = receipts.find((receipt) =>
|
|
236
|
+
traced.slice(receipt.lineStart, receipt.lineEnd) === "Relative-position movement cues:"
|
|
237
|
+
);
|
|
238
|
+
const movement = receipts.find((receipt) =>
|
|
239
|
+
traced.slice(receipt.lineStart, receipt.lineEnd).startsWith("Observation 20->21:")
|
|
240
|
+
);
|
|
241
|
+
assert.ok(heading);
|
|
242
|
+
assert.ok(movement);
|
|
243
|
+
assert.equal(heading.lineageStatus, "exact");
|
|
244
|
+
assert.deepEqual(heading.actionArchiveRowIds, []);
|
|
245
|
+
assert.deepEqual(heading.observationArchiveRowIds, []);
|
|
246
|
+
assert.equal(movement.lineageStatus, "unavailable");
|
|
247
|
+
assert.deepEqual(movement.actionArchiveRowIds, []);
|
|
248
|
+
assert.deepEqual(movement.observationArchiveRowIds, []);
|
|
249
|
+
assert.equal(JSON.stringify(movement).includes("rule win"), false);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("trajectory receipts mark id-less and partially id-less direct sources unavailable", async () => {
|
|
253
|
+
const engine = new ReceiptEngine([
|
|
254
|
+
{
|
|
255
|
+
session_id: "ama",
|
|
256
|
+
turn_index: 1,
|
|
257
|
+
role: "user",
|
|
258
|
+
content: "[Action 1]: move right",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 402,
|
|
262
|
+
session_id: "ama",
|
|
263
|
+
turn_index: 1,
|
|
264
|
+
role: "assistant",
|
|
265
|
+
content: "[Observation 1]: reached the wall",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
session_id: "ama",
|
|
269
|
+
turn_index: 2,
|
|
270
|
+
role: "user",
|
|
271
|
+
content: "[Action 2]: move left",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
session_id: "ama",
|
|
275
|
+
turn_index: 2,
|
|
276
|
+
role: "assistant",
|
|
277
|
+
content: "[Observation 2]: reached the door",
|
|
278
|
+
},
|
|
279
|
+
]);
|
|
280
|
+
const receipts: TrajectoryAnalysisLineReceipt[] = [];
|
|
281
|
+
const text = await buildTrajectoryAnalysisRecallSection({
|
|
282
|
+
engine,
|
|
283
|
+
sessionId: "ama",
|
|
284
|
+
query: "Compare steps 1 and 2 and provide the full observations.",
|
|
285
|
+
maxChars: 4_000,
|
|
286
|
+
onLineSelected: (receipt) => receipts.push(receipt),
|
|
287
|
+
});
|
|
288
|
+
const findLine = (prefix: string) => receipts.find((receipt) =>
|
|
289
|
+
text.slice(receipt.lineStart, receipt.lineEnd).startsWith(prefix)
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const idLessAction = findLine("[Action 1]");
|
|
293
|
+
const identifiedObservation = findLine("[Observation 1]");
|
|
294
|
+
const idLessObservation = findLine("[Observation 2]");
|
|
295
|
+
assert.ok(idLessAction);
|
|
296
|
+
assert.ok(identifiedObservation);
|
|
297
|
+
assert.ok(idLessObservation);
|
|
298
|
+
assert.equal(idLessAction.lineageStatus, "unavailable");
|
|
299
|
+
assert.deepEqual(idLessAction.actionArchiveRowIds, []);
|
|
300
|
+
assert.equal(identifiedObservation.lineageStatus, "exact");
|
|
301
|
+
assert.deepEqual(identifiedObservation.observationArchiveRowIds, [402]);
|
|
302
|
+
assert.equal(idLessObservation.lineageStatus, "unavailable");
|
|
303
|
+
assert.deepEqual(idLessObservation.observationArchiveRowIds, []);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("inferred-location receipts identify only the action that establishes the final location", async () => {
|
|
307
|
+
const engine = new ReceiptEngine([
|
|
308
|
+
{
|
|
309
|
+
id: 501,
|
|
310
|
+
session_id: "ama",
|
|
311
|
+
turn_index: 1,
|
|
312
|
+
role: "user",
|
|
313
|
+
content: "[Action 1]: open cabinet 1",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 502,
|
|
317
|
+
session_id: "ama",
|
|
318
|
+
turn_index: 2,
|
|
319
|
+
role: "user",
|
|
320
|
+
content: "[Action 2]: move cabinet 1 to kitchen",
|
|
321
|
+
},
|
|
322
|
+
]);
|
|
323
|
+
const receipts: TrajectoryAnalysisLineReceipt[] = [];
|
|
324
|
+
const text = await buildTrajectoryAnalysisRecallSection({
|
|
325
|
+
engine,
|
|
326
|
+
sessionId: "ama",
|
|
327
|
+
query:
|
|
328
|
+
"What is the state of cabinet 1 at step 2 and what was its prior whole changes history?",
|
|
329
|
+
maxChars: 4_000,
|
|
330
|
+
onLineSelected: (receipt) => receipts.push(receipt),
|
|
331
|
+
});
|
|
332
|
+
const location = receipts.find((receipt) =>
|
|
333
|
+
text.slice(receipt.lineStart, receipt.lineEnd).startsWith(
|
|
334
|
+
"Inferred cabinet 1 location at step 2: kitchen",
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
assert.ok(location);
|
|
339
|
+
assert.equal(location.lineageStatus, "exact");
|
|
340
|
+
assert.deepEqual(location.actionArchiveRowIds, [502]);
|
|
341
|
+
assert.equal(location.actionArchiveRowIds.includes(501), false);
|
|
342
|
+
assert.deepEqual(location.observationArchiveRowIds, []);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("stateful inventory-change lines do not publish partial current-row lineage", async () => {
|
|
346
|
+
const engine = new ReceiptEngine([
|
|
347
|
+
{
|
|
348
|
+
id: 601,
|
|
349
|
+
session_id: "ama",
|
|
350
|
+
turn_index: 1,
|
|
351
|
+
role: "user",
|
|
352
|
+
content: "[Action 1]: take apple 1 from shelf 1",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: 602,
|
|
356
|
+
session_id: "ama",
|
|
357
|
+
turn_index: 2,
|
|
358
|
+
role: "user",
|
|
359
|
+
content: "[Action 2]: place apple 1 into bowl 1",
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
const receipts: TrajectoryAnalysisLineReceipt[] = [];
|
|
363
|
+
const text = await buildTrajectoryAnalysisRecallSection({
|
|
364
|
+
engine,
|
|
365
|
+
sessionId: "ama",
|
|
366
|
+
query: "What changes occurred to the inventory throughout the trajectory?",
|
|
367
|
+
maxChars: 4_000,
|
|
368
|
+
onLineSelected: (receipt) => receipts.push(receipt),
|
|
369
|
+
});
|
|
370
|
+
const placed = receipts.find((receipt) =>
|
|
371
|
+
text.slice(receipt.lineStart, receipt.lineEnd).startsWith(
|
|
372
|
+
"[Action 2]: place apple 1 into bowl 1 => inventory removed apple 1",
|
|
373
|
+
)
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
assert.ok(placed);
|
|
377
|
+
assert.equal(placed.lineageStatus, "unavailable");
|
|
378
|
+
assert.deepEqual(placed.actionArchiveRowIds, []);
|
|
379
|
+
assert.deepEqual(placed.observationArchiveRowIds, []);
|
|
380
|
+
});
|