@chendpoc/pi-memory 0.1.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.
Files changed (177) hide show
  1. package/README.md +180 -0
  2. package/dist/adapters/piComplete.d.ts +17 -0
  3. package/dist/adapters/piComplete.d.ts.map +1 -0
  4. package/dist/adapters/piComplete.js +169 -0
  5. package/dist/adapters/piComplete.js.map +1 -0
  6. package/dist/bundle/install.d.ts +34 -0
  7. package/dist/bundle/install.d.ts.map +1 -0
  8. package/dist/bundle/install.js +183 -0
  9. package/dist/bundle/install.js.map +1 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +245 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/config.d.ts +27 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +49 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/errclass.d.ts +7 -0
  19. package/dist/errclass.d.ts.map +1 -0
  20. package/dist/errclass.js +32 -0
  21. package/dist/errclass.js.map +1 -0
  22. package/dist/extension.d.ts +24 -0
  23. package/dist/extension.d.ts.map +1 -0
  24. package/dist/extension.js +7 -0
  25. package/dist/extension.js.map +1 -0
  26. package/dist/fallback/index.d.ts +11 -0
  27. package/dist/fallback/index.d.ts.map +1 -0
  28. package/dist/fallback/index.js +16 -0
  29. package/dist/fallback/index.js.map +1 -0
  30. package/dist/fallback/llmRerank.d.ts +19 -0
  31. package/dist/fallback/llmRerank.d.ts.map +1 -0
  32. package/dist/fallback/llmRerank.js +60 -0
  33. package/dist/fallback/llmRerank.js.map +1 -0
  34. package/dist/fallback/memoryMd.d.ts +6 -0
  35. package/dist/fallback/memoryMd.d.ts.map +1 -0
  36. package/dist/fallback/memoryMd.js +35 -0
  37. package/dist/fallback/memoryMd.js.map +1 -0
  38. package/dist/fallback/sessionIndex.d.ts +35 -0
  39. package/dist/fallback/sessionIndex.d.ts.map +1 -0
  40. package/dist/fallback/sessionIndex.js +222 -0
  41. package/dist/fallback/sessionIndex.js.map +1 -0
  42. package/dist/fallback/sessionSearch.d.ts +18 -0
  43. package/dist/fallback/sessionSearch.d.ts.map +1 -0
  44. package/dist/fallback/sessionSearch.js +161 -0
  45. package/dist/fallback/sessionSearch.js.map +1 -0
  46. package/dist/index.d.ts +25 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +24 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/paths.d.ts +7 -0
  51. package/dist/paths.d.ts.map +1 -0
  52. package/dist/paths.js +26 -0
  53. package/dist/paths.js.map +1 -0
  54. package/dist/pi-extension.d.ts +6 -0
  55. package/dist/pi-extension.d.ts.map +1 -0
  56. package/dist/pi-extension.js +224 -0
  57. package/dist/pi-extension.js.map +1 -0
  58. package/dist/preflight/detectIntents.d.ts +102 -0
  59. package/dist/preflight/detectIntents.d.ts.map +1 -0
  60. package/dist/preflight/detectIntents.js +624 -0
  61. package/dist/preflight/detectIntents.js.map +1 -0
  62. package/dist/preflight/hook.d.ts +58 -0
  63. package/dist/preflight/hook.d.ts.map +1 -0
  64. package/dist/preflight/hook.js +77 -0
  65. package/dist/preflight/hook.js.map +1 -0
  66. package/dist/preflight/render.d.ts +21 -0
  67. package/dist/preflight/render.d.ts.map +1 -0
  68. package/dist/preflight/render.js +132 -0
  69. package/dist/preflight/render.js.map +1 -0
  70. package/dist/preflight/strip.d.ts +11 -0
  71. package/dist/preflight/strip.d.ts.map +1 -0
  72. package/dist/preflight/strip.js +46 -0
  73. package/dist/preflight/strip.js.map +1 -0
  74. package/dist/service.d.ts +56 -0
  75. package/dist/service.d.ts.map +1 -0
  76. package/dist/service.js +158 -0
  77. package/dist/service.js.map +1 -0
  78. package/dist/sidecar/bundle.d.ts +19 -0
  79. package/dist/sidecar/bundle.d.ts.map +1 -0
  80. package/dist/sidecar/bundle.js +39 -0
  81. package/dist/sidecar/bundle.js.map +1 -0
  82. package/dist/sidecar/client.d.ts +17 -0
  83. package/dist/sidecar/client.d.ts.map +1 -0
  84. package/dist/sidecar/client.js +107 -0
  85. package/dist/sidecar/client.js.map +1 -0
  86. package/dist/sidecar/process.d.ts +14 -0
  87. package/dist/sidecar/process.d.ts.map +1 -0
  88. package/dist/sidecar/process.js +126 -0
  89. package/dist/sidecar/process.js.map +1 -0
  90. package/dist/tools/memoryAppend.d.ts +37 -0
  91. package/dist/tools/memoryAppend.d.ts.map +1 -0
  92. package/dist/tools/memoryAppend.js +99 -0
  93. package/dist/tools/memoryAppend.js.map +1 -0
  94. package/dist/tools/memoryRecall.d.ts +113 -0
  95. package/dist/tools/memoryRecall.d.ts.map +1 -0
  96. package/dist/tools/memoryRecall.js +325 -0
  97. package/dist/tools/memoryRecall.js.map +1 -0
  98. package/dist/trainer/bundleBuilder.d.ts +30 -0
  99. package/dist/trainer/bundleBuilder.d.ts.map +1 -0
  100. package/dist/trainer/bundleBuilder.js +106 -0
  101. package/dist/trainer/bundleBuilder.js.map +1 -0
  102. package/dist/trainer/bundleLoader.d.ts +12 -0
  103. package/dist/trainer/bundleLoader.d.ts.map +1 -0
  104. package/dist/trainer/bundleLoader.js +59 -0
  105. package/dist/trainer/bundleLoader.js.map +1 -0
  106. package/dist/trainer/deltaMerge.d.ts +38 -0
  107. package/dist/trainer/deltaMerge.d.ts.map +1 -0
  108. package/dist/trainer/deltaMerge.js +183 -0
  109. package/dist/trainer/deltaMerge.js.map +1 -0
  110. package/dist/trainer/entityResolver.d.ts +27 -0
  111. package/dist/trainer/entityResolver.d.ts.map +1 -0
  112. package/dist/trainer/entityResolver.js +92 -0
  113. package/dist/trainer/entityResolver.js.map +1 -0
  114. package/dist/trainer/extractFacts.d.ts +67 -0
  115. package/dist/trainer/extractFacts.d.ts.map +1 -0
  116. package/dist/trainer/extractFacts.js +213 -0
  117. package/dist/trainer/extractFacts.js.map +1 -0
  118. package/dist/trainer/index.d.ts +54 -0
  119. package/dist/trainer/index.d.ts.map +1 -0
  120. package/dist/trainer/index.js +82 -0
  121. package/dist/trainer/index.js.map +1 -0
  122. package/dist/trainer/llmExtractor.d.ts +16 -0
  123. package/dist/trainer/llmExtractor.d.ts.map +1 -0
  124. package/dist/trainer/llmExtractor.js +146 -0
  125. package/dist/trainer/llmExtractor.js.map +1 -0
  126. package/dist/trainer/marker.d.ts +10 -0
  127. package/dist/trainer/marker.d.ts.map +1 -0
  128. package/dist/trainer/marker.js +28 -0
  129. package/dist/trainer/marker.js.map +1 -0
  130. package/dist/trainer/scheduler.d.ts +31 -0
  131. package/dist/trainer/scheduler.d.ts.map +1 -0
  132. package/dist/trainer/scheduler.js +72 -0
  133. package/dist/trainer/scheduler.js.map +1 -0
  134. package/dist/trainer/sessionLoader.d.ts +23 -0
  135. package/dist/trainer/sessionLoader.d.ts.map +1 -0
  136. package/dist/trainer/sessionLoader.js +106 -0
  137. package/dist/trainer/sessionLoader.js.map +1 -0
  138. package/dist/types.d.ts +135 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +8 -0
  141. package/dist/types.js.map +1 -0
  142. package/package.json +78 -0
  143. package/src/adapters/piComplete.ts +233 -0
  144. package/src/bundle/install.ts +206 -0
  145. package/src/cli.ts +254 -0
  146. package/src/config.ts +92 -0
  147. package/src/errclass.ts +37 -0
  148. package/src/extension.ts +23 -0
  149. package/src/fallback/index.ts +24 -0
  150. package/src/fallback/llmRerank.ts +90 -0
  151. package/src/fallback/memoryMd.ts +36 -0
  152. package/src/fallback/sessionIndex.ts +289 -0
  153. package/src/fallback/sessionSearch.ts +181 -0
  154. package/src/index.ts +213 -0
  155. package/src/paths.ts +28 -0
  156. package/src/pi-extension.ts +276 -0
  157. package/src/preflight/detectIntents.ts +654 -0
  158. package/src/preflight/hook.ts +136 -0
  159. package/src/preflight/render.ts +185 -0
  160. package/src/preflight/strip.ts +50 -0
  161. package/src/service.ts +202 -0
  162. package/src/sidecar/bundle.ts +52 -0
  163. package/src/sidecar/client.ts +166 -0
  164. package/src/sidecar/process.ts +145 -0
  165. package/src/tools/memoryAppend.ts +113 -0
  166. package/src/tools/memoryRecall.ts +364 -0
  167. package/src/trainer/bundleBuilder.ts +192 -0
  168. package/src/trainer/bundleLoader.ts +105 -0
  169. package/src/trainer/deltaMerge.ts +221 -0
  170. package/src/trainer/entityResolver.ts +140 -0
  171. package/src/trainer/extractFacts.ts +312 -0
  172. package/src/trainer/index.ts +147 -0
  173. package/src/trainer/llmExtractor.ts +206 -0
  174. package/src/trainer/marker.ts +30 -0
  175. package/src/trainer/scheduler.ts +104 -0
  176. package/src/trainer/sessionLoader.ts +139 -0
  177. package/src/types.ts +168 -0
