@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,46 @@
1
+ import { inspectRecords, isExpired, listRecordFacts, recallRecords, sameScope, saveTurn } from "../../internal/store.js";
2
+ //#region src/providers/in-memory/index.ts
3
+ /**
4
+ * Zero-dependency memory adapter backed by a `Map`. Records vanish on process
5
+ * restart, so this is for local development, tests, and single-process demos —
6
+ * not multi-process production (each worker gets its own Map). For production,
7
+ * use {@link redis} from `@tanstack/ai-memory/redis`.
8
+ *
9
+ * By default `save` stores the raw user/assistant turn and `recall` scores it
10
+ * lexically + by recency. Pass an `embedder` for semantic scoring and/or an
11
+ * `extract` function to persist derived facts.
12
+ */
13
+ function inMemory(options = {}) {
14
+ const records = /* @__PURE__ */ new Map();
15
+ function sweep() {
16
+ const now = Date.now();
17
+ const live = [];
18
+ for (const r of records.values()) if (isExpired(r, now)) records.delete(r.id);
19
+ else live.push(r);
20
+ return live;
21
+ }
22
+ const store = {
23
+ async add(batch) {
24
+ const now = Date.now();
25
+ for (const r of batch) records.set(r.id, {
26
+ ...r,
27
+ updatedAt: now
28
+ });
29
+ sweep();
30
+ },
31
+ async loadScope(scope) {
32
+ return sweep().filter((r) => sameScope(r.scope, scope));
33
+ }
34
+ };
35
+ return {
36
+ id: "in-memory",
37
+ recall: (scope, query) => recallRecords(store, scope, query, options),
38
+ save: (scope, turn) => saveTurn(store, scope, turn, options),
39
+ inspect: (scope) => inspectRecords(store, scope),
40
+ listFacts: (scope) => listRecordFacts(store, scope)
41
+ };
42
+ }
43
+ //#endregion
44
+ export { inMemory };
45
+
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/providers/in-memory/index.ts"],"sourcesContent":["import {\n inspectRecords,\n isExpired,\n listRecordFacts,\n recallRecords,\n sameScope,\n saveTurn,\n} from '../../internal/store'\nimport type {\n BuiltinOptions,\n MemoryRecord,\n RecordStore,\n} from '../../internal/store'\nimport type { MemoryAdapter, MemoryScope } from '../../types'\n\n/**\n * Options for {@link inMemory}. Retrieval/extraction knobs that used to live on\n * the middleware are adapter options here.\n */\nexport interface InMemoryOptions extends BuiltinOptions {}\n\n/**\n * Zero-dependency memory adapter backed by a `Map`. Records vanish on process\n * restart, so this is for local development, tests, and single-process demos —\n * not multi-process production (each worker gets its own Map). For production,\n * use {@link redis} from `@tanstack/ai-memory/redis`.\n *\n * By default `save` stores the raw user/assistant turn and `recall` scores it\n * lexically + by recency. Pass an `embedder` for semantic scoring and/or an\n * `extract` function to persist derived facts.\n */\nexport function inMemory(options: InMemoryOptions = {}): MemoryAdapter {\n const records = new Map<string, MemoryRecord>()\n\n function sweep(): Array<MemoryRecord> {\n const now = Date.now()\n const live: Array<MemoryRecord> = []\n for (const r of records.values()) {\n if (isExpired(r, now)) records.delete(r.id)\n else live.push(r)\n }\n return live\n }\n\n const store: RecordStore = {\n async add(batch) {\n const now = Date.now()\n for (const r of batch) records.set(r.id, { ...r, updatedAt: now })\n sweep()\n },\n async loadScope(scope: MemoryScope) {\n return sweep().filter((r) => sameScope(r.scope, scope))\n },\n }\n\n return {\n id: 'in-memory',\n recall: (scope, query) => recallRecords(store, scope, query, options),\n save: (scope, turn) => saveTurn(store, scope, turn, options),\n inspect: (scope) => inspectRecords(store, scope),\n listFacts: (scope) => listRecordFacts(store, scope),\n }\n}\n"],"mappings":";;;;;;;;;;;;AA+BA,SAAgB,SAAS,UAA2B,CAAC,GAAkB;CACrE,MAAM,0BAAU,IAAI,IAA0B;CAE9C,SAAS,QAA6B;EACpC,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,OAA4B,CAAC;EACnC,KAAK,MAAM,KAAK,QAAQ,OAAO,GAC7B,IAAI,UAAU,GAAG,GAAG,GAAG,QAAQ,OAAO,EAAE,EAAE;OACrC,KAAK,KAAK,CAAC;EAElB,OAAO;CACT;CAEA,MAAM,QAAqB;EACzB,MAAM,IAAI,OAAO;GACf,MAAM,MAAM,KAAK,IAAI;GACrB,KAAK,MAAM,KAAK,OAAO,QAAQ,IAAI,EAAE,IAAI;IAAE,GAAG;IAAG,WAAW;GAAI,CAAC;GACjE,MAAM;EACR;EACA,MAAM,UAAU,OAAoB;GAClC,OAAO,MAAM,CAAC,CAAC,QAAQ,MAAM,UAAU,EAAE,OAAO,KAAK,CAAC;EACxD;CACF;CAEA,OAAO;EACL,IAAI;EACJ,SAAS,OAAO,UAAU,cAAc,OAAO,OAAO,OAAO,OAAO;EACpE,OAAO,OAAO,SAAS,SAAS,OAAO,OAAO,MAAM,OAAO;EAC3D,UAAU,UAAU,eAAe,OAAO,KAAK;EAC/C,YAAY,UAAU,gBAAgB,OAAO,KAAK;CACpD;AACF"}
@@ -0,0 +1,14 @@
1
+ import { MemoryAdapter } from '../../types.js';
2
+ export interface Mem0Options {
3
+ /** Durable user id. Falls back to `scope.userId`, then `'demo-user'`. */
4
+ user?: string;
5
+ /** mem0 server URL. Defaults to `MEM0_URL` or `http://localhost:8000`. */
6
+ baseUrl?: string;
7
+ /** Bearer token. Defaults to `MEM0_ADMIN_API_KEY`. */
8
+ apiKey?: string;
9
+ /** Ask mem0 to rerank search results. Defaults to `true`. */
10
+ rerank?: boolean;
11
+ /** Minimum search score. Defaults to `0.1`. */
12
+ threshold?: number;
13
+ }
14
+ export declare function mem0(options?: Mem0Options): MemoryAdapter;
@@ -0,0 +1,150 @@
1
+ //#region src/providers/mem0/index.ts
2
+ function asRecord(value) {
3
+ return value && typeof value === "object" ? value : void 0;
4
+ }
5
+ function asString(value) {
6
+ return typeof value === "string" ? value : void 0;
7
+ }
8
+ /** Pull the array of items out of a mem0 response (`{results: []}` or a bare array). */
9
+ function itemsOf(data) {
10
+ const rec = asRecord(data);
11
+ const candidate = rec && "results" in rec ? rec.results : data;
12
+ if (!Array.isArray(candidate)) return [];
13
+ return candidate.filter((m) => !!m && typeof m === "object");
14
+ }
15
+ function mem0(options = {}) {
16
+ const baseUrl = options.baseUrl ?? process.env.MEM0_URL ?? "http://localhost:8000";
17
+ const apiKey = options.apiKey ?? process.env.MEM0_ADMIN_API_KEY ?? "";
18
+ const rerank = options.rerank ?? true;
19
+ const threshold = options.threshold ?? .1;
20
+ function headers() {
21
+ const h = { "Content-Type": "application/json" };
22
+ if (apiKey) h.Authorization = `Bearer ${apiKey}`;
23
+ return h;
24
+ }
25
+ function userId(scope) {
26
+ return options.user ?? scope.userId ?? "demo-user";
27
+ }
28
+ /**
29
+ * mem0 `run_id` — conversation/run isolation. Maps 1:1 to `scope.threadId` so
30
+ * same-user memories do not leak across threads.
31
+ */
32
+ function runId(scope) {
33
+ return scope.threadId;
34
+ }
35
+ async function safeJson(fn) {
36
+ const start = Date.now();
37
+ try {
38
+ const res = await fn();
39
+ const latencyMs = Date.now() - start;
40
+ if (!res.ok) {
41
+ const text = await res.text().catch(() => "");
42
+ return {
43
+ ok: false,
44
+ latencyMs,
45
+ error: `HTTP ${res.status}: ${text.slice(0, 300)}`
46
+ };
47
+ }
48
+ return {
49
+ ok: true,
50
+ latencyMs,
51
+ data: await res.json().catch(() => null)
52
+ };
53
+ } catch (err) {
54
+ return {
55
+ ok: false,
56
+ latencyMs: Date.now() - start,
57
+ error: err instanceof Error ? err.message : String(err)
58
+ };
59
+ }
60
+ }
61
+ async function loadMemories(scope) {
62
+ const params = new URLSearchParams({
63
+ user_id: userId(scope),
64
+ run_id: runId(scope)
65
+ });
66
+ const url = `${baseUrl}/memories?${params.toString()}`;
67
+ return safeJson(() => fetch(url, {
68
+ method: "GET",
69
+ headers: headers()
70
+ }));
71
+ }
72
+ return {
73
+ id: "mem0",
74
+ async save(scope, turn) {
75
+ const result = await safeJson(() => fetch(`${baseUrl}/memories`, {
76
+ method: "POST",
77
+ headers: headers(),
78
+ body: JSON.stringify({
79
+ messages: [{
80
+ role: "user",
81
+ content: turn.user
82
+ }, {
83
+ role: "assistant",
84
+ content: turn.assistant
85
+ }],
86
+ user_id: userId(scope),
87
+ run_id: runId(scope)
88
+ })
89
+ }));
90
+ return [{
91
+ ok: result.ok,
92
+ latencyMs: result.latencyMs,
93
+ raw: result.ok ? result.data : void 0,
94
+ error: result.ok ? void 0 : result.error
95
+ }];
96
+ },
97
+ async recall(scope, query) {
98
+ const result = await safeJson(() => fetch(`${baseUrl}/search`, {
99
+ method: "POST",
100
+ headers: headers(),
101
+ body: JSON.stringify({
102
+ query,
103
+ user_id: userId(scope),
104
+ run_id: runId(scope),
105
+ rerank,
106
+ threshold
107
+ })
108
+ }));
109
+ if (!result.ok) return {
110
+ systemPrompt: "",
111
+ fragments: [],
112
+ raw: { error: result.error }
113
+ };
114
+ const fragments = itemsOf(result.data).map((m) => ({
115
+ text: asString(m.memory) ?? asString(m.text) ?? JSON.stringify(m),
116
+ source: asString(m.id) ?? "mem0"
117
+ }));
118
+ return {
119
+ systemPrompt: fragments.length === 0 ? "" : `Recalled memory:\n${fragments.map((f) => `- (${f.source}) ${f.text}`).join("\n")}`,
120
+ fragments,
121
+ raw: result.data
122
+ };
123
+ },
124
+ async inspect(scope) {
125
+ const result = await loadMemories(scope);
126
+ return {
127
+ takenAt: (/* @__PURE__ */ new Date()).toISOString(),
128
+ data: result.ok ? result.data : { error: result.error }
129
+ };
130
+ },
131
+ async listFacts(scope) {
132
+ const result = await loadMemories(scope);
133
+ if (!result.ok) return [];
134
+ return itemsOf(result.data).map((m) => {
135
+ const text = asString(m.memory);
136
+ if (!text) return null;
137
+ return {
138
+ id: asString(m.id) ?? "mem0",
139
+ text,
140
+ source: "memory",
141
+ createdAt: asString(m.updated_at) ?? asString(m.created_at)
142
+ };
143
+ }).filter((f) => f !== null);
144
+ }
145
+ };
146
+ }
147
+ //#endregion
148
+ export { mem0 };
149
+
150
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/providers/mem0/index.ts"],"sourcesContent":["/**\n * mem0 memory adapter — talks to a mem0 server over plain HTTP (no SDK, so no\n * peer dependency). mem0 owns extraction and ranking server-side; this adapter\n * maps the `recall`/`save` contract onto its `/memories` and `/search` endpoints.\n *\n * Requires a running mem0 server. Point it at one via `baseUrl` (or the\n * `MEM0_URL` env var); pass `apiKey` (or `MEM0_ADMIN_API_KEY`) when it's secured.\n */\n\nimport type {\n MemoryAdapter,\n MemoryFact,\n MemoryFragment,\n MemoryScope,\n MemorySnapshot,\n MemoryTurn,\n RecallResult,\n SaveReceipt,\n} from '../../types'\n\nexport interface Mem0Options {\n /** Durable user id. Falls back to `scope.userId`, then `'demo-user'`. */\n user?: string\n /** mem0 server URL. Defaults to `MEM0_URL` or `http://localhost:8000`. */\n baseUrl?: string\n /** Bearer token. Defaults to `MEM0_ADMIN_API_KEY`. */\n apiKey?: string\n /** Ask mem0 to rerank search results. Defaults to `true`. */\n rerank?: boolean\n /** Minimum search score. Defaults to `0.1`. */\n threshold?: number\n}\n\ntype JsonResult =\n | { ok: true; latencyMs: number; data: unknown }\n | { ok: false; latencyMs: number; error: string }\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object'\n ? (value as Record<string, unknown>)\n : undefined\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined\n}\n\n/** Pull the array of items out of a mem0 response (`{results: []}` or a bare array). */\nfunction itemsOf(data: unknown): Array<Record<string, unknown>> {\n const rec = asRecord(data)\n const candidate = rec && 'results' in rec ? rec.results : data\n if (!Array.isArray(candidate)) return []\n return candidate.filter(\n (m): m is Record<string, unknown> => !!m && typeof m === 'object',\n )\n}\n\nexport function mem0(options: Mem0Options = {}): MemoryAdapter {\n const baseUrl =\n options.baseUrl ?? process.env.MEM0_URL ?? 'http://localhost:8000'\n const apiKey = options.apiKey ?? process.env.MEM0_ADMIN_API_KEY ?? ''\n const rerank = options.rerank ?? true\n const threshold = options.threshold ?? 0.1\n\n function headers(): Record<string, string> {\n const h: Record<string, string> = { 'Content-Type': 'application/json' }\n if (apiKey) h.Authorization = `Bearer ${apiKey}`\n return h\n }\n\n function userId(scope: MemoryScope): string {\n return options.user ?? scope.userId ?? 'demo-user'\n }\n\n /**\n * mem0 `run_id` — conversation/run isolation. Maps 1:1 to `scope.threadId` so\n * same-user memories do not leak across threads.\n */\n function runId(scope: MemoryScope): string {\n return scope.threadId\n }\n\n async function safeJson(fn: () => Promise<Response>): Promise<JsonResult> {\n const start = Date.now()\n try {\n const res = await fn()\n const latencyMs = Date.now() - start\n if (!res.ok) {\n const text = await res.text().catch(() => '')\n return {\n ok: false,\n latencyMs,\n error: `HTTP ${res.status}: ${text.slice(0, 300)}`,\n }\n }\n const data = await res.json().catch(() => null)\n return { ok: true, latencyMs, data }\n } catch (err) {\n return {\n ok: false,\n latencyMs: Date.now() - start,\n error: err instanceof Error ? err.message : String(err),\n }\n }\n }\n\n async function loadMemories(scope: MemoryScope): Promise<JsonResult> {\n const params = new URLSearchParams({\n user_id: userId(scope),\n run_id: runId(scope),\n })\n const url = `${baseUrl}/memories?${params.toString()}`\n return safeJson(() => fetch(url, { method: 'GET', headers: headers() }))\n }\n\n return {\n id: 'mem0',\n\n async save(scope, turn: MemoryTurn): Promise<Array<SaveReceipt>> {\n const result = await safeJson(() =>\n fetch(`${baseUrl}/memories`, {\n method: 'POST',\n headers: headers(),\n body: JSON.stringify({\n messages: [\n { role: 'user', content: turn.user },\n { role: 'assistant', content: turn.assistant },\n ],\n user_id: userId(scope),\n run_id: runId(scope),\n }),\n }),\n )\n return [\n {\n ok: result.ok,\n latencyMs: result.latencyMs,\n raw: result.ok ? result.data : undefined,\n error: result.ok ? undefined : result.error,\n },\n ]\n },\n\n async recall(scope, query): Promise<RecallResult> {\n const result = await safeJson(() =>\n fetch(`${baseUrl}/search`, {\n method: 'POST',\n headers: headers(),\n body: JSON.stringify({\n query,\n user_id: userId(scope),\n run_id: runId(scope),\n rerank,\n threshold,\n }),\n }),\n )\n if (!result.ok) {\n return { systemPrompt: '', fragments: [], raw: { error: result.error } }\n }\n const fragments: Array<MemoryFragment> = itemsOf(result.data).map(\n (m) => ({\n text: asString(m.memory) ?? asString(m.text) ?? JSON.stringify(m),\n source: asString(m.id) ?? 'mem0',\n }),\n )\n const systemPrompt =\n fragments.length === 0\n ? ''\n : `Recalled memory:\\n${fragments.map((f) => `- (${f.source}) ${f.text}`).join('\\n')}`\n return { systemPrompt, fragments, raw: result.data }\n },\n\n async inspect(scope): Promise<MemorySnapshot> {\n const result = await loadMemories(scope)\n return {\n takenAt: new Date().toISOString(),\n data: result.ok ? result.data : { error: result.error },\n }\n },\n\n async listFacts(scope): Promise<Array<MemoryFact>> {\n const result = await loadMemories(scope)\n if (!result.ok) return []\n return itemsOf(result.data)\n .map((m): MemoryFact | null => {\n const text = asString(m.memory)\n if (!text) return null\n return {\n id: asString(m.id) ?? 'mem0',\n text,\n source: 'memory',\n createdAt: asString(m.updated_at) ?? asString(m.created_at),\n }\n })\n .filter((f): f is MemoryFact => f !== null)\n },\n }\n}\n"],"mappings":";AAqCA,SAAS,SAAS,OAAqD;CACrE,OAAO,SAAS,OAAO,UAAU,WAC5B,QACD,KAAA;AACN;AAEA,SAAS,SAAS,OAAoC;CACpD,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;;AAGA,SAAS,QAAQ,MAA+C;CAC9D,MAAM,MAAM,SAAS,IAAI;CACzB,MAAM,YAAY,OAAO,aAAa,MAAM,IAAI,UAAU;CAC1D,IAAI,CAAC,MAAM,QAAQ,SAAS,GAAG,OAAO,CAAC;CACvC,OAAO,UAAU,QACd,MAAoC,CAAC,CAAC,KAAK,OAAO,MAAM,QAC3D;AACF;AAEA,SAAgB,KAAK,UAAuB,CAAC,GAAkB;CAC7D,MAAM,UACJ,QAAQ,WAAW,QAAQ,IAAI,YAAY;CAC7C,MAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,sBAAsB;CACnE,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,YAAY,QAAQ,aAAa;CAEvC,SAAS,UAAkC;EACzC,MAAM,IAA4B,EAAE,gBAAgB,mBAAmB;EACvE,IAAI,QAAQ,EAAE,gBAAgB,UAAU;EACxC,OAAO;CACT;CAEA,SAAS,OAAO,OAA4B;EAC1C,OAAO,QAAQ,QAAQ,MAAM,UAAU;CACzC;;;;;CAMA,SAAS,MAAM,OAA4B;EACzC,OAAO,MAAM;CACf;CAEA,eAAe,SAAS,IAAkD;EACxE,MAAM,QAAQ,KAAK,IAAI;EACvB,IAAI;GACF,MAAM,MAAM,MAAM,GAAG;GACrB,MAAM,YAAY,KAAK,IAAI,IAAI;GAC/B,IAAI,CAAC,IAAI,IAAI;IACX,MAAM,OAAO,MAAM,IAAI,KAAK,CAAC,CAAC,YAAY,EAAE;IAC5C,OAAO;KACL,IAAI;KACJ;KACA,OAAO,QAAQ,IAAI,OAAO,IAAI,KAAK,MAAM,GAAG,GAAG;IACjD;GACF;GAEA,OAAO;IAAE,IAAI;IAAM;IAAW,MAAA,MADX,IAAI,KAAK,CAAC,CAAC,YAAY,IAAI;GACX;EACrC,SAAS,KAAK;GACZ,OAAO;IACL,IAAI;IACJ,WAAW,KAAK,IAAI,IAAI;IACxB,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GACxD;EACF;CACF;CAEA,eAAe,aAAa,OAAyC;EACnE,MAAM,SAAS,IAAI,gBAAgB;GACjC,SAAS,OAAO,KAAK;GACrB,QAAQ,MAAM,KAAK;EACrB,CAAC;EACD,MAAM,MAAM,GAAG,QAAQ,YAAY,OAAO,SAAS;EACnD,OAAO,eAAe,MAAM,KAAK;GAAE,QAAQ;GAAO,SAAS,QAAQ;EAAE,CAAC,CAAC;CACzE;CAEA,OAAO;EACL,IAAI;EAEJ,MAAM,KAAK,OAAO,MAA+C;GAC/D,MAAM,SAAS,MAAM,eACnB,MAAM,GAAG,QAAQ,YAAY;IAC3B,QAAQ;IACR,SAAS,QAAQ;IACjB,MAAM,KAAK,UAAU;KACnB,UAAU,CACR;MAAE,MAAM;MAAQ,SAAS,KAAK;KAAK,GACnC;MAAE,MAAM;MAAa,SAAS,KAAK;KAAU,CAC/C;KACA,SAAS,OAAO,KAAK;KACrB,QAAQ,MAAM,KAAK;IACrB,CAAC;GACH,CAAC,CACH;GACA,OAAO,CACL;IACE,IAAI,OAAO;IACX,WAAW,OAAO;IAClB,KAAK,OAAO,KAAK,OAAO,OAAO,KAAA;IAC/B,OAAO,OAAO,KAAK,KAAA,IAAY,OAAO;GACxC,CACF;EACF;EAEA,MAAM,OAAO,OAAO,OAA8B;GAChD,MAAM,SAAS,MAAM,eACnB,MAAM,GAAG,QAAQ,UAAU;IACzB,QAAQ;IACR,SAAS,QAAQ;IACjB,MAAM,KAAK,UAAU;KACnB;KACA,SAAS,OAAO,KAAK;KACrB,QAAQ,MAAM,KAAK;KACnB;KACA;IACF,CAAC;GACH,CAAC,CACH;GACA,IAAI,CAAC,OAAO,IACV,OAAO;IAAE,cAAc;IAAI,WAAW,CAAC;IAAG,KAAK,EAAE,OAAO,OAAO,MAAM;GAAE;GAEzE,MAAM,YAAmC,QAAQ,OAAO,IAAI,CAAC,CAAC,KAC3D,OAAO;IACN,MAAM,SAAS,EAAE,MAAM,KAAK,SAAS,EAAE,IAAI,KAAK,KAAK,UAAU,CAAC;IAChE,QAAQ,SAAS,EAAE,EAAE,KAAK;GAC5B,EACF;GAKA,OAAO;IAAE,cAHP,UAAU,WAAW,IACjB,KACA,qBAAqB,UAAU,KAAK,MAAM,MAAM,EAAE,OAAO,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,IAAI;IAC/D;IAAW,KAAK,OAAO;GAAK;EACrD;EAEA,MAAM,QAAQ,OAAgC;GAC5C,MAAM,SAAS,MAAM,aAAa,KAAK;GACvC,OAAO;IACL,0BAAS,IAAI,KAAK,EAAA,CAAE,YAAY;IAChC,MAAM,OAAO,KAAK,OAAO,OAAO,EAAE,OAAO,OAAO,MAAM;GACxD;EACF;EAEA,MAAM,UAAU,OAAmC;GACjD,MAAM,SAAS,MAAM,aAAa,KAAK;GACvC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC;GACxB,OAAO,QAAQ,OAAO,IAAI,CAAC,CACxB,KAAK,MAAyB;IAC7B,MAAM,OAAO,SAAS,EAAE,MAAM;IAC9B,IAAI,CAAC,MAAM,OAAO;IAClB,OAAO;KACL,IAAI,SAAS,EAAE,EAAE,KAAK;KACtB;KACA,QAAQ;KACR,WAAW,SAAS,EAAE,UAAU,KAAK,SAAS,EAAE,UAAU;IAC5D;GACF,CAAC,CAAC,CACD,QAAQ,MAAuB,MAAM,IAAI;EAC9C;CACF;AACF"}
@@ -0,0 +1,54 @@
1
+ import { BuiltinOptions } from '../../internal/store.js';
2
+ import { MemoryAdapter } from '../../types.js';
3
+ /**
4
+ * Minimal subset of the Redis client API the adapter uses. Shaped to match
5
+ * `ioredis` directly (lowercase method names). For node-redis v4+'s camelCase
6
+ * API, wrap the client with {@link fromNodeRedis}.
7
+ */
8
+ export interface RedisLike {
9
+ set: (key: string, value: string) => Promise<unknown>;
10
+ get: (key: string) => Promise<string | null>;
11
+ del: (...keys: Array<string>) => Promise<unknown>;
12
+ sadd: (key: string, ...members: Array<string>) => Promise<unknown>;
13
+ srem: (key: string, ...members: Array<string>) => Promise<unknown>;
14
+ smembers: (key: string) => Promise<Array<string>>;
15
+ mget: (...keys: Array<string>) => Promise<Array<string | null>>;
16
+ }
17
+ /** node-redis v4+ default-mode (camelCase) surface used by {@link fromNodeRedis}. */
18
+ export interface NodeRedisLike {
19
+ get: (key: string) => Promise<string | null>;
20
+ set: (key: string, value: string) => Promise<unknown>;
21
+ del: (keys: Array<string> | string) => Promise<number>;
22
+ sAdd: (key: string, members: string | Array<string>) => Promise<number>;
23
+ sRem: (key: string, members: string | Array<string>) => Promise<number>;
24
+ sMembers: (key: string) => Promise<Array<string>>;
25
+ mGet: (keys: Array<string>) => Promise<Array<string | null>>;
26
+ }
27
+ /**
28
+ * Wrap a node-redis v4+ default-mode client (camelCase API) into the lowercase
29
+ * {@link RedisLike} shape this adapter expects. For `ioredis`, no wrapper is
30
+ * needed — pass the client directly.
31
+ */
32
+ export declare function fromNodeRedis(client: NodeRedisLike): RedisLike;
33
+ export interface RedisOptions extends BuiltinOptions {
34
+ /** A Redis client implementing {@link RedisLike} (ioredis, or wrapped node-redis). */
35
+ redis: RedisLike;
36
+ /** Key prefix. Defaults to `'tanstack-ai:memory'`. */
37
+ prefix?: string;
38
+ }
39
+ /**
40
+ * Production memory adapter backed by plain Redis (no vector index required).
41
+ * Ranks client-side (lexical + optional cosine + recency + importance), so it's
42
+ * suited to up to ~10k records per scope. Bring your own client (`ioredis`, or
43
+ * node-redis wrapped with {@link fromNodeRedis}).
44
+ *
45
+ * Storage model:
46
+ * ```text
47
+ * {prefix}:record:{id} -> JSON MemoryRecord
48
+ * {prefix}:index:{tenantId or _}:{userId or _}:{threadId} -> Set<id>
49
+ * ```
50
+ * Segments are escaped (so `:`, `\\`, `_` in values cannot collide). Missing
51
+ * optional dims become `_` (omit ≠ match any — same exact-match model as the
52
+ * built-in `sameScope` helper). No dual-read of older index layouts.
53
+ */
54
+ export declare function redis(options: RedisOptions): MemoryAdapter;
@@ -0,0 +1,118 @@
1
+ import { inspectRecords, isExpired, listRecordFacts, recallRecords, saveTurn } from "../../internal/store.js";
2
+ //#region src/providers/redis/index.ts
3
+ /**
4
+ * Wrap a node-redis v4+ default-mode client (camelCase API) into the lowercase
5
+ * {@link RedisLike} shape this adapter expects. For `ioredis`, no wrapper is
6
+ * needed — pass the client directly.
7
+ */
8
+ function fromNodeRedis(client) {
9
+ return {
10
+ get: (key) => client.get(key),
11
+ set: (key, value) => client.set(key, value),
12
+ del: (...keys) => client.del(keys),
13
+ sadd: (key, ...members) => client.sAdd(key, members),
14
+ srem: (key, ...members) => client.sRem(key, members),
15
+ smembers: (key) => client.sMembers(key),
16
+ mget: (...keys) => client.mGet(keys)
17
+ };
18
+ }
19
+ /**
20
+ * Escape the `:` scope-key delimiter (and the `\` escape character itself) in a
21
+ * scope value before composing the colon-joined key. Without this, a scope
22
+ * value containing `:` could shift segment positions and collide two different
23
+ * scopes' index buckets. `_` is escaped too so a literal `_` value can't collide
24
+ * with the unset-key placeholder.
25
+ */
26
+ function escapeScopeValue(value) {
27
+ return value.replace(/[\\:_]/g, "\\$&");
28
+ }
29
+ var warnedMalformedIds = /* @__PURE__ */ new Set();
30
+ var MALFORMED_WARN_CAP = 100;
31
+ function warnMalformedRow(id, err) {
32
+ if (warnedMalformedIds.has(id) || warnedMalformedIds.size >= MALFORMED_WARN_CAP) return;
33
+ warnedMalformedIds.add(id);
34
+ console.warn(`[tanstack-ai-memory] redis: skipped malformed record JSON (id=${id}). The row is left in place (not deleted) in case it is recoverable. Reason: ${String(err)}`);
35
+ }
36
+ /**
37
+ * Production memory adapter backed by plain Redis (no vector index required).
38
+ * Ranks client-side (lexical + optional cosine + recency + importance), so it's
39
+ * suited to up to ~10k records per scope. Bring your own client (`ioredis`, or
40
+ * node-redis wrapped with {@link fromNodeRedis}).
41
+ *
42
+ * Storage model:
43
+ * ```text
44
+ * {prefix}:record:{id} -> JSON MemoryRecord
45
+ * {prefix}:index:{tenantId or _}:{userId or _}:{threadId} -> Set<id>
46
+ * ```
47
+ * Segments are escaped (so `:`, `\\`, `_` in values cannot collide). Missing
48
+ * optional dims become `_` (omit ≠ match any — same exact-match model as the
49
+ * built-in `sameScope` helper). No dual-read of older index layouts.
50
+ */
51
+ function redis(options) {
52
+ const client = options.redis;
53
+ const prefix = options.prefix ?? "tanstack-ai:memory";
54
+ const scopeKey = (scope) => {
55
+ const tenant = scope.tenantId != null && scope.tenantId !== "" ? scope.tenantId : "_";
56
+ const user = scope.userId != null && scope.userId !== "" ? scope.userId : "_";
57
+ return `${escapeScopeValue(tenant)}:${escapeScopeValue(user)}:${escapeScopeValue(scope.threadId)}`;
58
+ };
59
+ const indexKey = (scope) => `${prefix}:index:${scopeKey(scope)}`;
60
+ const recordKey = (id) => `${prefix}:record:${id}`;
61
+ const store = {
62
+ async add(batch) {
63
+ const now = Date.now();
64
+ for (const r of batch) {
65
+ const next = {
66
+ ...r,
67
+ updatedAt: now
68
+ };
69
+ await client.set(recordKey(r.id), JSON.stringify(next));
70
+ await client.sadd(indexKey(r.scope), r.id);
71
+ }
72
+ },
73
+ async loadScope(scope) {
74
+ const idx = indexKey(scope);
75
+ const ids = await client.smembers(idx);
76
+ if (ids.length === 0) return [];
77
+ const raws = await client.mget(...ids.map(recordKey));
78
+ const out = [];
79
+ const stale = [];
80
+ for (let i = 0; i < raws.length; i++) {
81
+ const raw = raws[i];
82
+ const id = ids[i];
83
+ if (!raw) {
84
+ stale.push(id);
85
+ continue;
86
+ }
87
+ let record;
88
+ try {
89
+ record = JSON.parse(raw);
90
+ } catch (err) {
91
+ warnMalformedRow(id, err);
92
+ continue;
93
+ }
94
+ if (isExpired(record)) {
95
+ stale.push(id);
96
+ continue;
97
+ }
98
+ out.push(record);
99
+ }
100
+ if (stale.length > 0) {
101
+ await client.srem(idx, ...stale);
102
+ await client.del(...stale.map(recordKey));
103
+ }
104
+ return out;
105
+ }
106
+ };
107
+ return {
108
+ id: "redis",
109
+ recall: (scope, query) => recallRecords(store, scope, query, options),
110
+ save: (scope, turn) => saveTurn(store, scope, turn, options),
111
+ inspect: (scope) => inspectRecords(store, scope),
112
+ listFacts: (scope) => listRecordFacts(store, scope)
113
+ };
114
+ }
115
+ //#endregion
116
+ export { fromNodeRedis, redis };
117
+
118
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/providers/redis/index.ts"],"sourcesContent":["import {\n inspectRecords,\n isExpired,\n listRecordFacts,\n recallRecords,\n saveTurn,\n} from '../../internal/store'\nimport type {\n BuiltinOptions,\n MemoryRecord,\n RecordStore,\n} from '../../internal/store'\nimport type { MemoryAdapter, MemoryScope } from '../../types'\n\n/**\n * Minimal subset of the Redis client API the adapter uses. Shaped to match\n * `ioredis` directly (lowercase method names). For node-redis v4+'s camelCase\n * API, wrap the client with {@link fromNodeRedis}.\n */\nexport interface RedisLike {\n set: (key: string, value: string) => Promise<unknown>\n get: (key: string) => Promise<string | null>\n del: (...keys: Array<string>) => Promise<unknown>\n sadd: (key: string, ...members: Array<string>) => Promise<unknown>\n srem: (key: string, ...members: Array<string>) => Promise<unknown>\n smembers: (key: string) => Promise<Array<string>>\n mget: (...keys: Array<string>) => Promise<Array<string | null>>\n}\n\n/** node-redis v4+ default-mode (camelCase) surface used by {@link fromNodeRedis}. */\nexport interface NodeRedisLike {\n get: (key: string) => Promise<string | null>\n set: (key: string, value: string) => Promise<unknown>\n del: (keys: Array<string> | string) => Promise<number>\n sAdd: (key: string, members: string | Array<string>) => Promise<number>\n sRem: (key: string, members: string | Array<string>) => Promise<number>\n sMembers: (key: string) => Promise<Array<string>>\n mGet: (keys: Array<string>) => Promise<Array<string | null>>\n}\n\n/**\n * Wrap a node-redis v4+ default-mode client (camelCase API) into the lowercase\n * {@link RedisLike} shape this adapter expects. For `ioredis`, no wrapper is\n * needed — pass the client directly.\n */\nexport function fromNodeRedis(client: NodeRedisLike): RedisLike {\n return {\n get: (key) => client.get(key),\n set: (key, value) => client.set(key, value),\n del: (...keys) => client.del(keys),\n sadd: (key, ...members) => client.sAdd(key, members),\n srem: (key, ...members) => client.sRem(key, members),\n smembers: (key) => client.sMembers(key),\n mget: (...keys) => client.mGet(keys),\n }\n}\n\nexport interface RedisOptions extends BuiltinOptions {\n /** A Redis client implementing {@link RedisLike} (ioredis, or wrapped node-redis). */\n redis: RedisLike\n /** Key prefix. Defaults to `'tanstack-ai:memory'`. */\n prefix?: string\n}\n\n/**\n * Escape the `:` scope-key delimiter (and the `\\` escape character itself) in a\n * scope value before composing the colon-joined key. Without this, a scope\n * value containing `:` could shift segment positions and collide two different\n * scopes' index buckets. `_` is escaped too so a literal `_` value can't collide\n * with the unset-key placeholder.\n */\nfunction escapeScopeValue(value: string): string {\n return value.replace(/[\\\\:_]/g, '\\\\$&')\n}\n\n// Track ids we've warned about so ongoing corruption of DIFFERENT ids keeps\n// surfacing, bounded so a pathological store can't spam the console forever.\nconst warnedMalformedIds = new Set<string>()\nconst MALFORMED_WARN_CAP = 100\nfunction warnMalformedRow(id: string, err: unknown): void {\n if (\n warnedMalformedIds.has(id) ||\n warnedMalformedIds.size >= MALFORMED_WARN_CAP\n ) {\n return\n }\n warnedMalformedIds.add(id)\n console.warn(\n `[tanstack-ai-memory] redis: skipped malformed record JSON (id=${id}). ` +\n `The row is left in place (not deleted) in case it is recoverable. ` +\n `Reason: ${String(err)}`,\n )\n}\n\n/**\n * Production memory adapter backed by plain Redis (no vector index required).\n * Ranks client-side (lexical + optional cosine + recency + importance), so it's\n * suited to up to ~10k records per scope. Bring your own client (`ioredis`, or\n * node-redis wrapped with {@link fromNodeRedis}).\n *\n * Storage model:\n * ```text\n * {prefix}:record:{id} -> JSON MemoryRecord\n * {prefix}:index:{tenantId or _}:{userId or _}:{threadId} -> Set<id>\n * ```\n * Segments are escaped (so `:`, `\\\\`, `_` in values cannot collide). Missing\n * optional dims become `_` (omit ≠ match any — same exact-match model as the\n * built-in `sameScope` helper). No dual-read of older index layouts.\n */\nexport function redis(options: RedisOptions): MemoryAdapter {\n const client = options.redis\n const prefix = options.prefix ?? 'tanstack-ai:memory'\n\n const scopeKey = (scope: MemoryScope): string => {\n const tenant =\n scope.tenantId != null && scope.tenantId !== '' ? scope.tenantId : '_'\n const user =\n scope.userId != null && scope.userId !== '' ? scope.userId : '_'\n return `${escapeScopeValue(tenant)}:${escapeScopeValue(user)}:${escapeScopeValue(scope.threadId)}`\n }\n const indexKey = (scope: MemoryScope): string =>\n `${prefix}:index:${scopeKey(scope)}`\n const recordKey = (id: string): string => `${prefix}:record:${id}`\n\n const store: RecordStore = {\n async add(batch) {\n const now = Date.now()\n for (const r of batch) {\n const next: MemoryRecord = { ...r, updatedAt: now }\n await client.set(recordKey(r.id), JSON.stringify(next))\n await client.sadd(indexKey(r.scope), r.id)\n }\n },\n\n async loadScope(scope: MemoryScope) {\n const idx = indexKey(scope)\n const ids = await client.smembers(idx)\n if (ids.length === 0) return []\n const raws = await client.mget(...ids.map(recordKey))\n const out: Array<MemoryRecord> = []\n const stale: Array<string> = []\n for (let i = 0; i < raws.length; i++) {\n const raw = raws[i] as string | null\n const id = ids[i] as string\n if (!raw) {\n stale.push(id)\n continue\n }\n let record: MemoryRecord\n try {\n record = JSON.parse(raw) as MemoryRecord\n } catch (err) {\n // Malformed JSON is skipped, NOT swept — a parse failure isn't proof\n // the data is unrecoverable (truncated read, older schema, etc.).\n warnMalformedRow(id, err)\n continue\n }\n if (isExpired(record)) {\n stale.push(id)\n continue\n }\n out.push(record)\n }\n if (stale.length > 0) {\n await client.srem(idx, ...stale)\n await client.del(...stale.map(recordKey))\n }\n return out\n },\n }\n\n return {\n id: 'redis',\n recall: (scope, query) => recallRecords(store, scope, query, options),\n save: (scope, turn) => saveTurn(store, scope, turn, options),\n inspect: (scope) => inspectRecords(store, scope),\n listFacts: (scope) => listRecordFacts(store, scope),\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ import { Scope, Tool } from '@tanstack/ai';
2
+ /**
3
+ * Isolation scope for memory reads and writes. Alias of the shared {@link Scope}
4
+ * identity type from `@tanstack/ai` so memory and persistence share one
5
+ * vocabulary (`threadId`, optional `userId` / `tenantId` / `namespace`).
6
+ *
7
+ * Opaque to the middleware — each adapter interprets it (vendors map it to
8
+ * bank/user ids; the built-in stores key their internal record space by it).
9
+ *
10
+ * Resolve every field server-side from trusted session/auth state. A client-
11
+ * originated `threadId` is only safe after you validate it belongs to the
12
+ * session user; never accept bare `userId`/`tenantId` from the request body.
13
+ */
14
+ export type MemoryScope = Scope;
15
+ /** A completed conversation turn handed to {@link MemoryAdapter.save}. */
16
+ export interface MemoryTurn {
17
+ user: string;
18
+ assistant: string;
19
+ }
20
+ /** A discrete recalled item, when the adapter produces them. */
21
+ export interface MemoryFragment {
22
+ /** The recalled text. */
23
+ text: string;
24
+ /** Provenance hint (record id, vendor result type, etc.). */
25
+ source: string;
26
+ }
27
+ /**
28
+ * Result of {@link MemoryAdapter.recall}. Everything the middleware needs to
29
+ * augment the run: a pre-rendered prompt block, optional discrete fragments,
30
+ * and optional tools the adapter wants exposed to the model this turn.
31
+ */
32
+ export interface RecallResult {
33
+ /**
34
+ * Pre-rendered block to inject into the system prompt. An empty string means
35
+ * "nothing to inject" — the middleware skips it.
36
+ */
37
+ systemPrompt: string;
38
+ /**
39
+ * Discrete recalled items, when the adapter produces them. Omitted for
40
+ * engines that return synthesized output (e.g. honcho's dialectic answer).
41
+ */
42
+ fragments?: Array<MemoryFragment>;
43
+ /**
44
+ * Tools the adapter wants exposed to the model for this turn (e.g. hindsight's
45
+ * retain/recall/reflect tools). Merged into the run's tool set by the
46
+ * middleware. Omit or `[]` when the adapter exposes no tools.
47
+ */
48
+ tools?: Array<Tool>;
49
+ /**
50
+ * System-prompt text explaining when/how to use {@link RecallResult.tools}.
51
+ * Injected ahead of `systemPrompt`. Omit or `''` when there are no tools.
52
+ */
53
+ toolGuidance?: string;
54
+ /** Raw vendor payload, surfaced for devtools/inspection. */
55
+ raw?: unknown;
56
+ }
57
+ /**
58
+ * Receipt for a single underlying write performed by {@link MemoryAdapter.save}.
59
+ * One turn can produce several receipts (e.g. hindsight writes the user and
60
+ * assistant utterances separately), so `save` returns an array.
61
+ */
62
+ export interface SaveReceipt {
63
+ ok: boolean;
64
+ /** Optional adapter-reported write latency (ms), for devtools. */
65
+ latencyMs?: number;
66
+ /** Present when `ok` is `false`. */
67
+ error?: string;
68
+ /** Raw vendor payload, surfaced for devtools/inspection. */
69
+ raw?: unknown;
70
+ }
71
+ /** Full snapshot returned by the optional {@link MemoryAdapter.inspect}. */
72
+ export interface MemorySnapshot {
73
+ /** ISO timestamp when the snapshot was taken. */
74
+ takenAt: string;
75
+ /** Adapter-defined snapshot payload. */
76
+ data: unknown;
77
+ }
78
+ /** A flat fact row returned by the optional {@link MemoryAdapter.listFacts}. */
79
+ export interface MemoryFact {
80
+ id: string;
81
+ text: string;
82
+ source?: string;
83
+ /** ISO timestamp, when the adapter tracks creation time. */
84
+ createdAt?: string;
85
+ }
86
+ /**
87
+ * The single memory adapter contract. All backends — the built-in `inMemory()`
88
+ * and `redis()` adapters as well as vendor adapters (`hindsight()`, `mem0()`,
89
+ * `honcho()`) — implement `recall` + `save`. `inspect`/`listFacts` are optional
90
+ * and exist only for devtools/admin surfaces.
91
+ */
92
+ export interface MemoryAdapter {
93
+ /** Stable id used in logs, devtools, and event payloads (e.g. 'in-memory', 'hindsight'). */
94
+ readonly id: string;
95
+ /** Optional human-readable label; defaults to {@link MemoryAdapter.id} in logs. */
96
+ readonly name?: string;
97
+ /**
98
+ * Read side — retrieve what's relevant to `query` within `scope`. The ranking
99
+ * strategy (lexical, semantic, hybrid, vendor-native) is entirely the
100
+ * adapter's concern.
101
+ */
102
+ recall: (scope: MemoryScope, query: string) => Promise<RecallResult>;
103
+ /**
104
+ * Write side — persist a completed turn. Extraction (turn → stored facts)
105
+ * happens HERE, inside the adapter. Returns one receipt per underlying write.
106
+ */
107
+ save: (scope: MemoryScope, turn: MemoryTurn) => Promise<Array<SaveReceipt>>;
108
+ /** Optional — full snapshot for a devtools inspection panel. */
109
+ inspect?: (scope: MemoryScope) => Promise<MemorySnapshot>;
110
+ /** Optional — flat fact list for a devtools panel. */
111
+ listFacts?: (scope: MemoryScope) => Promise<Array<MemoryFact>>;
112
+ }
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "@tanstack/ai-memory",
3
+ "version": "0.0.0",
4
+ "description": "Pluggable memory adapters for TanStack AI memoryMiddleware",
5
+ "author": "",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/TanStack/ai.git",
13
+ "directory": "packages/ai-memory"
14
+ },
15
+ "type": "module",
16
+ "module": "./dist/esm/index.js",
17
+ "types": "./dist/esm/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/esm/index.d.ts",
21
+ "import": "./dist/esm/index.js"
22
+ },
23
+ "./in-memory": {
24
+ "types": "./dist/esm/providers/in-memory/index.d.ts",
25
+ "import": "./dist/esm/providers/in-memory/index.js"
26
+ },
27
+ "./redis": {
28
+ "types": "./dist/esm/providers/redis/index.d.ts",
29
+ "import": "./dist/esm/providers/redis/index.js"
30
+ },
31
+ "./hindsight": {
32
+ "types": "./dist/esm/providers/hindsight/index.d.ts",
33
+ "import": "./dist/esm/providers/hindsight/index.js"
34
+ },
35
+ "./mem0": {
36
+ "types": "./dist/esm/providers/mem0/index.d.ts",
37
+ "import": "./dist/esm/providers/mem0/index.js"
38
+ },
39
+ "./honcho": {
40
+ "types": "./dist/esm/providers/honcho/index.d.ts",
41
+ "import": "./dist/esm/providers/honcho/index.js"
42
+ }
43
+ },
44
+ "sideEffects": false,
45
+ "files": [
46
+ "dist",
47
+ "src",
48
+ "skills"
49
+ ],
50
+ "scripts": {
51
+ "build": "vite build",
52
+ "clean": "premove ./build ./dist",
53
+ "lint:fix": "oxlint src --type-aware --fix",
54
+ "test:build": "publint --strict",
55
+ "test:oxlint": "oxlint src --type-aware",
56
+ "test:lib": "vitest --passWithNoTests",
57
+ "test:lib:dev": "pnpm test:lib --watch",
58
+ "test:types": "tsc"
59
+ },
60
+ "keywords": [
61
+ "ai",
62
+ "tanstack",
63
+ "memory",
64
+ "redis",
65
+ "rag",
66
+ "tanstack-intent"
67
+ ],
68
+ "dependencies": {
69
+ "@tanstack/ai-event-client": "workspace:*"
70
+ },
71
+ "peerDependencies": {
72
+ "@honcho-ai/sdk": ">=2.0.0",
73
+ "@tanstack/ai": "workspace:^",
74
+ "@vectorize-io/hindsight-client": ">=0.6.0",
75
+ "ioredis": ">=5.0.0",
76
+ "redis": ">=4.0.0"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "ioredis": {
80
+ "optional": true
81
+ },
82
+ "redis": {
83
+ "optional": true
84
+ },
85
+ "@vectorize-io/hindsight-client": {
86
+ "optional": true
87
+ },
88
+ "@honcho-ai/sdk": {
89
+ "optional": true
90
+ }
91
+ },
92
+ "devDependencies": {
93
+ "@honcho-ai/sdk": "^2.1.1",
94
+ "@tanstack/ai": "workspace:*",
95
+ "@vectorize-io/hindsight-client": "^0.6.1",
96
+ "@vitest/coverage-v8": "4.0.14",
97
+ "ioredis-mock": "^8.9.0",
98
+ "redis": "^4.7.0"
99
+ }
100
+ }