@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,99 @@
1
+ ---
2
+ name: tanstack-ai-memory
3
+ description: Use when wiring memoryMiddleware from @tanstack/ai-memory into a chat() call — covers the recall/save adapter contract, scope shape and server-side scope security, the recall-inject / deferred-save lifecycle, choosing an adapter (inMemory, redis, hindsight, mem0, honcho), and devtools events.
4
+ ---
5
+
6
+ # TanStack AI Memory Middleware
7
+
8
+ Use this when adding **server-side memory** to a `chat()` call. Everything lives in
9
+ `@tanstack/ai-memory`. A memory adapter is a single contract with two verbs — `recall`
10
+ and `save` — and the middleware is thin: it recalls into the system prompt before the
11
+ model runs and defers `save` after the turn finishes.
12
+
13
+ ## When to reach for it
14
+
15
+ - A user expects "remember what I told you last time."
16
+ - Per-user or per-thread context that must survive across sessions.
17
+ - A hosted memory service (mem0, Honcho, Hindsight).
18
+
19
+ Do NOT use this just to keep recent messages — that's the `messages` array on `chat()`.
20
+ Memory is for cross-turn / cross-session recall, not within-turn history.
21
+
22
+ ## Wire it up
23
+
24
+ ```ts
25
+ import { chat } from '@tanstack/ai'
26
+ import { openaiText } from '@tanstack/ai-openai'
27
+ import { memoryMiddleware } from '@tanstack/ai-memory'
28
+ import { inMemory } from '@tanstack/ai-memory/in-memory'
29
+
30
+ const memory = inMemory() // dev/tests only — see the in-memory skill
31
+
32
+ const stream = chat({
33
+ adapter: openaiText('gpt-5.5'),
34
+ messages,
35
+ context: { session }, // attached by your auth middleware
36
+ middleware: [
37
+ memoryMiddleware({
38
+ adapter: memory,
39
+ // Derive scope server-side from trusted session state.
40
+ scope: (ctx) => {
41
+ const session = getSession(ctx)
42
+ return { threadId: session.threadId, userId: session.userId }
43
+ },
44
+ }),
45
+ ],
46
+ })
47
+ ```
48
+
49
+ `memoryMiddleware` options: `adapter`, `scope` (static or a function of `ctx`),
50
+ `role` (`'recall+save'` default, or `'save-only'`), and `onRecall` / `onSave` telemetry
51
+ callbacks.
52
+
53
+ ## The contract
54
+
55
+ ```ts
56
+ interface MemoryAdapter {
57
+ id: string
58
+ recall(scope, query): Promise<RecallResult> // { systemPrompt, fragments?, tools?, toolGuidance? }
59
+ save(scope, turn): Promise<Array<SaveReceipt>> // turn = { user, assistant }; extraction lives HERE
60
+ inspect?(scope): Promise<MemorySnapshot> // optional (devtools)
61
+ listFacts?(scope): Promise<Array<MemoryFact>> // optional (devtools)
62
+ }
63
+ ```
64
+
65
+ - `recall` decides relevance and renders a `systemPrompt`; it may also return `tools` +
66
+ `toolGuidance` to hand the model direct control of memory (hindsight does this).
67
+ - `save` owns extraction — turning the raw turn into whatever gets persisted.
68
+
69
+ ## Scope security
70
+
71
+ `MemoryScope` is an alias of the shared `Scope` type from `@tanstack/ai`:
72
+ `{ threadId, userId?, tenantId?, namespace? }`. It is the isolation boundary. **Never
73
+ trust a client-supplied `userId`/`threadId`.** Resolve scope server-side from
74
+ session/auth and pass the validated session through `chat({ context: { session } })`. If
75
+ you accept a thread id from the request body, validate it belongs to the session user
76
+ BEFORE using it.
77
+
78
+ ## Adapters
79
+
80
+ - `inMemory()` / `redis()` — exact match on `threadId` + optional `userId`/`tenantId`
81
+ (`namespace` ignored). Redis index keys include all three segments.
82
+ - `hindsight()` — bank `{tenant|_}__{user}__{threadId}`.
83
+ - `mem0()` — `user_id` + `run_id` (`threadId`); no `tenantId`.
84
+ - `honcho()` — session `{tenant|_}__{threadId}`; peer tenant-prefixed when set.
85
+ - Custom — implement `recall`/`save` and run `@tanstack/ai-memory/tests/contract`.
86
+
87
+ ## Failure modes
88
+
89
+ Memory failures are non-fatal: a throwing `recall` or `save` emits `memory:error` and
90
+ the run continues with degraded memory. Streaming is never blocked; a failed save never
91
+ fails the turn.
92
+
93
+ ## Devtools
94
+
95
+ Five events on `aiEventClient` (from `@tanstack/ai-event-client`):
96
+ `memory:retrieve:started` / `:completed`, `memory:persist:started` / `:completed`,
97
+ `memory:error` (`phase: 'recall' | 'save'`). Payloads carry the adapter id and
98
+ fragment/receipt counts, not full memory text. Error events include `scope` only
99
+ when it was already resolved; if the resolver threw, `scope` is omitted.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: tanstack-ai-memory-hindsight
3
+ description: Use when wiring hindsight() from @tanstack/ai-memory/hindsight — a hosted memory adapter that buckets memory per conversation and exposes retain/recall/reflect tools to the model. Requires the optional @vectorize-io/hindsight-client peer.
4
+ ---
5
+
6
+ # Hindsight Memory Adapter
7
+
8
+ Hosted `recall`/`save` adapter backed by Hindsight. Hindsight owns extraction and
9
+ ranking server-side, buckets memory into per-conversation "banks"
10
+ (`{tenantId|_}__{user}__{threadId}`), and — uniquely — exposes LLM **tools** through `recall` so the
11
+ model can retain/recall/reflect directly.
12
+
13
+ ## Setup
14
+
15
+ ```ts
16
+ import { memoryMiddleware } from '@tanstack/ai-memory'
17
+ import { hindsight } from '@tanstack/ai-memory/hindsight'
18
+
19
+ const memory = hindsight({ user: currentUserId }) // baseUrl defaults to HINDSIGHT_URL
20
+
21
+ memoryMiddleware({ adapter: memory, scope })
22
+ ```
23
+
24
+ `@vectorize-io/hindsight-client` is an **optional peer dependency**, loaded lazily on
25
+ first use — install it where you use `hindsight()`.
26
+
27
+ ## Options
28
+
29
+ - `user` — durable user id for the bank key (falls back to `scope.userId`).
30
+ - `baseUrl` — Hindsight server URL (default `HINDSIGHT_URL` or `http://localhost:8888`).
31
+ - `budget` — recall budget: `'low' | 'mid' | 'high'` (default `'mid'`).
32
+ - `onToolRetain` / `onToolRecall` — callbacks fired when the model uses the memory tools.
33
+
34
+ **Scope fields:** bank id is `{tenantId|_}__{user}__{threadId}`. `namespace` is ignored.
35
+
36
+ ## Tools
37
+
38
+ `recall` returns `hindsight_retain`, `hindsight_recall`, and `hindsight_reflect` in its
39
+ `tools` plus a `toolGuidance` block. `memoryMiddleware` merges them into the run so the
40
+ model can manage long-term memory itself.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: tanstack-ai-memory-honcho
3
+ description: Use when wiring honcho() from @tanstack/ai-memory/honcho — a hosted memory adapter where recall is a dialectic answer over the user's representation (no discrete fragments). Requires the optional @honcho-ai/sdk peer.
4
+ ---
5
+
6
+ # Honcho Memory Adapter
7
+
8
+ Hosted `recall`/`save` adapter backed by Honcho. Honcho models memory as peers
9
+ exchanging messages in a session; `recall` returns a **synthesized dialectic answer**
10
+ over the user peer's representation (so there are no discrete fragments), and `save`
11
+ appends the turn's messages to the session.
12
+
13
+ ## Setup
14
+
15
+ ```ts
16
+ import { memoryMiddleware } from '@tanstack/ai-memory'
17
+ import { honcho } from '@tanstack/ai-memory/honcho'
18
+
19
+ const memory = honcho({ user: currentUserId }) // baseURL defaults to HONCHO_URL
20
+
21
+ memoryMiddleware({ adapter: memory, scope })
22
+ ```
23
+
24
+ `@honcho-ai/sdk` is an **optional peer dependency**, loaded lazily on first use — install
25
+ it where you use `honcho()`.
26
+
27
+ ## Options
28
+
29
+ - `user` — user peer id (falls back to `scope.userId`, then `'demo-user'`).
30
+ - `baseURL` — Honcho server URL (default `HONCHO_URL` or `http://localhost:8001`).
31
+ - `workspaceId` — default `HONCHO_APP_NAME` or `'ai-memory'`.
32
+ - `apiKey` — default `HONCHO_API_KEY`.
33
+ - `assistantId` — assistant peer id (default `'assistant'`).
34
+
35
+ **Scope fields:** session key = `{tenantId|_}__{threadId}`; peer id is
36
+ `{tenantId}__{user}` when `tenantId` is set, otherwise `user` / `scope.userId`.
37
+ `namespace` is ignored.
38
+
39
+ `recall` calls the user peer's dialectic `chat()` and injects the answer as the system
40
+ prompt; Honcho exposes no LLM tools.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: tanstack-ai-memory-in-memory
3
+ description: Use when wiring inMemory() from @tanstack/ai-memory/in-memory — explains setup, options (embedder, extract, topK/minScore), when to pick it (dev/tests/single-process demos), and what NOT to use it for (multi-process or persistent).
4
+ ---
5
+
6
+ # In-Memory Memory Adapter
7
+
8
+ Zero-dependency `recall`/`save` adapter backed by a `Map`. Records vanish on process
9
+ restart.
10
+
11
+ ## When to use it
12
+
13
+ - Local development.
14
+ - Vitest / Playwright tests.
15
+ - Single-process demos where users don't need persistence.
16
+
17
+ ## When NOT to use it
18
+
19
+ - Production multi-process deployments — every worker has its own `Map`; users get
20
+ inconsistent memory.
21
+ - Anything that needs survival across restarts.
22
+
23
+ For production, use `redis()` (see the `tanstack-ai-memory-redis` skill).
24
+
25
+ ## Setup
26
+
27
+ ```ts
28
+ import { memoryMiddleware } from '@tanstack/ai-memory'
29
+ import { inMemory } from '@tanstack/ai-memory/in-memory'
30
+
31
+ const memory = inMemory()
32
+
33
+ memoryMiddleware({ adapter: memory, scope })
34
+ ```
35
+
36
+ ## Options
37
+
38
+ `inMemory(options?)` accepts:
39
+
40
+ - `topK` (default 6), `minScore` (default 0.15), `kinds` — recall tuning.
41
+ - `embedder: { embed(text): Promise<number[]> }` — enable semantic scoring (both
42
+ `recall` and `save` embed through it).
43
+ - `extract(turn, scope)` — return derived facts to persist alongside the raw turn
44
+ (e.g. call an LLM to pull out preferences). Without it, `save` stores the raw
45
+ user/assistant messages and `recall` scores them lexically + by recency.
46
+ - `render(hits)` — replace the built-in prompt renderer.
47
+
48
+ ## Capacity
49
+
50
+ The adapter scans every record in a scope per `recall`. Fine up to ~100k records; beyond
51
+ that, switch to Redis.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: tanstack-ai-memory-mem0
3
+ description: Use when wiring mem0() from @tanstack/ai-memory/mem0 — a hosted memory adapter that talks to a mem0 server over plain HTTP (no SDK peer). Requires a running mem0 server.
4
+ ---
5
+
6
+ # mem0 Memory Adapter
7
+
8
+ Hosted `recall`/`save` adapter backed by a mem0 server. mem0 owns extraction and ranking
9
+ server-side. Talks to the server over plain HTTP — **no SDK peer dependency**.
10
+
11
+ ## Setup
12
+
13
+ ```ts
14
+ import { memoryMiddleware } from '@tanstack/ai-memory'
15
+ import { mem0 } from '@tanstack/ai-memory/mem0'
16
+
17
+ const memory = mem0({ user: currentUserId }) // baseUrl defaults to MEM0_URL
18
+
19
+ memoryMiddleware({ adapter: memory, scope })
20
+ ```
21
+
22
+ Requires a running mem0 server (self-hosted or hosted). Point it via `baseUrl` (or
23
+ `MEM0_URL`); pass `apiKey` (or `MEM0_ADMIN_API_KEY`) when secured.
24
+
25
+ ## Options
26
+
27
+ - `user` — mem0 `user_id` (falls back to `scope.userId`, then `'demo-user'`).
28
+ - `baseUrl` — mem0 server URL (default `MEM0_URL` or `http://localhost:8000`).
29
+ - `apiKey` — bearer token (default `MEM0_ADMIN_API_KEY`).
30
+ - `rerank` (default `true`), `threshold` (default `0.1`) — search tuning.
31
+
32
+ **Scope fields:** requests send `user_id` and `run_id` (`scope.threadId`). `tenantId`
33
+ and `namespace` are not sent — encode multi-tenant isolation into `user` if needed.
34
+
35
+ `save` posts the `{ user, assistant }` turn to `/memories`; `recall` queries `/search`
36
+ and renders the results into the system prompt. mem0 exposes no LLM tools.
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: tanstack-ai-memory-redis
3
+ description: Use when wiring redis() from @tanstack/ai-memory/redis in production — covers client setup (ioredis or node-redis via fromNodeRedis), the storage model, client-side ranking limits, and troubleshooting.
4
+ ---
5
+
6
+ # Redis Memory Adapter
7
+
8
+ Production-grade `recall`/`save` adapter backed by plain Redis (no vector index
9
+ required). Ranks client-side (lexical + optional cosine + recency + importance).
10
+
11
+ ## Setup
12
+
13
+ Bring your own Redis client. `ioredis` wires in directly; `redis` (node-redis v4+) needs
14
+ a small wrapper.
15
+
16
+ ### Option A: `ioredis`
17
+
18
+ ```ts
19
+ import Redis from 'ioredis'
20
+ import { memoryMiddleware } from '@tanstack/ai-memory'
21
+ import { redis } from '@tanstack/ai-memory/redis'
22
+
23
+ const client = new Redis(process.env.REDIS_URL)
24
+ const memory = redis({ redis: client, prefix: 'myapp:memory' })
25
+
26
+ memoryMiddleware({ adapter: memory, scope })
27
+ ```
28
+
29
+ ### Option B: `redis` (node-redis v4+)
30
+
31
+ ```ts
32
+ import { createClient } from 'redis'
33
+ import { memoryMiddleware } from '@tanstack/ai-memory'
34
+ import { redis, fromNodeRedis } from '@tanstack/ai-memory/redis'
35
+
36
+ const client = createClient({ url: process.env.REDIS_URL })
37
+ await client.connect()
38
+
39
+ const memory = redis({
40
+ redis: fromNodeRedis(client),
41
+ prefix: 'myapp:memory',
42
+ })
43
+
44
+ memoryMiddleware({ adapter: memory, scope })
45
+ ```
46
+
47
+ node-redis exposes a camelCase API (`sAdd`, `mGet`); `fromNodeRedis` translates it
48
+ to the lowercase `RedisLike` shape. Passing a raw node-redis client without the wrapper
49
+ throws `client.sadd is not a function`.
50
+
51
+ `redis()` accepts the same `topK` / `minScore` / `kinds` / `embedder` / `extract` options
52
+ as `inMemory()`.
53
+
54
+ ## Storage model
55
+
56
+ ```text
57
+ {prefix}:record:{id} -> JSON record
58
+ {prefix}:index:{tenantId or _}:{userId or _}:{threadId} -> Set<id>
59
+ ```
60
+
61
+ `save` writes the record and adds it to the scope's index set; `recall` loads the set,
62
+ scores, and renders. Scope values are escaped (`:`, `\`, and `_`) so a delimiter or the
63
+ unset placeholder inside a dim can't collide two scopes.
64
+
65
+ **Hard cut:** there is no dual-read of older index layouts. If you previously wrote under
66
+ a different shape (e.g. without `tenantId`), reindex or wipe — old keys are orphaned.
67
+
68
+ Always pass the same `tenantId`/`userId`/`threadId` on write and read: missing optional
69
+ dims become `_`, so omit ≠ "match any".
70
+
71
+ ## Ranking limits
72
+
73
+ Ranking is client-side: `recall` loads every record for the scope into Node and scores
74
+ it. Fine up to ~10k records per scope. Beyond that, write a vector-index-aware adapter
75
+ against the same `recall`/`save` contract.
76
+
77
+ ## Troubleshooting
78
+
79
+ - **Records not visible across processes:** ensure every process uses the same
80
+ `REDIS_URL` and `prefix`.
81
+ - **Malformed JSON rows:** a row whose JSON won't parse is skipped on read and **left in
82
+ place** (never deleted) — the signal is a one-time `console.warn` per bad id. Fix or
83
+ delete the offending `{prefix}:record:{id}` key to remediate.
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export {
2
+ memoryMiddleware,
3
+ MEMORY_STATE_EVENT,
4
+ type MemoryMiddlewareOptions,
5
+ type MemoryMiddlewareRole,
6
+ type MemoryRecallInfo,
7
+ type MemorySaveInfo,
8
+ type MemoryStateEventValue,
9
+ } from './middleware'
10
+
11
+ export type {
12
+ MemoryAdapter,
13
+ MemoryScope,
14
+ MemoryTurn,
15
+ MemoryFragment,
16
+ RecallResult,
17
+ SaveReceipt,
18
+ MemorySnapshot,
19
+ MemoryFact,
20
+ } from './types'
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { sameScope } from './store'
3
+
4
+ describe('sameScope', () => {
5
+ const full = {
6
+ threadId: 's',
7
+ userId: 'u',
8
+ tenantId: 'a',
9
+ }
10
+
11
+ it('matches when threadId and optional dims agree', () => {
12
+ expect(sameScope(full, full)).toBe(true)
13
+ expect(sameScope(full, { threadId: 's', userId: 'u', tenantId: 'a' })).toBe(
14
+ true,
15
+ )
16
+ })
17
+
18
+ it('rejects different threadId', () => {
19
+ expect(sameScope(full, { ...full, threadId: 'other' })).toBe(false)
20
+ })
21
+
22
+ it('rejects different userId', () => {
23
+ expect(
24
+ sameScope(full, { threadId: 's', userId: 'other', tenantId: 'a' }),
25
+ ).toBe(false)
26
+ })
27
+
28
+ it('rejects different tenantId', () => {
29
+ expect(sameScope(full, { ...full, tenantId: 'b' })).toBe(false)
30
+ })
31
+
32
+ it('treats omitted optional dims as exact (not wildcards)', () => {
33
+ // A query without tenant/user must not match a record that has them —
34
+ // same isolation model as Redis composite index keys.
35
+ expect(sameScope(full, { threadId: 's', userId: 'u' })).toBe(false)
36
+ expect(sameScope(full, { threadId: 's' })).toBe(false)
37
+ expect(sameScope(full, { threadId: 's', userId: '', tenantId: '' })).toBe(
38
+ false,
39
+ )
40
+ })
41
+
42
+ it('matches when both sides omit the same optional dims', () => {
43
+ expect(sameScope({ threadId: 's' }, { threadId: 's' })).toBe(true)
44
+ expect(
45
+ sameScope({ threadId: 's', userId: 'u' }, { threadId: 's', userId: 'u' }),
46
+ ).toBe(true)
47
+ })
48
+
49
+ it('treats empty string as unset for optional dims', () => {
50
+ expect(sameScope({ threadId: 's', userId: '' }, { threadId: 's' })).toBe(
51
+ true,
52
+ )
53
+ })
54
+
55
+ it('ignores namespace (reserved — no subsystem keys on it yet)', () => {
56
+ expect(
57
+ sameScope(
58
+ { ...full, namespace: 'bank-a' },
59
+ { ...full, namespace: 'bank-b' },
60
+ ),
61
+ ).toBe(true)
62
+ })
63
+ })