@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,139 @@
1
+ import type { Tool } from '@tanstack/ai'
2
+ import type { MemoryFragment, RecallResult, SaveReceipt } from '../../types'
3
+ import type { HindsightRuntime } from './index'
4
+
5
+ export interface HindsightToolDeps {
6
+ getRuntime: () => Promise<HindsightRuntime>
7
+ bankId: string
8
+ budget: string
9
+ onToolRetain?: (receipt: SaveReceipt) => void
10
+ onToolRecall?: (query: string, result: RecallResult) => void
11
+ }
12
+
13
+ function stringField(args: unknown, key: string): string {
14
+ if (args && typeof args === 'object' && key in args) {
15
+ const value = (args as Record<string, unknown>)[key]
16
+ if (typeof value === 'string') return value
17
+ }
18
+ return ''
19
+ }
20
+
21
+ /**
22
+ * Build the hindsight LLM tools (retain / recall / reflect). These let the model
23
+ * take direct control of long-term memory beyond the automatic recall/save the
24
+ * middleware performs. Returned in `RecallResult.tools` and merged into the run.
25
+ */
26
+ export function makeHindsightTools(deps: HindsightToolDeps): Array<Tool> {
27
+ const retainTool: Tool = {
28
+ name: 'hindsight_retain',
29
+ description:
30
+ 'Explicitly store a fact, decision, or piece of context to remember in future sessions. Call this when the user shares something important about themselves, their preferences, their work, or any detail that should persist beyond this conversation.',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {
34
+ content: {
35
+ type: 'string',
36
+ description:
37
+ 'The exact fact, decision, or piece of context to store. Write it as a self-contained statement that will still make sense out of conversation context.',
38
+ },
39
+ },
40
+ required: ['content'],
41
+ additionalProperties: false,
42
+ },
43
+ async execute(args) {
44
+ const content = stringField(args, 'content')
45
+ const start = Date.now()
46
+ try {
47
+ const { client } = await deps.getRuntime()
48
+ const data = await client.retain(deps.bankId, content, {
49
+ context: 'chat:tool',
50
+ timestamp: new Date(),
51
+ })
52
+ deps.onToolRetain?.({
53
+ ok: true,
54
+ latencyMs: Date.now() - start,
55
+ raw: data,
56
+ })
57
+ return { ok: true }
58
+ } catch (err) {
59
+ const error = err instanceof Error ? err.message : String(err)
60
+ deps.onToolRetain?.({ ok: false, latencyMs: Date.now() - start, error })
61
+ return { ok: false, error }
62
+ }
63
+ },
64
+ }
65
+
66
+ const recallTool: Tool = {
67
+ name: 'hindsight_recall',
68
+ description:
69
+ "Query memory directly with a specific question. Use this when you need context that may not have surfaced in the automatic recall — for example, to look up a different topic than the user's last message, or to find facts about an entity mentioned in passing.",
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ query: {
74
+ type: 'string',
75
+ description: 'Natural-language question or topic to look up.',
76
+ },
77
+ },
78
+ required: ['query'],
79
+ additionalProperties: false,
80
+ },
81
+ async execute(args) {
82
+ const query = stringField(args, 'query')
83
+ const start = Date.now()
84
+ try {
85
+ const { client, recallToPrompt } = await deps.getRuntime()
86
+ const data = await client.recall(deps.bankId, query, {
87
+ budget: deps.budget,
88
+ })
89
+ const systemPrompt = recallToPrompt(data)
90
+ const fragments: Array<MemoryFragment> = (data.results ?? []).map(
91
+ (r) => ({
92
+ text: r.text,
93
+ source: r.type ?? r.id,
94
+ }),
95
+ )
96
+ deps.onToolRecall?.(query, {
97
+ systemPrompt,
98
+ fragments,
99
+ latencyMs: Date.now() - start,
100
+ raw: data,
101
+ } as RecallResult)
102
+ return systemPrompt || '(no relevant memories found)'
103
+ } catch (err) {
104
+ const error = err instanceof Error ? err.message : String(err)
105
+ return `(no memory available: ${error})`
106
+ }
107
+ },
108
+ }
109
+
110
+ const reflectTool: Tool = {
111
+ name: 'hindsight_reflect',
112
+ description:
113
+ 'Synthesize across many memories to answer questions that require reasoning over accumulated knowledge, rather than retrieving specific facts. Use this for questions like "what do I know about this user\'s stack?" or "what has the user been working on lately?"',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ query: {
118
+ type: 'string',
119
+ description:
120
+ 'The synthesis question to reflect on, e.g. "what do I know about the user\'s preferences?"',
121
+ },
122
+ },
123
+ required: ['query'],
124
+ additionalProperties: false,
125
+ },
126
+ async execute(args) {
127
+ const query = stringField(args, 'query')
128
+ try {
129
+ const { client } = await deps.getRuntime()
130
+ const data = await client.reflect(deps.bankId, query)
131
+ return data.text ?? '(no reflection)'
132
+ } catch (err) {
133
+ return `(reflection failed: ${err instanceof Error ? err.message : String(err)})`
134
+ }
135
+ },
136
+ }
137
+
138
+ return [retainTool, recallTool, reflectTool]
139
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Honcho memory adapter. Honcho models memory as peers exchanging messages in a
3
+ * session and answers recall via a "dialectic" query over the user peer's
4
+ * representation — so `recall` returns a synthesized answer (no discrete
5
+ * fragments) and `save` appends the turn's messages to the session.
6
+ *
7
+ * `@honcho-ai/sdk` is an OPTIONAL peer dependency, loaded lazily on first use.
8
+ */
9
+
10
+ import type { Peer, Session } from '@honcho-ai/sdk'
11
+ import type {
12
+ MemoryAdapter,
13
+ MemoryFact,
14
+ MemoryScope,
15
+ MemorySnapshot,
16
+ MemoryTurn,
17
+ RecallResult,
18
+ SaveReceipt,
19
+ } from '../../types'
20
+
21
+ export interface HonchoOptions {
22
+ /** Durable user id. Falls back to `scope.userId`, then `'demo-user'`. */
23
+ user?: string
24
+ /** Honcho server URL. Defaults to `HONCHO_URL` or `http://localhost:8001`. */
25
+ baseURL?: string
26
+ /** Workspace id. Defaults to `HONCHO_APP_NAME` or `'ai-memory'`. */
27
+ workspaceId?: string
28
+ /** API key. Defaults to `HONCHO_API_KEY` (or `'dev-no-auth'`). */
29
+ apiKey?: string
30
+ /** Assistant peer id. Defaults to `'assistant'`. */
31
+ assistantId?: string
32
+ }
33
+
34
+ type Timed<T> =
35
+ | { ok: true; latencyMs: number; data: T }
36
+ | { ok: false; latencyMs: number; error: string }
37
+
38
+ async function timed<T>(fn: () => Promise<T>): Promise<Timed<T>> {
39
+ const start = Date.now()
40
+ try {
41
+ const data = await fn()
42
+ return { ok: true, latencyMs: Date.now() - start, data }
43
+ } catch (err) {
44
+ return {
45
+ ok: false,
46
+ latencyMs: Date.now() - start,
47
+ error: err instanceof Error ? err.message : String(err),
48
+ }
49
+ }
50
+ }
51
+
52
+ const HONCHO_LINE_RE = /^\[(?<ts>[^\]]+)\]\s+(?<text>.+)$/
53
+
54
+ /** Parse a Honcho `peer.representation()` text blob into flat fact rows. */
55
+ export function parseHonchoRepresentation(raw: string): Array<MemoryFact> {
56
+ return raw
57
+ .split('\n')
58
+ .map((line) => line.trim())
59
+ .filter(
60
+ (line) =>
61
+ line.length > 0 &&
62
+ !line.startsWith('##') &&
63
+ !line.startsWith('Explicit Observations'),
64
+ )
65
+ .map((line, i): MemoryFact => {
66
+ const m = line.match(HONCHO_LINE_RE)
67
+ if (m?.groups?.ts && m.groups.text) {
68
+ return {
69
+ id: `honcho-${m.groups.ts}-${i}`,
70
+ text: m.groups.text,
71
+ source: 'observation',
72
+ createdAt: m.groups.ts,
73
+ }
74
+ }
75
+ return { id: `honcho-${i}`, text: line, source: 'representation' }
76
+ })
77
+ }
78
+
79
+ export function honcho(options: HonchoOptions = {}): MemoryAdapter {
80
+ const assistantId = options.assistantId ?? 'assistant'
81
+
82
+ // Client + entity caches live in this factory's closure — each honcho()
83
+ // instance owns its own.
84
+ type Client = Awaited<ReturnType<typeof loadClient>>
85
+ let clientPromise: Promise<Client> | null = null
86
+ const sessionCache = new Map<string, Promise<Session>>()
87
+ const userPeerCache = new Map<string, Promise<Peer>>()
88
+ let assistantPeerPromise: Promise<Peer> | null = null
89
+
90
+ async function loadClient() {
91
+ const mod = await import('@honcho-ai/sdk')
92
+ return new mod.Honcho({
93
+ baseURL:
94
+ options.baseURL ?? process.env.HONCHO_URL ?? 'http://localhost:8001',
95
+ workspaceId:
96
+ options.workspaceId ?? process.env.HONCHO_APP_NAME ?? 'ai-memory',
97
+ apiKey: options.apiKey ?? process.env.HONCHO_API_KEY ?? 'dev-no-auth',
98
+ })
99
+ }
100
+
101
+ function getClient(): Promise<Client> {
102
+ if (!clientPromise) clientPromise = loadClient()
103
+ return clientPromise
104
+ }
105
+
106
+ function cached<T>(
107
+ cache: Map<string, Promise<T>>,
108
+ key: string,
109
+ create: () => Promise<T>,
110
+ ): Promise<T> {
111
+ const existing = cache.get(key)
112
+ if (existing) return existing
113
+ const created = create().catch((err) => {
114
+ if (cache.get(key) === created) cache.delete(key)
115
+ throw err
116
+ })
117
+ cache.set(key, created)
118
+ return created
119
+ }
120
+
121
+ function getUserPeer(userId: string): Promise<Peer> {
122
+ return cached(userPeerCache, userId, async () =>
123
+ (await getClient()).peer(userId),
124
+ )
125
+ }
126
+ function getAssistantPeer(): Promise<Peer> {
127
+ if (!assistantPeerPromise) {
128
+ assistantPeerPromise = (async () =>
129
+ (await getClient()).peer(assistantId))().catch((err) => {
130
+ assistantPeerPromise = null
131
+ throw err
132
+ })
133
+ }
134
+ return assistantPeerPromise
135
+ }
136
+ function getSession(sessionKey: string): Promise<Session> {
137
+ return cached(sessionCache, sessionKey, async () =>
138
+ (await getClient()).session(sessionKey),
139
+ )
140
+ }
141
+
142
+ /** Honcho session id — tenant-qualified so tenants cannot share sessions. */
143
+ function sessionKeyFor(scope: MemoryScope): string {
144
+ const tenant =
145
+ scope.tenantId != null && scope.tenantId !== '' ? scope.tenantId : '_'
146
+ return `${tenant}__${scope.threadId}`
147
+ }
148
+
149
+ /**
150
+ * Honcho peer id. When `tenantId` is set, prefix the durable user so peers
151
+ * cannot collide across tenants.
152
+ */
153
+ function userIdFor(scope: MemoryScope): string {
154
+ const user = options.user ?? scope.userId ?? 'demo-user'
155
+ if (scope.tenantId != null && scope.tenantId !== '') {
156
+ return `${scope.tenantId}__${user}`
157
+ }
158
+ return user
159
+ }
160
+
161
+ return {
162
+ id: 'honcho',
163
+
164
+ async save(scope, turn: MemoryTurn): Promise<Array<SaveReceipt>> {
165
+ const result = await timed(async () => {
166
+ const [userPeer, assistantPeer, session] = await Promise.all([
167
+ getUserPeer(userIdFor(scope)),
168
+ getAssistantPeer(),
169
+ getSession(sessionKeyFor(scope)),
170
+ ])
171
+ return session.addMessages([
172
+ userPeer.message(turn.user),
173
+ assistantPeer.message(turn.assistant),
174
+ ])
175
+ })
176
+ return [
177
+ {
178
+ ok: result.ok,
179
+ latencyMs: result.latencyMs,
180
+ raw: result.ok ? result.data : undefined,
181
+ error: result.ok ? undefined : result.error,
182
+ },
183
+ ]
184
+ },
185
+
186
+ async recall(scope, query): Promise<RecallResult> {
187
+ const result = await timed(async () => {
188
+ const [userPeer, session] = await Promise.all([
189
+ getUserPeer(userIdFor(scope)),
190
+ getSession(sessionKeyFor(scope)),
191
+ ])
192
+ return userPeer.chat(query, { session })
193
+ })
194
+ if (!result.ok) {
195
+ return { systemPrompt: '', raw: { error: result.error } }
196
+ }
197
+ const text = typeof result.data === 'string' ? result.data : ''
198
+ return { systemPrompt: text, raw: { dialectic: text } }
199
+ },
200
+
201
+ async inspect(scope): Promise<MemorySnapshot> {
202
+ const session = await getSession(sessionKeyFor(scope)).catch(() => null)
203
+ if (!session) {
204
+ return {
205
+ takenAt: new Date().toISOString(),
206
+ data: { error: 'failed to get session' },
207
+ }
208
+ }
209
+ const [messages, summaries] = await Promise.all([
210
+ timed(() => session.messages({ size: 50 })),
211
+ timed(() => session.summaries()),
212
+ ])
213
+ return {
214
+ takenAt: new Date().toISOString(),
215
+ data: {
216
+ messages: messages.ok ? messages.data : { error: messages.error },
217
+ summaries: summaries.ok ? summaries.data : { error: summaries.error },
218
+ },
219
+ }
220
+ },
221
+
222
+ async listFacts(scope): Promise<Array<MemoryFact>> {
223
+ const result = await timed(async () => {
224
+ const userPeer = await getUserPeer(userIdFor(scope))
225
+ return userPeer.representation()
226
+ })
227
+ if (!result.ok) return []
228
+ const raw =
229
+ typeof result.data === 'string'
230
+ ? result.data
231
+ : String(
232
+ (result.data as { representation?: unknown }).representation ??
233
+ '',
234
+ )
235
+ return parseHonchoRepresentation(raw)
236
+ },
237
+ }
238
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ inspectRecords,
3
+ isExpired,
4
+ listRecordFacts,
5
+ recallRecords,
6
+ sameScope,
7
+ saveTurn,
8
+ } from '../../internal/store'
9
+ import type {
10
+ BuiltinOptions,
11
+ MemoryRecord,
12
+ RecordStore,
13
+ } from '../../internal/store'
14
+ import type { MemoryAdapter, MemoryScope } from '../../types'
15
+
16
+ /**
17
+ * Options for {@link inMemory}. Retrieval/extraction knobs that used to live on
18
+ * the middleware are adapter options here.
19
+ */
20
+ export interface InMemoryOptions extends BuiltinOptions {}
21
+
22
+ /**
23
+ * Zero-dependency memory adapter backed by a `Map`. Records vanish on process
24
+ * restart, so this is for local development, tests, and single-process demos —
25
+ * not multi-process production (each worker gets its own Map). For production,
26
+ * use {@link redis} from `@tanstack/ai-memory/redis`.
27
+ *
28
+ * By default `save` stores the raw user/assistant turn and `recall` scores it
29
+ * lexically + by recency. Pass an `embedder` for semantic scoring and/or an
30
+ * `extract` function to persist derived facts.
31
+ */
32
+ export function inMemory(options: InMemoryOptions = {}): MemoryAdapter {
33
+ const records = new Map<string, MemoryRecord>()
34
+
35
+ function sweep(): Array<MemoryRecord> {
36
+ const now = Date.now()
37
+ const live: Array<MemoryRecord> = []
38
+ for (const r of records.values()) {
39
+ if (isExpired(r, now)) records.delete(r.id)
40
+ else live.push(r)
41
+ }
42
+ return live
43
+ }
44
+
45
+ const store: RecordStore = {
46
+ async add(batch) {
47
+ const now = Date.now()
48
+ for (const r of batch) records.set(r.id, { ...r, updatedAt: now })
49
+ sweep()
50
+ },
51
+ async loadScope(scope: MemoryScope) {
52
+ return sweep().filter((r) => sameScope(r.scope, scope))
53
+ },
54
+ }
55
+
56
+ return {
57
+ id: 'in-memory',
58
+ recall: (scope, query) => recallRecords(store, scope, query, options),
59
+ save: (scope, turn) => saveTurn(store, scope, turn, options),
60
+ inspect: (scope) => inspectRecords(store, scope),
61
+ listFacts: (scope) => listRecordFacts(store, scope),
62
+ }
63
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * mem0 memory adapter — talks to a mem0 server over plain HTTP (no SDK, so no
3
+ * peer dependency). mem0 owns extraction and ranking server-side; this adapter
4
+ * maps the `recall`/`save` contract onto its `/memories` and `/search` endpoints.
5
+ *
6
+ * Requires a running mem0 server. Point it at one via `baseUrl` (or the
7
+ * `MEM0_URL` env var); pass `apiKey` (or `MEM0_ADMIN_API_KEY`) when it's secured.
8
+ */
9
+
10
+ import type {
11
+ MemoryAdapter,
12
+ MemoryFact,
13
+ MemoryFragment,
14
+ MemoryScope,
15
+ MemorySnapshot,
16
+ MemoryTurn,
17
+ RecallResult,
18
+ SaveReceipt,
19
+ } from '../../types'
20
+
21
+ export interface Mem0Options {
22
+ /** Durable user id. Falls back to `scope.userId`, then `'demo-user'`. */
23
+ user?: string
24
+ /** mem0 server URL. Defaults to `MEM0_URL` or `http://localhost:8000`. */
25
+ baseUrl?: string
26
+ /** Bearer token. Defaults to `MEM0_ADMIN_API_KEY`. */
27
+ apiKey?: string
28
+ /** Ask mem0 to rerank search results. Defaults to `true`. */
29
+ rerank?: boolean
30
+ /** Minimum search score. Defaults to `0.1`. */
31
+ threshold?: number
32
+ }
33
+
34
+ type JsonResult =
35
+ | { ok: true; latencyMs: number; data: unknown }
36
+ | { ok: false; latencyMs: number; error: string }
37
+
38
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
39
+ return value && typeof value === 'object'
40
+ ? (value as Record<string, unknown>)
41
+ : undefined
42
+ }
43
+
44
+ function asString(value: unknown): string | undefined {
45
+ return typeof value === 'string' ? value : undefined
46
+ }
47
+
48
+ /** Pull the array of items out of a mem0 response (`{results: []}` or a bare array). */
49
+ function itemsOf(data: unknown): Array<Record<string, unknown>> {
50
+ const rec = asRecord(data)
51
+ const candidate = rec && 'results' in rec ? rec.results : data
52
+ if (!Array.isArray(candidate)) return []
53
+ return candidate.filter(
54
+ (m): m is Record<string, unknown> => !!m && typeof m === 'object',
55
+ )
56
+ }
57
+
58
+ export function mem0(options: Mem0Options = {}): MemoryAdapter {
59
+ const baseUrl =
60
+ options.baseUrl ?? process.env.MEM0_URL ?? 'http://localhost:8000'
61
+ const apiKey = options.apiKey ?? process.env.MEM0_ADMIN_API_KEY ?? ''
62
+ const rerank = options.rerank ?? true
63
+ const threshold = options.threshold ?? 0.1
64
+
65
+ function headers(): Record<string, string> {
66
+ const h: Record<string, string> = { 'Content-Type': 'application/json' }
67
+ if (apiKey) h.Authorization = `Bearer ${apiKey}`
68
+ return h
69
+ }
70
+
71
+ function userId(scope: MemoryScope): string {
72
+ return options.user ?? scope.userId ?? 'demo-user'
73
+ }
74
+
75
+ /**
76
+ * mem0 `run_id` — conversation/run isolation. Maps 1:1 to `scope.threadId` so
77
+ * same-user memories do not leak across threads.
78
+ */
79
+ function runId(scope: MemoryScope): string {
80
+ return scope.threadId
81
+ }
82
+
83
+ async function safeJson(fn: () => Promise<Response>): Promise<JsonResult> {
84
+ const start = Date.now()
85
+ try {
86
+ const res = await fn()
87
+ const latencyMs = Date.now() - start
88
+ if (!res.ok) {
89
+ const text = await res.text().catch(() => '')
90
+ return {
91
+ ok: false,
92
+ latencyMs,
93
+ error: `HTTP ${res.status}: ${text.slice(0, 300)}`,
94
+ }
95
+ }
96
+ const data = await res.json().catch(() => null)
97
+ return { ok: true, latencyMs, data }
98
+ } catch (err) {
99
+ return {
100
+ ok: false,
101
+ latencyMs: Date.now() - start,
102
+ error: err instanceof Error ? err.message : String(err),
103
+ }
104
+ }
105
+ }
106
+
107
+ async function loadMemories(scope: MemoryScope): Promise<JsonResult> {
108
+ const params = new URLSearchParams({
109
+ user_id: userId(scope),
110
+ run_id: runId(scope),
111
+ })
112
+ const url = `${baseUrl}/memories?${params.toString()}`
113
+ return safeJson(() => fetch(url, { method: 'GET', headers: headers() }))
114
+ }
115
+
116
+ return {
117
+ id: 'mem0',
118
+
119
+ async save(scope, turn: MemoryTurn): Promise<Array<SaveReceipt>> {
120
+ const result = await safeJson(() =>
121
+ fetch(`${baseUrl}/memories`, {
122
+ method: 'POST',
123
+ headers: headers(),
124
+ body: JSON.stringify({
125
+ messages: [
126
+ { role: 'user', content: turn.user },
127
+ { role: 'assistant', content: turn.assistant },
128
+ ],
129
+ user_id: userId(scope),
130
+ run_id: runId(scope),
131
+ }),
132
+ }),
133
+ )
134
+ return [
135
+ {
136
+ ok: result.ok,
137
+ latencyMs: result.latencyMs,
138
+ raw: result.ok ? result.data : undefined,
139
+ error: result.ok ? undefined : result.error,
140
+ },
141
+ ]
142
+ },
143
+
144
+ async recall(scope, query): Promise<RecallResult> {
145
+ const result = await safeJson(() =>
146
+ fetch(`${baseUrl}/search`, {
147
+ method: 'POST',
148
+ headers: headers(),
149
+ body: JSON.stringify({
150
+ query,
151
+ user_id: userId(scope),
152
+ run_id: runId(scope),
153
+ rerank,
154
+ threshold,
155
+ }),
156
+ }),
157
+ )
158
+ if (!result.ok) {
159
+ return { systemPrompt: '', fragments: [], raw: { error: result.error } }
160
+ }
161
+ const fragments: Array<MemoryFragment> = itemsOf(result.data).map(
162
+ (m) => ({
163
+ text: asString(m.memory) ?? asString(m.text) ?? JSON.stringify(m),
164
+ source: asString(m.id) ?? 'mem0',
165
+ }),
166
+ )
167
+ const systemPrompt =
168
+ fragments.length === 0
169
+ ? ''
170
+ : `Recalled memory:\n${fragments.map((f) => `- (${f.source}) ${f.text}`).join('\n')}`
171
+ return { systemPrompt, fragments, raw: result.data }
172
+ },
173
+
174
+ async inspect(scope): Promise<MemorySnapshot> {
175
+ const result = await loadMemories(scope)
176
+ return {
177
+ takenAt: new Date().toISOString(),
178
+ data: result.ok ? result.data : { error: result.error },
179
+ }
180
+ },
181
+
182
+ async listFacts(scope): Promise<Array<MemoryFact>> {
183
+ const result = await loadMemories(scope)
184
+ if (!result.ok) return []
185
+ return itemsOf(result.data)
186
+ .map((m): MemoryFact | null => {
187
+ const text = asString(m.memory)
188
+ if (!text) return null
189
+ return {
190
+ id: asString(m.id) ?? 'mem0',
191
+ text,
192
+ source: 'memory',
193
+ createdAt: asString(m.updated_at) ?? asString(m.created_at),
194
+ }
195
+ })
196
+ .filter((f): f is MemoryFact => f !== null)
197
+ },
198
+ }
199
+ }