@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.
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/internal/store.d.ts +95 -0
- package/dist/esm/internal/store.js +201 -0
- package/dist/esm/internal/store.js.map +1 -0
- package/dist/esm/internal/store.test.d.ts +1 -0
- package/dist/esm/middleware.d.ts +73 -0
- package/dist/esm/middleware.js +250 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/providers/hindsight/index.d.ts +50 -0
- package/dist/esm/providers/hindsight/index.js +152 -0
- package/dist/esm/providers/hindsight/index.js.map +1 -0
- package/dist/esm/providers/hindsight/tools.d.ts +16 -0
- package/dist/esm/providers/hindsight/tools.js +119 -0
- package/dist/esm/providers/hindsight/tools.js.map +1 -0
- package/dist/esm/providers/honcho/index.d.ts +16 -0
- package/dist/esm/providers/honcho/index.js +150 -0
- package/dist/esm/providers/honcho/index.js.map +1 -0
- package/dist/esm/providers/in-memory/index.d.ts +19 -0
- package/dist/esm/providers/in-memory/index.js +46 -0
- package/dist/esm/providers/in-memory/index.js.map +1 -0
- package/dist/esm/providers/mem0/index.d.ts +14 -0
- package/dist/esm/providers/mem0/index.js +150 -0
- package/dist/esm/providers/mem0/index.js.map +1 -0
- package/dist/esm/providers/redis/index.d.ts +54 -0
- package/dist/esm/providers/redis/index.js +118 -0
- package/dist/esm/providers/redis/index.js.map +1 -0
- package/dist/esm/types.d.ts +112 -0
- package/package.json +100 -0
- package/skills/tanstack-ai-memory/SKILL.md +99 -0
- package/skills/tanstack-ai-memory-hindsight/SKILL.md +40 -0
- package/skills/tanstack-ai-memory-honcho/SKILL.md +40 -0
- package/skills/tanstack-ai-memory-in-memory/SKILL.md +51 -0
- package/skills/tanstack-ai-memory-mem0/SKILL.md +36 -0
- package/skills/tanstack-ai-memory-redis/SKILL.md +83 -0
- package/src/index.ts +20 -0
- package/src/internal/store.test.ts +63 -0
- package/src/internal/store.ts +378 -0
- package/src/middleware.ts +386 -0
- package/src/providers/hindsight/index.ts +237 -0
- package/src/providers/hindsight/tools.ts +139 -0
- package/src/providers/honcho/index.ts +238 -0
- package/src/providers/in-memory/index.ts +63 -0
- package/src/providers/mem0/index.ts +199 -0
- package/src/providers/redis/index.ts +179 -0
- 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
|
+
}
|