@tanstack/ai-memory 0.0.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 (46) hide show
  1. package/dist/esm/index.d.ts +2 -0
  2. package/dist/esm/index.js +2 -0
  3. package/dist/esm/internal/store.d.ts +95 -0
  4. package/dist/esm/internal/store.js +201 -0
  5. package/dist/esm/internal/store.js.map +1 -0
  6. package/dist/esm/internal/store.test.d.ts +1 -0
  7. package/dist/esm/middleware.d.ts +73 -0
  8. package/dist/esm/middleware.js +250 -0
  9. package/dist/esm/middleware.js.map +1 -0
  10. package/dist/esm/providers/hindsight/index.d.ts +50 -0
  11. package/dist/esm/providers/hindsight/index.js +152 -0
  12. package/dist/esm/providers/hindsight/index.js.map +1 -0
  13. package/dist/esm/providers/hindsight/tools.d.ts +16 -0
  14. package/dist/esm/providers/hindsight/tools.js +119 -0
  15. package/dist/esm/providers/hindsight/tools.js.map +1 -0
  16. package/dist/esm/providers/honcho/index.d.ts +16 -0
  17. package/dist/esm/providers/honcho/index.js +150 -0
  18. package/dist/esm/providers/honcho/index.js.map +1 -0
  19. package/dist/esm/providers/in-memory/index.d.ts +19 -0
  20. package/dist/esm/providers/in-memory/index.js +46 -0
  21. package/dist/esm/providers/in-memory/index.js.map +1 -0
  22. package/dist/esm/providers/mem0/index.d.ts +14 -0
  23. package/dist/esm/providers/mem0/index.js +150 -0
  24. package/dist/esm/providers/mem0/index.js.map +1 -0
  25. package/dist/esm/providers/redis/index.d.ts +54 -0
  26. package/dist/esm/providers/redis/index.js +118 -0
  27. package/dist/esm/providers/redis/index.js.map +1 -0
  28. package/dist/esm/types.d.ts +112 -0
  29. package/package.json +100 -0
  30. package/skills/tanstack-ai-memory/SKILL.md +99 -0
  31. package/skills/tanstack-ai-memory-hindsight/SKILL.md +40 -0
  32. package/skills/tanstack-ai-memory-honcho/SKILL.md +40 -0
  33. package/skills/tanstack-ai-memory-in-memory/SKILL.md +51 -0
  34. package/skills/tanstack-ai-memory-mem0/SKILL.md +36 -0
  35. package/skills/tanstack-ai-memory-redis/SKILL.md +83 -0
  36. package/src/index.ts +20 -0
  37. package/src/internal/store.test.ts +63 -0
  38. package/src/internal/store.ts +378 -0
  39. package/src/middleware.ts +386 -0
  40. package/src/providers/hindsight/index.ts +237 -0
  41. package/src/providers/hindsight/tools.ts +139 -0
  42. package/src/providers/honcho/index.ts +238 -0
  43. package/src/providers/in-memory/index.ts +63 -0
  44. package/src/providers/mem0/index.ts +199 -0
  45. package/src/providers/redis/index.ts +179 -0
  46. package/src/types.ts +159 -0