@@ -0,0 +1,364 @@
1
+ import { createFallbackQuery } from "../fallback/index.js";
2
+ import { rerankWithLLM, type RerankOptions, type RankedResult } from "../fallback/llmRerank.js";
3
+ import type { SessionSearchHit } from "../fallback/sessionSearch.js";
4
+ import type { FallbackQuery, MemoryQuerier, MemoryRecallArgs, QueryIntent, QueryMode, ResponseEnvelope, ToolResult } from "../types.js";
5
+
6
+ export const MEMORY_RECALL_NAME = "memory_recall";
7
+
8
+ export const MEMORY_RECALL_DESCRIPTION =
9
+ "Read structured episodic memory from the local TLM sidecar — past sessions consolidated into long-term records.\n" +
10
+ "Modes:\n" +
11
+ "- direct_relation: one-hop predicate (e.g. \"what did X create?\"). Read groups[].via_relations.\n" +
12
+ "- path_query: multi-hop / possessive. relation_constraints is the ordered path; inverse hops use ^-1.\n" +
13
+ "- typed_neighborhood: typed target with exactly one relation. Requires candidate_type.\n\n" +
14
+ "Ground answers in supporting evidence internally; tell the user \"past records\" / \"以前的记录\", not raw event IDs.";
15
+
16
+ export const MEMORY_RECALL_PROMPT_SNIPPET =
17
+ "Query local episodic memory by entity or relationship";
18
+
19
+ export const MEMORY_RECALL_PROMPT_GUIDELINES = [
20
+ "Use memory_recall when the user asks about past sessions, people, projects, or decisions stored in long-term memory.",
21
+ "Use memory_recall for relationship questions (e.g. who is X to me) when implicit preflight did not already answer.",
22
+ ] as const;
23
+
24
+ export const MEMORY_RECALL_PARAMETERS = {
25
+ type: "object",
26
+ properties: {
27
+ mode: {
28
+ type: "string",
29
+ enum: ["direct_relation", "path_query", "typed_neighborhood"],
30
+ },
31
+ anchor_mentions: { type: "array", items: { type: "string" } },
32
+ relation_constraints: { type: "array", items: { type: "string" } },
33
+ candidate_type: { type: "string" },
34
+ scope_filter: { type: "array", items: { type: "string" } },
35
+ target_slot: { type: "string", enum: ["head", "tail"] },
36
+ time_window: { type: "string" },
37
+ evidence_budget: { type: "integer" },
38
+ result_limit: { type: "integer" },
39
+ },
40
+ required: ["anchor_mentions"],
41
+ } as const;
42
+
43
+ export class MemoryRecallTool {
44
+ constructor(
45
+ private readonly service: MemoryQuerier,
46
+ private readonly fallback: FallbackQuery | null = null,
47
+ private readonly rerankOpts: RerankOptions | null = null,
48
+ ) {}
49
+
50
+ info() {
51
+ return {
52
+ name: MEMORY_RECALL_NAME,
53
+ description: MEMORY_RECALL_DESCRIPTION,
54
+ parameters: MEMORY_RECALL_PARAMETERS,
55
+ };
56
+ }
57
+
58
+ async run(argsJson: string, signal?: AbortSignal): Promise<ToolResult> {
59
+ const args = parseArgs(argsJson);
60
+ if ("error" in args) {
61
+ return { content: args.error, isError: true };
62
+ }
63
+
64
+ const intent = argsToIntent(args);
65
+ const validation = validateArgs(args);
66
+ if (validation) {
67
+ return { content: validation, isError: true };
68
+ }
69
+
70
+ return this.runIntent(intent, signal);
71
+ }
72
+
73
+ async runIntent(intent: QueryIntent, signal?: AbortSignal): Promise<ToolResult> {
74
+ if (this.service.status() !== "ready") {
75
+ return this.fallbackResult(intent, "service_unavailable", "fallback");
76
+ }
77
+
78
+ let result = await this.service.query(intent, signal);
79
+ if (result.transportError || result.errorClass === "unavailable") {
80
+ return this.fallbackResult(intent, "service_unavailable", "fallback");
81
+ }
82
+
83
+ if (result.errorClass === "retryable") {
84
+ await sleep(500, signal);
85
+ result = await this.service.query(intent, signal);
86
+ if (
87
+ result.transportError ||
88
+ result.errorClass === "unavailable" ||
89
+ result.errorClass === "retryable"
90
+ ) {
91
+ return this.fallbackResult(intent, "retryable_failed", "fallback_after_retry");
92
+ }
93
+ }
94
+
95
+ if (result.errorClass === "permanent") {
96
+ return permanentResult(result.env);
97
+ }
98
+
99
+ return shapeResult(result.env!);
100
+ }
101
+
102
+ private async fallbackResult(
103
+ intent: QueryIntent,
104
+ reason: string,
105
+ source: string,
106
+ ): Promise<ToolResult> {
107
+ const candidates: Record<string, unknown>[] = [];
108
+ const warnings: Record<string, unknown>[] = [];
109
+
110
+ if (this.fallback) {
111
+ const query = intent.anchor_mentions.map((s) => s.trim()).filter(Boolean).join(" ");
112
+ const limit = intent.result_limit && intent.result_limit > 0 ? intent.result_limit : 10;
113
+ try {
114
+ const hits = await this.fallback.sessionKeyword(query, limit);
115
+
116
+ let reranked: RankedResult[] | null = null;
117
+ if (this.rerankOpts && hits.length > 0) {
118
+ try {
119
+ reranked = await rerankWithLLM(
120
+ query,
121
+ hits as SessionSearchHit[],
122
+ this.rerankOpts,
123
+ );
124
+ } catch {
125
+ /* silent fallback to original order */
126
+ }
127
+ }
128
+
129
+ if (reranked) {
130
+ for (const r of reranked) {
131
+ const original = hits[r.index];
132
+ candidates.push({
133
+ value: r.summary,
134
+ score: r.score,
135
+ evidence: "llm_rerank",
136
+ ...(typeof original === "object" && original !== null &&
137
+ "session_id" in (original as Record<string, unknown>)
138
+ ? { scope: "session_search", session_id: (original as Record<string, unknown>).session_id }
139
+ : {}),
140
+ });
141
+ }
142
+ } else {
143
+ for (const h of hits) {
144
+ candidates.push({
145
+ value: typeof h === "string" ? h : JSON.stringify(h),
146
+ evidence: "text_search",
147
+ ...(typeof h === "object" &&
148
+ h !== null &&
149
+ "session_id" in (h as Record<string, unknown>)
150
+ ? { scope: "session_search" }
151
+ : {}),
152
+ });
153
+ }
154
+ }
155
+ } catch (err) {
156
+ warnings.push({
157
+ code: "fallback_session_search_failed",
158
+ message: err instanceof Error ? err.message : String(err),
159
+ });
160
+ }
161
+ try {
162
+ const snippet = await this.fallback.memoryFileSnippet(query);
163
+ if (snippet) {
164
+ candidates.push({
165
+ value: snippet,
166
+ evidence: "text_search",
167
+ scope: "memory_md",
168
+ });
169
+ }
170
+ } catch {
171
+ /* optional */
172
+ }
173
+ }
174
+
175
+ const out = {
176
+ source,
177
+ evidence_quality: "text_search",
178
+ bundle_version: null,
179
+ candidates,
180
+ warnings,
181
+ fallback_reason: reason,
182
+ };
183
+ return { content: JSON.stringify(out) };
184
+ }
185
+ }
186
+
187
+ function parseArgs(argsJson: string): MemoryRecallArgs | { error: string } {
188
+ let raw: Record<string, unknown>;
189
+ try {
190
+ raw = JSON.parse(coerceArgs(argsJson)) as Record<string, unknown>;
191
+ } catch (e) {
192
+ return { error: `invalid input: ${e instanceof Error ? e.message : e}` };
193
+ }
194
+ const anchor = raw.anchor_mentions;
195
+ if (!Array.isArray(anchor) || anchor.length === 0) {
196
+ return { error: "anchor_mentions is required and must be non-empty" };
197
+ }
198
+ return raw as unknown as MemoryRecallArgs;
199
+ }
200
+
201
+ function argsToIntent(a: MemoryRecallArgs): QueryIntent {
202
+ return {
203
+ mode: (a.mode as QueryMode) || "direct_relation",
204
+ anchor_mentions: a.anchor_mentions,
205
+ relation_constraints: a.relation_constraints,
206
+ candidate_type: a.candidate_type,
207
+ scope_filter: a.scope_filter,
208
+ target_slot: (a.target_slot as QueryIntent["target_slot"]) ?? "",
209
+ time_window: a.time_window,
210
+ evidence_budget: a.evidence_budget && a.evidence_budget > 0 ? a.evidence_budget : 5,
211
+ result_limit: a.result_limit && a.result_limit > 0 ? a.result_limit : 10,
212
+ };
213
+ }
214
+
215
+ function validateArgs(a: MemoryRecallArgs): string | null {
216
+ for (const rel of a.relation_constraints ?? []) {
217
+ if (isBroadRelation(rel)) {
218
+ return "memory_recall requires concrete relation_constraints. Broad relations like related_to are not valid for structured lookup.";
219
+ }
220
+ }
221
+ if (a.mode === "typed_neighborhood") {
222
+ if (!a.candidate_type?.trim()) {
223
+ return "typed_neighborhood requires candidate_type.";
224
+ }
225
+ if ((a.relation_constraints?.length ?? 0) !== 1) {
226
+ return "typed_neighborhood requires exactly one relation_constraints value.";
227
+ }
228
+ }
229
+ return null;
230
+ }
231
+
232
+ function isBroadRelation(rel: string): boolean {
233
+ let r = rel.trim().toLowerCase();
234
+ r = r.replace(/^\^/, "").replace(/\^-1$/, "");
235
+ return ["related_to", "relates_to", "associated_with", "about", "mentions", "other"].includes(r);
236
+ }
237
+
238
+ function coerceArgs(argsJson: string): string {
239
+ let raw: Record<string, unknown>;
240
+ try {
241
+ raw = JSON.parse(argsJson) as Record<string, unknown>;
242
+ } catch {
243
+ return argsJson;
244
+ }
245
+ let changed = false;
246
+ for (const field of ["anchor_mentions", "relation_constraints", "scope_filter"]) {
247
+ const v = raw[field];
248
+ if (typeof v === "string") {
249
+ try {
250
+ raw[field] = JSON.parse(v);
251
+ changed = true;
252
+ } catch {
253
+ /* keep */
254
+ }
255
+ }
256
+ }
257
+ for (const field of ["result_limit", "evidence_budget"]) {
258
+ const v = raw[field];
259
+ if (typeof v === "string") {
260
+ const n = Number(v);
261
+ if (!Number.isNaN(n)) {
262
+ raw[field] = n;
263
+ changed = true;
264
+ }
265
+ }
266
+ }
267
+ return changed ? JSON.stringify(raw) : argsJson;
268
+ }
269
+
270
+ function shapeResult(env: ResponseEnvelope): ToolResult {
271
+ let quality = "structured";
272
+ const warnings = envelopeWarnings(env);
273
+ if (env.reason === "degraded") {
274
+ quality = "structured_degraded";
275
+ warnings.unshift({
276
+ code: "bundle_degraded",
277
+ message: "memory bundle degraded — results may be incomplete",
278
+ });
279
+ }
280
+ const cands = env.candidates.map((c) => {
281
+ const m: Record<string, unknown> = {
282
+ value: c.value,
283
+ score: c.score,
284
+ evidence: c.evidence,
285
+ supporting_event_ids: c.supporting_event_ids,
286
+ };
287
+ if (c.scope != null) m.scope = c.scope;
288
+ if (c.support_count != null) m.support_count = c.support_count;
289
+ if (c.distinct_session_count != null) {
290
+ m.distinct_session_count = c.distinct_session_count;
291
+ }
292
+ return m;
293
+ });
294
+ const out = {
295
+ source: "memory_sidecar",
296
+ evidence_quality: quality,
297
+ bundle_version: env.bundle_version ?? null,
298
+ memory_block: env.memory_block ?? null,
299
+ candidates: cands,
300
+ warnings,
301
+ fallback_reason: null,
302
+ };
303
+ return { content: JSON.stringify(out) };
304
+ }
305
+
306
+ function permanentResult(env: ResponseEnvelope | null): ToolResult {
307
+ const out = {
308
+ source: "memory_sidecar",
309
+ evidence_quality: "structured",
310
+ bundle_version: env?.bundle_version ?? null,
311
+ candidates: [],
312
+ warnings: envelopeWarnings(env),
313
+ fallback_reason: null,
314
+ };
315
+ return { content: JSON.stringify(out), isError: true };
316
+ }
317
+
318
+ function envelopeWarnings(env: ResponseEnvelope | null): Record<string, unknown>[] {
319
+ const out: Record<string, unknown>[] = [];
320
+ if (!env) return out;
321
+ for (const w of env.warnings ?? []) {
322
+ out.push({ code: w.code, message: w.message });
323
+ }
324
+ if (env.error) {
325
+ out.push({
326
+ code: env.error.code,
327
+ message: env.error.message,
328
+ sub_code: env.error.details?.sub_code,
329
+ });
330
+ }
331
+ return out;
332
+ }
333
+
334
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
335
+ return new Promise((resolve, reject) => {
336
+ if (signal?.aborted) {
337
+ reject(new Error("aborted"));
338
+ return;
339
+ }
340
+ const t = setTimeout(resolve, ms);
341
+ signal?.addEventListener("abort", () => {
342
+ clearTimeout(t);
343
+ reject(new Error("aborted"));
344
+ }, { once: true });
345
+ });
346
+ }
347
+
348
+ /** @deprecated Use createFallbackQuery — kept for backward-compatible imports. */
349
+ export function createStubFallback(
350
+ memoryMdPaths: string[],
351
+ sessionsDir = "",
352
+ ): FallbackQuery {
353
+ return createFallbackQuery({ sessionsDir, memoryMdPaths });
354
+ }
355
+
356
+ export { createFallbackQuery };
357
+
358
+ export function createMemoryRecallTool(
359
+ service: MemoryQuerier,
360
+ fallback: FallbackQuery | null,
361
+ rerankOpts?: RerankOptions | null,
362
+ ): MemoryRecallTool {
363
+ return new MemoryRecallTool(service, fallback, rerankOpts ?? null);
364
+ }
@@ -0,0 +1,192 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import type { ResolvedGraph, ResolvedEntity, ResolvedRelation } from "./entityResolver.js";
6
+ import type { ExtractedEvent } from "./extractFacts.js";
7
+ import type { BundleManifest, BundleManifestFile } from "../sidecar/bundle.js";
8
+
9
+ export interface BundleData {
10
+ graph: ResolvedGraph;
11
+ events: ExtractedEvent[];
12
+ }
13
+
14
+ /** TLM-compatible entity record for the graph data file. */
15
+ interface BundleEntity {
16
+ entity_id: string;
17
+ label: string;
18
+ type: string;
19
+ aliases: string[];
20
+ mention_count: number;
21
+ distinct_session_count: number;
22
+ }
23
+
24
+ /** TLM-compatible relation edge for the graph data file. */
25
+ interface BundleEdge {
26
+ head_entity_id: string;
27
+ relation: string;
28
+ tail_entity_id: string;
29
+ supporting_event_ids: string[];
30
+ evidence: string;
31
+ }
32
+
33
+ /** TLM-compatible event record. */
34
+ interface BundleEvent {
35
+ event_id: string;
36
+ description: string;
37
+ session_id: string;
38
+ timestamp: string;
39
+ }
40
+
41
+ interface GraphDataFile {
42
+ entities: BundleEntity[];
43
+ edges: BundleEdge[];
44
+ events: BundleEvent[];
45
+ }
46
+
47
+ function eventId(ev: ExtractedEvent, idx: number): string {
48
+ const hash = createHash("sha256")
49
+ .update(`${ev.sessionId}:${ev.turnIndex}:${idx}`)
50
+ .digest("hex")
51
+ .slice(0, 12);
52
+ return `ev_${hash}`;
53
+ }
54
+
55
+ function distinctSessions(ent: ResolvedEntity): number {
56
+ const s = new Set(ent.mentions.map((m) => m.sessionId));
57
+ return s.size;
58
+ }
59
+
60
+ function buildGraphData(
61
+ graph: ResolvedGraph,
62
+ events: ExtractedEvent[],
63
+ ): GraphDataFile {
64
+ const eventRecords: BundleEvent[] = events.map((ev, i) => ({
65
+ event_id: eventId(ev, i),
66
+ description: ev.description,
67
+ session_id: ev.sessionId,
68
+ timestamp: ev.timestamp,
69
+ }));
70
+
71
+ const eventsBySession = new Map<string, string[]>();
72
+ for (const ev of eventRecords) {
73
+ let arr = eventsBySession.get(ev.session_id);
74
+ if (!arr) {
75
+ arr = [];
76
+ eventsBySession.set(ev.session_id, arr);
77
+ }
78
+ arr.push(ev.event_id);
79
+ }
80
+
81
+ const entities: BundleEntity[] = graph.entities.map((ent) => ({
82
+ entity_id: ent.id,
83
+ label: ent.canonicalName,
84
+ type: ent.type,
85
+ aliases: ent.aliases,
86
+ mention_count: ent.mentions.length,
87
+ distinct_session_count: distinctSessions(ent),
88
+ }));
89
+
90
+ const edges: BundleEdge[] = graph.relations.map((rel) => {
91
+ const evIds = eventsBySession.get(rel.sessionId) ?? [];
92
+ return {
93
+ head_entity_id: rel.headEntityId,
94
+ relation: rel.relation,
95
+ tail_entity_id: rel.tailEntityId,
96
+ supporting_event_ids: evIds.slice(0, 5),
97
+ evidence: rel.evidence,
98
+ };
99
+ });
100
+
101
+ return { entities, edges, events: eventRecords };
102
+ }
103
+
104
+ async function writeJsonFile(
105
+ dir: string,
106
+ filename: string,
107
+ data: unknown,
108
+ ): Promise<BundleManifestFile> {
109
+ const content = JSON.stringify(data, null, 2) + "\n";
110
+ const filePath = path.join(dir, filename);
111
+ await fs.writeFile(filePath, content, "utf8");
112
+ const sha256 = createHash("sha256").update(content).digest("hex");
113
+ return {
114
+ path: filename,
115
+ size: Buffer.byteLength(content, "utf8"),
116
+ sha256,
117
+ };
118
+ }
119
+
120
+ export interface BuildBundleOptions {
121
+ outputDir: string;
122
+ bundleVersion?: string;
123
+ }
124
+
125
+ export interface BuildBundleResult {
126
+ bundleDir: string;
127
+ manifest: BundleManifest;
128
+ stats: {
129
+ entityCount: number;
130
+ edgeCount: number;
131
+ eventCount: number;
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Build a TLM-compatible bundle directory from resolved graph + events.
137
+ *
138
+ * Produces:
139
+ * <outputDir>/bundles/<iso-ts>/
140
+ * manifest.json
141
+ * graph.json — entities, edges, events
142
+ */
143
+ export async function buildBundle(
144
+ data: BundleData,
145
+ opts: BuildBundleOptions,
146
+ ): Promise<BuildBundleResult> {
147
+ const bundleTs = new Date()
148
+ .toISOString()
149
+ .replace(/[:.]/g, "-");
150
+ const bundleVersion = opts.bundleVersion ?? "0.6.0";
151
+
152
+ const bundlesDir = path.join(opts.outputDir, "bundles");
153
+ const bundleDir = path.join(bundlesDir, bundleTs);
154
+ await fs.mkdir(bundleDir, { recursive: true, mode: 0o700 });
155
+
156
+ const graphData = buildGraphData(data.graph, data.events);
157
+ const files: BundleManifestFile[] = [];
158
+
159
+ const graphFile = await writeJsonFile(bundleDir, "graph.json", graphData);
160
+ files.push(graphFile);
161
+
162
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
163
+ const integrityInput = files.map((f) => f.sha256).join(":");
164
+ const integritySha256 = createHash("sha256")
165
+ .update(integrityInput)
166
+ .digest("hex");
167
+
168
+ const manifest: BundleManifest = {
169
+ bundle_ts: bundleTs,
170
+ bundle_version: bundleVersion,
171
+ size_bytes: totalSize,
172
+ integrity_sha256: integritySha256,
173
+ files,
174
+ };
175
+
176
+ const manifestContent = JSON.stringify(manifest, null, 2) + "\n";
177
+ await fs.writeFile(
178
+ path.join(bundleDir, "manifest.json"),
179
+ manifestContent,
180
+ "utf8",
181
+ );
182
+
183
+ return {
184
+ bundleDir,
185
+ manifest,
186
+ stats: {
187
+ entityCount: graphData.entities.length,
188
+ edgeCount: graphData.edges.length,
189
+ eventCount: graphData.events.length,
190
+ },
191
+ };
192
+ }
@@ -0,0 +1,105 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import type { ResolvedEntity, ResolvedRelation, ResolvedGraph } from "./entityResolver.js";
5
+ import type { ExtractedEvent, EntityType } from "./extractFacts.js";
6
+
7
+ interface BundleEntityRecord {
8
+ entity_id: string;
9
+ label: string;
10
+ type: string;
11
+ aliases: string[];
12
+ mention_count: number;
13
+ distinct_session_count: number;
14
+ }
15
+
16
+ interface BundleEdgeRecord {
17
+ head_entity_id: string;
18
+ relation: string;
19
+ tail_entity_id: string;
20
+ supporting_event_ids: string[];
21
+ evidence: string;
22
+ }
23
+
24
+ interface BundleEventRecord {
25
+ event_id: string;
26
+ description: string;
27
+ session_id: string;
28
+ timestamp: string;
29
+ }
30
+
31
+ interface GraphDataFile {
32
+ entities: BundleEntityRecord[];
33
+ edges: BundleEdgeRecord[];
34
+ events: BundleEventRecord[];
35
+ }
36
+
37
+ const VALID_ENTITY_TYPES = new Set<string>([
38
+ "Person", "Company", "Organization", "Project", "Tool",
39
+ "Language", "Concept", "Document", "File", "Location",
40
+ "Product", "Website", "Platform", "Unknown",
41
+ ]);
42
+
43
+ function toEntityType(t: string): EntityType {
44
+ return VALID_ENTITY_TYPES.has(t) ? (t as EntityType) : "Unknown";
45
+ }
46
+
47
+ export interface ExistingBundle {
48
+ graph: ResolvedGraph;
49
+ events: ExtractedEvent[];
50
+ }
51
+
52
+ /**
53
+ * Load the existing bundle's graph.json from `<bundleRoot>/current/graph.json`.
54
+ * Returns null when no current bundle exists or graph.json is missing/unparseable.
55
+ */
56
+ export async function loadExistingBundle(
57
+ bundleRoot: string,
58
+ ): Promise<ExistingBundle | null> {
59
+ const graphPath = path.join(bundleRoot, "current", "graph.json");
60
+
61
+ let raw: string;
62
+ try {
63
+ raw = await fs.readFile(graphPath, "utf8");
64
+ } catch {
65
+ return null;
66
+ }
67
+
68
+ let data: GraphDataFile;
69
+ try {
70
+ data = JSON.parse(raw) as GraphDataFile;
71
+ } catch {
72
+ return null;
73
+ }
74
+
75
+ if (!data.entities || !data.edges) return null;
76
+
77
+ const entities: ResolvedEntity[] = (data.entities ?? []).map((e) => ({
78
+ id: e.entity_id,
79
+ canonicalName: e.label,
80
+ type: toEntityType(e.type),
81
+ aliases: e.aliases ?? [],
82
+ mentions: [],
83
+ }));
84
+
85
+ const relations: ResolvedRelation[] = (data.edges ?? []).map((e) => ({
86
+ headEntityId: e.head_entity_id,
87
+ relation: e.relation,
88
+ tailEntityId: e.tail_entity_id,
89
+ sessionId: "",
90
+ turnIndex: 0,
91
+ evidence: e.evidence ?? "",
92
+ }));
93
+
94
+ const events: ExtractedEvent[] = (data.events ?? []).map((e) => ({
95
+ description: e.description,
96
+ sessionId: e.session_id,
97
+ timestamp: e.timestamp,
98
+ turnIndex: 0,
99
+ }));
100
+
101
+ return {
102
+ graph: { entities, relations },
103
+ events,
104
+ };
105
+ }