@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,179 @@
1
+ import {
2
+ inspectRecords,
3
+ isExpired,
4
+ listRecordFacts,
5
+ recallRecords,
6
+ saveTurn,
7
+ } from '../../internal/store'
8
+ import type {
9
+ BuiltinOptions,
10
+ MemoryRecord,
11
+ RecordStore,
12
+ } from '../../internal/store'
13
+ import type { MemoryAdapter, MemoryScope } from '../../types'
14
+
15
+ /**
16
+ * Minimal subset of the Redis client API the adapter uses. Shaped to match
17
+ * `ioredis` directly (lowercase method names). For node-redis v4+'s camelCase
18
+ * API, wrap the client with {@link fromNodeRedis}.
19
+ */
20
+ export interface RedisLike {
21
+ set: (key: string, value: string) => Promise<unknown>
22
+ get: (key: string) => Promise<string | null>
23
+ del: (...keys: Array<string>) => Promise<unknown>
24
+ sadd: (key: string, ...members: Array<string>) => Promise<unknown>
25
+ srem: (key: string, ...members: Array<string>) => Promise<unknown>
26
+ smembers: (key: string) => Promise<Array<string>>
27
+ mget: (...keys: Array<string>) => Promise<Array<string | null>>
28
+ }
29
+
30
+ /** node-redis v4+ default-mode (camelCase) surface used by {@link fromNodeRedis}. */
31
+ export interface NodeRedisLike {
32
+ get: (key: string) => Promise<string | null>
33
+ set: (key: string, value: string) => Promise<unknown>
34
+ del: (keys: Array<string> | string) => Promise<number>
35
+ sAdd: (key: string, members: string | Array<string>) => Promise<number>
36
+ sRem: (key: string, members: string | Array<string>) => Promise<number>
37
+ sMembers: (key: string) => Promise<Array<string>>
38
+ mGet: (keys: Array<string>) => Promise<Array<string | null>>
39
+ }
40
+
41
+ /**
42
+ * Wrap a node-redis v4+ default-mode client (camelCase API) into the lowercase
43
+ * {@link RedisLike} shape this adapter expects. For `ioredis`, no wrapper is
44
+ * needed — pass the client directly.
45
+ */
46
+ export function fromNodeRedis(client: NodeRedisLike): RedisLike {
47
+ return {
48
+ get: (key) => client.get(key),
49
+ set: (key, value) => client.set(key, value),
50
+ del: (...keys) => client.del(keys),
51
+ sadd: (key, ...members) => client.sAdd(key, members),
52
+ srem: (key, ...members) => client.sRem(key, members),
53
+ smembers: (key) => client.sMembers(key),
54
+ mget: (...keys) => client.mGet(keys),
55
+ }
56
+ }
57
+
58
+ export interface RedisOptions extends BuiltinOptions {
59
+ /** A Redis client implementing {@link RedisLike} (ioredis, or wrapped node-redis). */
60
+ redis: RedisLike
61
+ /** Key prefix. Defaults to `'tanstack-ai:memory'`. */
62
+ prefix?: string
63
+ }
64
+
65
+ /**
66
+ * Escape the `:` scope-key delimiter (and the `\` escape character itself) in a
67
+ * scope value before composing the colon-joined key. Without this, a scope
68
+ * value containing `:` could shift segment positions and collide two different
69
+ * scopes' index buckets. `_` is escaped too so a literal `_` value can't collide
70
+ * with the unset-key placeholder.
71
+ */
72
+ function escapeScopeValue(value: string): string {
73
+ return value.replace(/[\\:_]/g, '\\$&')
74
+ }
75
+
76
+ // Track ids we've warned about so ongoing corruption of DIFFERENT ids keeps
77
+ // surfacing, bounded so a pathological store can't spam the console forever.
78
+ const warnedMalformedIds = new Set<string>()
79
+ const MALFORMED_WARN_CAP = 100
80
+ function warnMalformedRow(id: string, err: unknown): void {
81
+ if (
82
+ warnedMalformedIds.has(id) ||
83
+ warnedMalformedIds.size >= MALFORMED_WARN_CAP
84
+ ) {
85
+ return
86
+ }
87
+ warnedMalformedIds.add(id)
88
+ console.warn(
89
+ `[tanstack-ai-memory] redis: skipped malformed record JSON (id=${id}). ` +
90
+ `The row is left in place (not deleted) in case it is recoverable. ` +
91
+ `Reason: ${String(err)}`,
92
+ )
93
+ }
94
+
95
+ /**
96
+ * Production memory adapter backed by plain Redis (no vector index required).
97
+ * Ranks client-side (lexical + optional cosine + recency + importance), so it's
98
+ * suited to up to ~10k records per scope. Bring your own client (`ioredis`, or
99
+ * node-redis wrapped with {@link fromNodeRedis}).
100
+ *
101
+ * Storage model:
102
+ * ```text
103
+ * {prefix}:record:{id} -> JSON MemoryRecord
104
+ * {prefix}:index:{tenantId or _}:{userId or _}:{threadId} -> Set<id>
105
+ * ```
106
+ * Segments are escaped (so `:`, `\\`, `_` in values cannot collide). Missing
107
+ * optional dims become `_` (omit ≠ match any — same exact-match model as the
108
+ * built-in `sameScope` helper). No dual-read of older index layouts.
109
+ */
110
+ export function redis(options: RedisOptions): MemoryAdapter {
111
+ const client = options.redis
112
+ const prefix = options.prefix ?? 'tanstack-ai:memory'
113
+
114
+ const scopeKey = (scope: MemoryScope): string => {
115
+ const tenant =
116
+ scope.tenantId != null && scope.tenantId !== '' ? scope.tenantId : '_'
117
+ const user =
118
+ scope.userId != null && scope.userId !== '' ? scope.userId : '_'
119
+ return `${escapeScopeValue(tenant)}:${escapeScopeValue(user)}:${escapeScopeValue(scope.threadId)}`
120
+ }
121
+ const indexKey = (scope: MemoryScope): string =>
122
+ `${prefix}:index:${scopeKey(scope)}`
123
+ const recordKey = (id: string): string => `${prefix}:record:${id}`
124
+
125
+ const store: RecordStore = {
126
+ async add(batch) {
127
+ const now = Date.now()
128
+ for (const r of batch) {
129
+ const next: MemoryRecord = { ...r, updatedAt: now }
130
+ await client.set(recordKey(r.id), JSON.stringify(next))
131
+ await client.sadd(indexKey(r.scope), r.id)
132
+ }
133
+ },
134
+
135
+ async loadScope(scope: MemoryScope) {
136
+ const idx = indexKey(scope)
137
+ const ids = await client.smembers(idx)
138
+ if (ids.length === 0) return []
139
+ const raws = await client.mget(...ids.map(recordKey))
140
+ const out: Array<MemoryRecord> = []
141
+ const stale: Array<string> = []
142
+ for (let i = 0; i < raws.length; i++) {
143
+ const raw = raws[i] as string | null
144
+ const id = ids[i] as string
145
+ if (!raw) {
146
+ stale.push(id)
147
+ continue
148
+ }
149
+ let record: MemoryRecord
150
+ try {
151
+ record = JSON.parse(raw) as MemoryRecord
152
+ } catch (err) {
153
+ // Malformed JSON is skipped, NOT swept — a parse failure isn't proof
154
+ // the data is unrecoverable (truncated read, older schema, etc.).
155
+ warnMalformedRow(id, err)
156
+ continue
157
+ }
158
+ if (isExpired(record)) {
159
+ stale.push(id)
160
+ continue
161
+ }
162
+ out.push(record)
163
+ }
164
+ if (stale.length > 0) {
165
+ await client.srem(idx, ...stale)
166
+ await client.del(...stale.map(recordKey))
167
+ }
168
+ return out
169
+ },
170
+ }
171
+
172
+ return {
173
+ id: 'redis',
174
+ recall: (scope, query) => recallRecords(store, scope, query, options),
175
+ save: (scope, turn) => saveTurn(store, scope, turn, options),
176
+ inspect: (scope) => inspectRecords(store, scope),
177
+ listFacts: (scope) => listRecordFacts(store, scope),
178
+ }
179
+ }
package/src/types.ts ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Public contract for the TanStack AI memory subsystem.
3
+ *
4
+ * A memory backend implements ONE contract with two verbs: {@link MemoryAdapter.recall}
5
+ * and {@link MemoryAdapter.save}. This is deliberately the shape every real memory
6
+ * provider (mem0, honcho, hindsight, …) already exposes — "what's relevant for this
7
+ * query?" and "remember this turn". The middleware ({@link memoryMiddleware}) is thin:
8
+ * it calls `recall` before the model runs and defers `save` after the turn finishes.
9
+ *
10
+ * Adapters own everything else. Extraction (turning a turn into stored facts),
11
+ * ranking, rendering into a prompt, scope isolation, and expiry are all the
12
+ * adapter's responsibility — the middleware never inspects records. The built-in
13
+ * `inMemory()` / `redis()` adapters keep their store/scoring internals private
14
+ * behind `recall`/`save`; vendor adapters map these two verbs onto their APIs.
15
+ */
16
+
17
+ import type { Scope, Tool } from '@tanstack/ai'
18
+
19
+ // ===========================
20
+ // Scope & turn primitives
21
+ // ===========================
22
+
23
+ /**
24
+ * Isolation scope for memory reads and writes. Alias of the shared {@link Scope}
25
+ * identity type from `@tanstack/ai` so memory and persistence share one
26
+ * vocabulary (`threadId`, optional `userId` / `tenantId` / `namespace`).
27
+ *
28
+ * Opaque to the middleware — each adapter interprets it (vendors map it to
29
+ * bank/user ids; the built-in stores key their internal record space by it).
30
+ *
31
+ * Resolve every field server-side from trusted session/auth state. A client-
32
+ * originated `threadId` is only safe after you validate it belongs to the
33
+ * session user; never accept bare `userId`/`tenantId` from the request body.
34
+ */
35
+ export type MemoryScope = Scope
36
+
37
+ /** A completed conversation turn handed to {@link MemoryAdapter.save}. */
38
+ export interface MemoryTurn {
39
+ user: string
40
+ assistant: string
41
+ }
42
+
43
+ // ===========================
44
+ // Recall
45
+ // ===========================
46
+
47
+ /** A discrete recalled item, when the adapter produces them. */
48
+ export interface MemoryFragment {
49
+ /** The recalled text. */
50
+ text: string
51
+ /** Provenance hint (record id, vendor result type, etc.). */
52
+ source: string
53
+ }
54
+
55
+ /**
56
+ * Result of {@link MemoryAdapter.recall}. Everything the middleware needs to
57
+ * augment the run: a pre-rendered prompt block, optional discrete fragments,
58
+ * and optional tools the adapter wants exposed to the model this turn.
59
+ */
60
+ export interface RecallResult {
61
+ /**
62
+ * Pre-rendered block to inject into the system prompt. An empty string means
63
+ * "nothing to inject" — the middleware skips it.
64
+ */
65
+ systemPrompt: string
66
+ /**
67
+ * Discrete recalled items, when the adapter produces them. Omitted for
68
+ * engines that return synthesized output (e.g. honcho's dialectic answer).
69
+ */
70
+ fragments?: Array<MemoryFragment>
71
+ /**
72
+ * Tools the adapter wants exposed to the model for this turn (e.g. hindsight's
73
+ * retain/recall/reflect tools). Merged into the run's tool set by the
74
+ * middleware. Omit or `[]` when the adapter exposes no tools.
75
+ */
76
+ tools?: Array<Tool>
77
+ /**
78
+ * System-prompt text explaining when/how to use {@link RecallResult.tools}.
79
+ * Injected ahead of `systemPrompt`. Omit or `''` when there are no tools.
80
+ */
81
+ toolGuidance?: string
82
+ /** Raw vendor payload, surfaced for devtools/inspection. */
83
+ raw?: unknown
84
+ }
85
+
86
+ // ===========================
87
+ // Save
88
+ // ===========================
89
+
90
+ /**
91
+ * Receipt for a single underlying write performed by {@link MemoryAdapter.save}.
92
+ * One turn can produce several receipts (e.g. hindsight writes the user and
93
+ * assistant utterances separately), so `save` returns an array.
94
+ */
95
+ export interface SaveReceipt {
96
+ ok: boolean
97
+ /** Optional adapter-reported write latency (ms), for devtools. */
98
+ latencyMs?: number
99
+ /** Present when `ok` is `false`. */
100
+ error?: string
101
+ /** Raw vendor payload, surfaced for devtools/inspection. */
102
+ raw?: unknown
103
+ }
104
+
105
+ // ===========================
106
+ // Optional introspection (devtools / admin panels)
107
+ // ===========================
108
+
109
+ /** Full snapshot returned by the optional {@link MemoryAdapter.inspect}. */
110
+ export interface MemorySnapshot {
111
+ /** ISO timestamp when the snapshot was taken. */
112
+ takenAt: string
113
+ /** Adapter-defined snapshot payload. */
114
+ data: unknown
115
+ }
116
+
117
+ /** A flat fact row returned by the optional {@link MemoryAdapter.listFacts}. */
118
+ export interface MemoryFact {
119
+ id: string
120
+ text: string
121
+ source?: string
122
+ /** ISO timestamp, when the adapter tracks creation time. */
123
+ createdAt?: string
124
+ }
125
+
126
+ // ===========================
127
+ // Adapter contract
128
+ // ===========================
129
+
130
+ /**
131
+ * The single memory adapter contract. All backends — the built-in `inMemory()`
132
+ * and `redis()` adapters as well as vendor adapters (`hindsight()`, `mem0()`,
133
+ * `honcho()`) — implement `recall` + `save`. `inspect`/`listFacts` are optional
134
+ * and exist only for devtools/admin surfaces.
135
+ */
136
+ export interface MemoryAdapter {
137
+ /** Stable id used in logs, devtools, and event payloads (e.g. 'in-memory', 'hindsight'). */
138
+ readonly id: string
139
+ /** Optional human-readable label; defaults to {@link MemoryAdapter.id} in logs. */
140
+ readonly name?: string
141
+
142
+ /**
143
+ * Read side — retrieve what's relevant to `query` within `scope`. The ranking
144
+ * strategy (lexical, semantic, hybrid, vendor-native) is entirely the
145
+ * adapter's concern.
146
+ */
147
+ recall: (scope: MemoryScope, query: string) => Promise<RecallResult>
148
+
149
+ /**
150
+ * Write side — persist a completed turn. Extraction (turn → stored facts)
151
+ * happens HERE, inside the adapter. Returns one receipt per underlying write.
152
+ */
153
+ save: (scope: MemoryScope, turn: MemoryTurn) => Promise<Array<SaveReceipt>>
154
+
155
+ /** Optional — full snapshot for a devtools inspection panel. */
156
+ inspect?: (scope: MemoryScope) => Promise<MemorySnapshot>
157
+ /** Optional — flat fact list for a devtools panel. */
158
+ listFacts?: (scope: MemoryScope) => Promise<Array<MemoryFact>>
159
+ }