@@ -0,0 +1,386 @@
1
+ import { aiEventClient } from '@tanstack/ai-event-client'
2
+ import type {
3
+ ChatMiddleware,
4
+ ChatMiddlewareConfig,
5
+ ChatMiddlewareContext,
6
+ ModelMessage,
7
+ StreamChunk,
8
+ } from '@tanstack/ai'
9
+ import type {
10
+ MemoryAdapter,
11
+ MemoryFact,
12
+ MemoryScope,
13
+ MemoryTurn,
14
+ RecallResult,
15
+ SaveReceipt,
16
+ } from './types'
17
+
18
+ /**
19
+ * CUSTOM stream-event name carrying server-side memory state to the browser.
20
+ * The middleware injects one of these per turn (via `onChunk`); the client
21
+ * devtools bridge (`@tanstack/ai-client`) recognizes it and re-emits `memory:*`
22
+ * on the browser event bus. This is how server-side memory reaches the browser
23
+ * DevTools panel — server-emitted `aiEventClient` events never cross runtimes;
24
+ * everything the panel shows is re-derived client-side from the chat stream
25
+ * (mirrors how generation results ride `CUSTOM` events — see `GENERATION_EVENTS`).
26
+ */
27
+ export const MEMORY_STATE_EVENT = 'memory:state'
28
+
29
+ /** Payload of the {@link MEMORY_STATE_EVENT} CUSTOM chunk. Captures memory state
30
+ * as of the turn's START — the snapshot reflects every prior turn's save; this
31
+ * turn's own save (deferred) surfaces in the next turn's snapshot. */
32
+ export interface MemoryStateEventValue {
33
+ scope: MemoryScope
34
+ adapter: string
35
+ /** The recall query (last user text). */
36
+ query: string
37
+ /** Recall metrics for the operations timeline. */
38
+ recall: {
39
+ fragmentCount: number
40
+ hasTools: boolean
41
+ systemPromptChars: number
42
+ durationMs: number
43
+ }
44
+ /** Live store snapshot, when the adapter supports `inspect`/`listFacts`. */
45
+ snapshot?: {
46
+ takenAt: string
47
+ data: unknown
48
+ facts: Array<MemoryFact>
49
+ }
50
+ }
51
+
52
+ /**
53
+ * How the middleware participates in the run:
54
+ * - `'recall+save'` (default): recall on init (inject prompt + tools), save on finish.
55
+ * - `'save-only'`: skip recall entirely — persist the turn but never read/inject.
56
+ */
57
+ export type MemoryMiddlewareRole = 'recall+save' | 'save-only'
58
+
59
+ export interface MemoryRecallInfo {
60
+ scope: MemoryScope
61
+ query: string
62
+ result: RecallResult
63
+ }
64
+
65
+ export interface MemorySaveInfo {
66
+ scope: MemoryScope
67
+ turn: MemoryTurn
68
+ receipts: Array<SaveReceipt>
69
+ }
70
+
71
+ export interface MemoryMiddlewareOptions {
72
+ /** The memory backend to recall from / save to. */
73
+ adapter: MemoryAdapter
74
+ /**
75
+ * Scope for every adapter call. The function form is the safer default for
76
+ * multi-tenant apps: derive scope per request from trusted, server-validated
77
+ * chat context — never from client input.
78
+ */
79
+ scope:
80
+ | MemoryScope
81
+ | ((ctx: ChatMiddlewareContext) => MemoryScope | Promise<MemoryScope>)
82
+ /** Participation role. Defaults to `'recall+save'`. */
83
+ role?: MemoryMiddlewareRole
84
+ /** Fired after `recall` completes (post-injection), for app telemetry. */
85
+ onRecall?: (info: MemoryRecallInfo) => void | Promise<void>
86
+ /** Fired after the deferred `save` completes, for app telemetry. */
87
+ onSave?: (info: MemorySaveInfo) => void | Promise<void>
88
+ }
89
+
90
+ /** Per-request scratch state, keyed by context in a module-level WeakMap so the
91
+ * same middleware instance is safe across concurrent `chat()` calls. */
92
+ interface MemoryRequestState {
93
+ resolvedScope?: MemoryScope
94
+ lastUserText: string
95
+ /** Pending devtools transport chunk, injected once by the first `onChunk`. */
96
+ stateChunk?: { emitted: boolean; value: MemoryStateEventValue }
97
+ }
98
+
99
+ const stateByCtx = new WeakMap<ChatMiddlewareContext, MemoryRequestState>()
100
+
101
+ /**
102
+ * Server-side memory middleware. Recalls relevant memory into the prompt before
103
+ * the model runs, then defers `save` of the completed turn after it finishes.
104
+ * All extraction/ranking/rendering lives in the adapter — this middleware only
105
+ * wires `recall`/`save` into the chat lifecycle and emits devtools events.
106
+ */
107
+ export function memoryMiddleware(
108
+ options: MemoryMiddlewareOptions,
109
+ ): ChatMiddleware {
110
+ const role = options.role ?? 'recall+save'
111
+
112
+ async function resolveScope(
113
+ ctx: ChatMiddlewareContext,
114
+ state: MemoryRequestState,
115
+ ): Promise<MemoryScope> {
116
+ if (state.resolvedScope) return state.resolvedScope
117
+ state.resolvedScope =
118
+ typeof options.scope === 'function'
119
+ ? await options.scope(ctx)
120
+ : options.scope
121
+ return state.resolvedScope
122
+ }
123
+
124
+ return {
125
+ name: `memory:${options.adapter.id}`,
126
+
127
+ async onConfig(ctx, config) {
128
+ if (ctx.phase !== 'init') return
129
+
130
+ const state: MemoryRequestState = { lastUserText: '' }
131
+ stateByCtx.set(ctx, state)
132
+
133
+ state.lastUserText = getMessageText(findLastUserMessage(config.messages))
134
+ if (!state.lastUserText || role === 'save-only') return
135
+
136
+ const startedAt = Date.now()
137
+ let scope: MemoryScope
138
+ let result: RecallResult
139
+ try {
140
+ scope = await resolveScope(ctx, state)
141
+ safeEmit('memory:retrieve:started', {
142
+ scope,
143
+ adapter: options.adapter.id,
144
+ query: state.lastUserText,
145
+ timestamp: startedAt,
146
+ })
147
+ result = await options.adapter.recall(scope, state.lastUserText)
148
+ } catch (error) {
149
+ safeEmit('memory:error', {
150
+ // Only attach scope when resolve already succeeded; otherwise omit
151
+ // (no empty-string / partial fake identity).
152
+ ...(state.resolvedScope ? { scope: state.resolvedScope } : {}),
153
+ adapter: options.adapter.id,
154
+ phase: 'recall',
155
+ error: errorInfo(error),
156
+ timestamp: Date.now(),
157
+ })
158
+ return
159
+ }
160
+
161
+ const tools = result.tools ?? []
162
+ const recallMetrics = {
163
+ fragmentCount: result.fragments?.length ?? 0,
164
+ hasTools: tools.length > 0,
165
+ systemPromptChars: result.systemPrompt.length,
166
+ durationMs: Date.now() - startedAt,
167
+ }
168
+ safeEmit('memory:retrieve:completed', {
169
+ scope,
170
+ adapter: options.adapter.id,
171
+ ...recallMetrics,
172
+ timestamp: Date.now(),
173
+ })
174
+ await options.onRecall?.({ scope, query: state.lastUserText, result })
175
+
176
+ // Stage the devtools transport chunk (recall metrics + current store
177
+ // snapshot). Injected into the stream by `onChunk` so it reaches the
178
+ // browser panel; see MEMORY_STATE_EVENT.
179
+ const snapshot = await gatherSnapshot(options.adapter, scope)
180
+ state.stateChunk = {
181
+ emitted: false,
182
+ value: {
183
+ scope,
184
+ adapter: options.adapter.id,
185
+ query: state.lastUserText,
186
+ recall: recallMetrics,
187
+ ...(snapshot ? { snapshot } : {}),
188
+ },
189
+ }
190
+
191
+ const memoryPrompts = [result.toolGuidance ?? '', result.systemPrompt]
192
+ const additions = memoryPrompts.filter((p) => p.length > 0)
193
+ if (additions.length === 0 && tools.length === 0) return
194
+
195
+ return {
196
+ systemPrompts: [...config.systemPrompts, ...additions],
197
+ tools: [...config.tools, ...tools],
198
+ } satisfies Partial<ChatMiddlewareConfig>
199
+ },
200
+
201
+ onChunk(ctx, chunk) {
202
+ // Inject the staged memory-state chunk exactly once, riding alongside the
203
+ // first stream chunk (typically RUN_STARTED) so the browser devtools sees
204
+ // it. Returning an array expands the stream; see ChatMiddleware.onChunk.
205
+ const state = stateByCtx.get(ctx)
206
+ if (!state?.stateChunk || state.stateChunk.emitted) return
207
+ state.stateChunk.emitted = true
208
+ const custom: StreamChunk = {
209
+ type: 'CUSTOM',
210
+ name: MEMORY_STATE_EVENT,
211
+ value: state.stateChunk.value,
212
+ timestamp: Date.now(),
213
+ }
214
+ return [chunk, custom]
215
+ },
216
+
217
+ onFinish(ctx, info) {
218
+ const state = stateByCtx.get(ctx)
219
+ stateByCtx.delete(ctx)
220
+ const userText =
221
+ state?.lastUserText || getMessageText(findLastUserMessage(ctx.messages))
222
+ const assistant = info.content
223
+ if (!userText || !assistant) return
224
+ const scope = state?.resolvedScope
225
+
226
+ ctx.defer(
227
+ (async () => {
228
+ // Resolve scope defensively — a throwing resolver must not escape the
229
+ // terminal hook. Memory failures are always non-fatal + observable.
230
+ let resolved: MemoryScope
231
+ try {
232
+ resolved =
233
+ scope ?? (await resolveScope(ctx, { lastUserText: userText }))
234
+ } catch (error) {
235
+ safeEmit('memory:error', {
236
+ adapter: options.adapter.id,
237
+ phase: 'save',
238
+ error: errorInfo(error),
239
+ timestamp: Date.now(),
240
+ })
241
+ return
242
+ }
243
+
244
+ const turn: MemoryTurn = { user: userText, assistant }
245
+ const startedAt = Date.now()
246
+ safeEmit('memory:persist:started', {
247
+ scope: resolved,
248
+ adapter: options.adapter.id,
249
+ timestamp: startedAt,
250
+ })
251
+ let receipts: Array<SaveReceipt>
252
+ try {
253
+ receipts = await options.adapter.save(resolved, turn)
254
+ } catch (error) {
255
+ receipts = [{ ok: false, error: String(error) }]
256
+ safeEmit('memory:error', {
257
+ scope: resolved,
258
+ adapter: options.adapter.id,
259
+ phase: 'save',
260
+ error: errorInfo(error),
261
+ timestamp: Date.now(),
262
+ })
263
+ }
264
+ safeEmit('memory:persist:completed', {
265
+ scope: resolved,
266
+ adapter: options.adapter.id,
267
+ receiptCount: receipts.length,
268
+ okCount: receipts.filter((r) => r.ok).length,
269
+ durationMs: Date.now() - startedAt,
270
+ timestamp: Date.now(),
271
+ })
272
+ await emitSnapshot(options.adapter, resolved)
273
+ await options.onSave?.({ scope: resolved, turn, receipts })
274
+ })(),
275
+ )
276
+ },
277
+ }
278
+ }
279
+
280
+ // ===========================
281
+ // Internals
282
+ // ===========================
283
+
284
+ /**
285
+ * Read the adapter's current stored state via the optional `inspect`/`listFacts`
286
+ * introspection methods. Returns `undefined` for adapters that don't implement
287
+ * `inspect` (they degrade to the metrics-only timeline). Fully guarded:
288
+ * introspection must never affect chat.
289
+ */
290
+ async function gatherSnapshot(
291
+ adapter: MemoryAdapter,
292
+ scope: MemoryScope,
293
+ ): Promise<
294
+ { takenAt: string; data: unknown; facts: Array<MemoryFact> } | undefined
295
+ > {
296
+ if (!adapter.inspect) return undefined
297
+ try {
298
+ const snapshot = await adapter.inspect(scope)
299
+ const facts = (await adapter.listFacts?.(scope)) ?? []
300
+ return { takenAt: snapshot.takenAt, data: snapshot.data, facts }
301
+ } catch {
302
+ // ignored — introspection is best-effort telemetry.
303
+ return undefined
304
+ }
305
+ }
306
+
307
+ /**
308
+ * DevTools-only: after a save, emit the adapter's current stored state on the
309
+ * (in-process) event bus, so a devtools consumer running in the SAME runtime as
310
+ * the chat (client-side execution / server-side listener) sees "what's in
311
+ * memory". For the standard server-side topology, the browser panel instead
312
+ * gets state via the {@link MEMORY_STATE_EVENT} stream chunk (see `onChunk`).
313
+ */
314
+ async function emitSnapshot(
315
+ adapter: MemoryAdapter,
316
+ scope: MemoryScope,
317
+ ): Promise<void> {
318
+ const snapshot = await gatherSnapshot(adapter, scope)
319
+ if (!snapshot) return
320
+ safeEmit('memory:snapshot', {
321
+ scope,
322
+ adapter: adapter.id,
323
+ ...snapshot,
324
+ timestamp: Date.now(),
325
+ })
326
+ }
327
+
328
+ function findLastUserMessage(
329
+ messages: ReadonlyArray<ModelMessage>,
330
+ ): ModelMessage | undefined {
331
+ for (let i = messages.length - 1; i >= 0; i--) {
332
+ const message = messages[i]
333
+ if (message && message.role === 'user') return message
334
+ }
335
+ return undefined
336
+ }
337
+
338
+ /**
339
+ * Extract plain text from a `ModelMessage`. Text lives on `part.content` for
340
+ * `TextPart`; bare strings in the content array are tolerated. All other
341
+ * content kinds (tool-call, image, …) yield '' so they don't pollute the
342
+ * recall query.
343
+ */
344
+ function getMessageText(message?: ModelMessage): string {
345
+ if (!message) return ''
346
+ if (typeof message.content === 'string') return message.content
347
+ if (Array.isArray(message.content)) {
348
+ return message.content
349
+ .map((part) => {
350
+ if (typeof part === 'string') return part
351
+ if (part.type === 'text' && typeof part.content === 'string') {
352
+ return part.content
353
+ }
354
+ return ''
355
+ })
356
+ .filter(Boolean)
357
+ .join('\n')
358
+ }
359
+ return ''
360
+ }
361
+
362
+ function errorInfo(error: unknown): { name: string; message: string } {
363
+ if (error instanceof Error)
364
+ return { name: error.name, message: error.message }
365
+ if (
366
+ error &&
367
+ typeof error === 'object' &&
368
+ 'name' in error &&
369
+ typeof error.name === 'string'
370
+ ) {
371
+ return {
372
+ name: error.name,
373
+ message: String((error as { message?: unknown }).message ?? error),
374
+ }
375
+ }
376
+ return { name: 'Error', message: String(error) }
377
+ }
378
+
379
+ /** Fire-and-forget devtools emit — telemetry failures must never affect chat. */
380
+ function safeEmit(...args: Parameters<typeof aiEventClient.emit>): void {
381
+ try {
382
+ aiEventClient.emit(...args)
383
+ } catch {
384
+ // ignored — telemetry must not affect chat behaviour
385
+ }
386
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Hindsight memory adapter. Hindsight owns extraction/ranking server-side and
3
+ * buckets memory into per-conversation "banks"
4
+ * (`{tenantId|_}__{userId}__{threadId}`). Recall
5
+ * returns a rendered prompt block AND a set of LLM tools (retain/recall/reflect)
6
+ * that let the model take direct control of memory.
7
+ *
8
+ * `@vectorize-io/hindsight-client` is an OPTIONAL peer dependency, loaded lazily.
9
+ */
10
+
11
+ import { makeHindsightTools } from './tools'
12
+ import type {
13
+ MemoryAdapter,
14
+ MemoryFact,
15
+ MemoryFragment,
16
+ MemoryScope,
17
+ MemorySnapshot,
18
+ MemoryTurn,
19
+ RecallResult,
20
+ SaveReceipt,
21
+ } from '../../types'
22
+
23
+ /** Recall payload shape (the subset this adapter reads). */
24
+ export interface HindsightRecallResponse {
25
+ results?: Array<{ text: string; type?: string; id: string }>
26
+ }
27
+
28
+ /**
29
+ * Structural view of the hindsight client — only the methods this adapter uses.
30
+ * Decouples the adapter from the SDK's exact type surface.
31
+ */
32
+ export interface HindsightClientLike {
33
+ retain: (
34
+ bankId: string,
35
+ text: string,
36
+ opts: { context: string; timestamp: Date },
37
+ ) => Promise<unknown>
38
+ recall: (
39
+ bankId: string,
40
+ query: string,
41
+ opts: { budget: string },
42
+ ) => Promise<HindsightRecallResponse>
43
+ reflect: (bankId: string, query: string) => Promise<{ text?: string }>
44
+ listMemories: (
45
+ bankId: string,
46
+ opts: { limit: number },
47
+ ) => Promise<{ items?: Array<Record<string, unknown>> }>
48
+ getBankProfile: (bankId: string) => Promise<unknown>
49
+ deleteBank: (bankId: string) => Promise<unknown>
50
+ }
51
+
52
+ export interface HindsightRuntime {
53
+ client: HindsightClientLike
54
+ recallToPrompt: (data: unknown) => string
55
+ }
56
+
57
+ export interface HindsightOptions {
58
+ /** Durable user id used in the bank key. Falls back to `scope.userId`, then `'demo-user'`. */
59
+ user?: string
60
+ /** Hindsight server URL. Defaults to `HINDSIGHT_URL` or `http://localhost:8888`. */
61
+ baseUrl?: string
62
+ /** Recall budget. Defaults to `'mid'`. */
63
+ budget?: 'low' | 'mid' | 'high'
64
+ /** Fired when a `hindsight_retain` tool call completes. */
65
+ onToolRetain?: (receipt: SaveReceipt) => void
66
+ /** Fired when a `hindsight_recall` tool call completes. */
67
+ onToolRecall?: (query: string, result: RecallResult) => void
68
+ }
69
+
70
+ const TOOL_GUIDANCE = `You have access to persistent long-term memory that survives across sessions.
71
+
72
+ Relevant memories for this turn have already been recalled and included in
73
+ your context. You also have three tools for direct control over memory:
74
+
75
+ - hindsight_retain(content): explicitly store a fact, decision, or piece of
76
+ context you want to ensure is remembered in future sessions.
77
+
78
+ - hindsight_recall(query): query memory directly with a specific question,
79
+ to look up a different topic than the user's last message.
80
+
81
+ - hindsight_reflect(question): synthesize across many memories to answer
82
+ questions that require reasoning over accumulated knowledge.
83
+
84
+ Prefer to use these tools when they would meaningfully improve your response.
85
+ You do not need to call them on every turn.`
86
+
87
+ export function hindsight(options: HindsightOptions = {}): MemoryAdapter {
88
+ const budget = options.budget ?? 'mid'
89
+ let runtimePromise: Promise<HindsightRuntime> | null = null
90
+
91
+ function getRuntime(): Promise<HindsightRuntime> {
92
+ if (!runtimePromise) {
93
+ runtimePromise = (async () => {
94
+ const mod = await import('@vectorize-io/hindsight-client')
95
+ const baseUrl =
96
+ options.baseUrl ??
97
+ process.env.HINDSIGHT_URL ??
98
+ 'http://localhost:8888'
99
+ // oxlint-disable-next-line eslint-js/no-restricted-syntax -- intentionally decoupled from the SDK's exact client type; the adapter only uses the HindsightClientLike subset
100
+ const client = new mod.HindsightClient({
101
+ baseUrl,
102
+ }) as unknown as HindsightClientLike
103
+ const recallToPrompt = mod.recallResponseToPromptString as (
104
+ data: unknown,
105
+ ) => string
106
+ return { client, recallToPrompt }
107
+ })().catch((err) => {
108
+ runtimePromise = null
109
+ throw err
110
+ })
111
+ }
112
+ return runtimePromise
113
+ }
114
+
115
+ function bankId(scope: MemoryScope): string {
116
+ const user = options.user ?? scope.userId ?? 'demo-user'
117
+ // Include tenant so multi-tenant deploys cannot share banks when user+thread
118
+ // collide. Unset tenant uses `_` (same placeholder convention as redis).
119
+ const tenant =
120
+ scope.tenantId != null && scope.tenantId !== '' ? scope.tenantId : '_'
121
+ return `${tenant}__${user}__${scope.threadId}`
122
+ }
123
+
124
+ return {
125
+ id: 'hindsight',
126
+
127
+ async save(scope, turn: MemoryTurn): Promise<Array<SaveReceipt>> {
128
+ const bank = bankId(scope)
129
+ const timestamp = new Date()
130
+ async function retain(
131
+ text: string,
132
+ context: string,
133
+ ): Promise<SaveReceipt> {
134
+ const start = Date.now()
135
+ try {
136
+ const { client } = await getRuntime()
137
+ const data = await client.retain(bank, text, { context, timestamp })
138
+ return { ok: true, latencyMs: Date.now() - start, raw: data }
139
+ } catch (err) {
140
+ return {
141
+ ok: false,
142
+ latencyMs: Date.now() - start,
143
+ error: err instanceof Error ? err.message : String(err),
144
+ }
145
+ }
146
+ }
147
+ return Promise.all([
148
+ retain(turn.user, 'chat:user'),
149
+ retain(turn.assistant, 'chat:assistant'),
150
+ ])
151
+ },
152
+
153
+ async recall(scope, query): Promise<RecallResult> {
154
+ const bank = bankId(scope)
155
+ const tools = makeHindsightTools({
156
+ getRuntime,
157
+ bankId: bank,
158
+ budget,
159
+ onToolRetain: options.onToolRetain,
160
+ onToolRecall: options.onToolRecall,
161
+ })
162
+ try {
163
+ const { client, recallToPrompt } = await getRuntime()
164
+ const data = await client.recall(bank, query, { budget })
165
+ const fragments: Array<MemoryFragment> = (data.results ?? []).map(
166
+ (r) => ({
167
+ text: r.text,
168
+ source: r.type ?? r.id,
169
+ }),
170
+ )
171
+ return {
172
+ systemPrompt: recallToPrompt(data),
173
+ fragments,
174
+ tools,
175
+ toolGuidance: TOOL_GUIDANCE,
176
+ raw: data,
177
+ }
178
+ } catch (err) {
179
+ return {
180
+ systemPrompt: '',
181
+ fragments: [],
182
+ tools,
183
+ toolGuidance: TOOL_GUIDANCE,
184
+ raw: { error: err instanceof Error ? err.message : String(err) },
185
+ }
186
+ }
187
+ },
188
+
189
+ async inspect(scope): Promise<MemorySnapshot> {
190
+ const bank = bankId(scope)
191
+ try {
192
+ const { client } = await getRuntime()
193
+ const [memories, profile] = await Promise.all([
194
+ client.listMemories(bank, { limit: 200 }),
195
+ client.getBankProfile(bank),
196
+ ])
197
+ return {
198
+ takenAt: new Date().toISOString(),
199
+ data: { memories, profile },
200
+ }
201
+ } catch (err) {
202
+ return {
203
+ takenAt: new Date().toISOString(),
204
+ data: { error: err instanceof Error ? err.message : String(err) },
205
+ }
206
+ }
207
+ },
208
+
209
+ async listFacts(scope): Promise<Array<MemoryFact>> {
210
+ const bank = bankId(scope)
211
+ try {
212
+ const { client } = await getRuntime()
213
+ const res = await client.listMemories(bank, { limit: 200 })
214
+ const items = res.items ?? []
215
+ return items
216
+ .map((m, i): MemoryFact | null => {
217
+ const text =
218
+ (typeof m.text === 'string' ? m.text : undefined) ??
219
+ (typeof m.content === 'string' ? m.content : undefined)
220
+ if (!text) return null
221
+ return {
222
+ id: typeof m.id === 'string' ? m.id : `hindsight-${i}`,
223
+ text,
224
+ source: typeof m.context === 'string' ? m.context : 'memory',
225
+ createdAt:
226
+ typeof m.created_at === 'string' ? m.created_at : undefined,
227
+ }
228
+ })
229
+ .filter((f): f is MemoryFact => f !== null)
230
+ } catch {
231
+ return []
232
+ }
233
+ },
234
+ }
235
+ }
236
+
237
+ export { makeHindsightTools } from './tools'