@tanstack/ai-persistence 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 (48) hide show
  1. package/dist/esm/blob-range.d.ts +51 -0
  2. package/dist/esm/blob-range.js +84 -0
  3. package/dist/esm/blob-range.js.map +1 -0
  4. package/dist/esm/capabilities.d.ts +5 -0
  5. package/dist/esm/capabilities.js +16 -0
  6. package/dist/esm/capabilities.js.map +1 -0
  7. package/dist/esm/index.d.ts +13 -0
  8. package/dist/esm/index.js +9 -0
  9. package/dist/esm/memory.d.ts +19 -0
  10. package/dist/esm/memory.js +319 -0
  11. package/dist/esm/memory.js.map +1 -0
  12. package/dist/esm/middleware.d.ts +252 -0
  13. package/dist/esm/middleware.js +872 -0
  14. package/dist/esm/middleware.js.map +1 -0
  15. package/dist/esm/reconstruct-generation.d.ts +129 -0
  16. package/dist/esm/reconstruct-generation.js +148 -0
  17. package/dist/esm/reconstruct-generation.js.map +1 -0
  18. package/dist/esm/reconstruct.d.ts +79 -0
  19. package/dist/esm/reconstruct.js +75 -0
  20. package/dist/esm/reconstruct.js.map +1 -0
  21. package/dist/esm/retrieve.d.ts +40 -0
  22. package/dist/esm/retrieve.js +54 -0
  23. package/dist/esm/retrieve.js.map +1 -0
  24. package/dist/esm/testkit/conformance.d.ts +33 -0
  25. package/dist/esm/testkit/conformance.js +997 -0
  26. package/dist/esm/testkit/conformance.js.map +1 -0
  27. package/dist/esm/types.d.ts +554 -0
  28. package/dist/esm/types.js +103 -0
  29. package/dist/esm/types.js.map +1 -0
  30. package/package.json +71 -0
  31. package/skills/ai-persistence/SKILL.md +218 -0
  32. package/skills/ai-persistence/build-cloudflare-adapter/SKILL.md +313 -0
  33. package/skills/ai-persistence/build-cloudflare-artifact-store/SKILL.md +693 -0
  34. package/skills/ai-persistence/build-custom-adapter/SKILL.md +328 -0
  35. package/skills/ai-persistence/build-drizzle-adapter/SKILL.md +562 -0
  36. package/skills/ai-persistence/build-prisma-adapter/SKILL.md +518 -0
  37. package/skills/ai-persistence/server/SKILL.md +210 -0
  38. package/skills/ai-persistence/stores/SKILL.md +485 -0
  39. package/src/blob-range.ts +101 -0
  40. package/src/capabilities.ts +18 -0
  41. package/src/index.ts +114 -0
  42. package/src/memory.ts +491 -0
  43. package/src/middleware.ts +1795 -0
  44. package/src/reconstruct-generation.ts +244 -0
  45. package/src/reconstruct.ts +149 -0
  46. package/src/retrieve.ts +77 -0
  47. package/src/testkit/conformance.ts +1288 -0
  48. package/src/types.ts +878 -0
@@ -0,0 +1,210 @@
1
+ ---
2
+ name: ai-persistence/server
3
+ description: >
4
+ Server chat state with withPersistence from @tanstack/ai-persistence.
5
+ Authoritative transcript, run lifecycle, durable interrupts/approvals,
6
+ chatParamsFromRequest, reconstructChat, snapshotStreaming. Use when the
7
+ server owns history, multi-device, or durable tool approvals. NOT client
8
+ localStorage (see ai-core/client-persistence in @tanstack/ai) and NOT
9
+ stream reconnect
10
+ alone.
11
+ type: sub-skill
12
+ library: tanstack-ai
13
+ library_version: '0.0.0'
14
+ sources:
15
+ - 'TanStack/ai:docs/persistence/chat-persistence.md'
16
+ - 'TanStack/ai:docs/persistence/overview.md'
17
+ - 'TanStack/ai:docs/persistence/controls.md'
18
+ ---
19
+
20
+ # Server Chat Persistence
21
+
22
+ > Builds on **ai-persistence**. Package: `@tanstack/ai-persistence`.
23
+
24
+ `withPersistence(persistence)` is a `ChatMiddleware` that writes chat **state**
25
+ to a backend: messages, runs, interrupts (optional metadata). It does not
26
+ mutate the chunk stream and does not replace delivery durability.
27
+
28
+ ## Setup
29
+
30
+ ```ts
31
+ import {
32
+ chat,
33
+ chatParamsFromRequest,
34
+ toServerSentEventsResponse,
35
+ } from '@tanstack/ai'
36
+ import { openaiText } from '@tanstack/ai-openai'
37
+ import { withPersistence } from '@tanstack/ai-persistence'
38
+ // Your adapter — see ai-persistence/stores.
39
+ import { persistence } from './persistence'
40
+
41
+ export async function POST(request: Request) {
42
+ const params = await chatParamsFromRequest(request)
43
+ const stream = chat({
44
+ adapter: openaiText('gpt-5.5'),
45
+ messages: params.messages,
46
+ threadId: params.threadId,
47
+ runId: params.runId,
48
+ ...(params.resume ? { resume: params.resume } : {}),
49
+ middleware: [withPersistence(persistence)],
50
+ })
51
+ return toServerSentEventsResponse(stream)
52
+ }
53
+ ```
54
+
55
+ Always pass `threadId` and `runId` from the client (via
56
+ `chatParamsFromRequest` / body helpers). Forward `resume` when the client
57
+ resolves pending interrupts.
58
+
59
+ For dev and tests, `memoryPersistence()` from `@tanstack/ai-persistence` is a
60
+ drop-in backend that implements all four stores in process.
61
+
62
+ ## What each store does
63
+
64
+ | Store | Role | Required? |
65
+ | ------------ | --------------------------------------- | ----------------------------------------- |
66
+ | `messages` | Full model-message transcript load/save | **Yes** for `withPersistence` |
67
+ | `runs` | Run status, timing, usage, errors | Optional; needed for interrupt durability |
68
+ | `interrupts` | Pending/resolved tool approvals & waits | Optional; **requires** `runs` |
69
+ | `metadata` | App-owned namespaced key/value | Optional |
70
+
71
+ Named shapes: `ChatTranscriptPersistence` (floor), `ChatPersistence` (all four).
72
+ **Annotate your factory with one of these**, not with bare `AIPersistence` —
73
+ the unparameterized type is the all-optional bag, and `withPersistence` rejects
74
+ it because `stores.messages` is possibly `undefined`.
75
+
76
+ ## Authoritative-history contract
77
+
78
+ - **Non-empty `messages`** → finish **overwrites** the stored thread with that
79
+ array. Post the **complete** transcript, never a delta.
80
+ - **Empty `messages`** → middleware **loads** the stored thread and continues.
81
+
82
+ ## When state is written
83
+
84
+ | Moment | Writes | Best-effort? |
85
+ | ------------------ | ----------------------------------------------------------------- | -------------------------------- |
86
+ | `onStart` | Pending turn snapshot (user + history) | Yes — failure does not abort |
87
+ | Interrupt boundary | New interrupts, run → `interrupted`, message snapshot | No |
88
+ | `onFinish` | Full transcript **first**, then run → `completed`, commit resumes | No |
89
+ | Stream (optional) | Throttled partial assistant text | Yes if `snapshotStreaming: true` |
90
+ | `onError` | Run → `failed` | Resumes stay pending |
91
+ | `onAbort` | Run → `aborted` — **but only sometimes** (see below) | Resumes stay pending |
92
+
93
+ ```ts
94
+ withPersistence(persistence, {
95
+ snapshotStreaming: true,
96
+ snapshotIntervalMs: 1000, // default
97
+ })
98
+ ```
99
+
100
+ ### `onAbort` writes conditionally, not always
101
+
102
+ A user pressing Stop and a user closing the tab produce the **identical**
103
+ connection close, so `onAbort` can never infer intent from the abort alone.
104
+ It writes:
105
+
106
+ - **`'aborted'`** (terminal, with `finishedAt`) when the abort is an explicit
107
+ cancel — `info.cancelRequested === true`, or a durable cancel request found
108
+ via `wasCancelRequested(runs, runId)` (both from `@tanstack/ai`; paired with
109
+ `requestRunCancel`/`RUN_CANCEL_REASON`) — **or** when the run is not
110
+ detachable at all (no sandbox/journal behind it, so there is nothing to
111
+ reattach to).
112
+ - **Nothing** when it is a plain disconnect on a **detachable** run (some
113
+ other middleware, e.g. `@tanstack/ai-sandbox`, has provided
114
+ `DetachableRunCapability` from `@tanstack/ai`). The record deliberately
115
+ stays `'running'` — the agent keeps running and a later attach can take it
116
+ over. (The detaching middleware, not `withPersistence`, is what stamps
117
+ `detachedSince`.)
118
+
119
+ Chat's `onAbort` and generation's `onAbort` (`withGenerationPersistence`) are
120
+ **asymmetric on purpose**: a generation job has no journal and no agent loop
121
+ to reattach to, so its `onAbort` always writes `'aborted'` unconditionally.
122
+ Do not "fix" that asymmetry by making generation conditional, or chat
123
+ unconditional — both are correct for what they wrap.
124
+
125
+ Never build a client, or a persistence backend, that assumes a disconnect
126
+ always finalizes the run — for a detachable run it usually does not, and
127
+ inventing a `finishedAt` for a still-`'running'` record breaks takeover.
128
+ Use `isTerminalRunStatus(status)` (from `@tanstack/ai-persistence`) to test
129
+ whether a status is finished, rather than re-listing
130
+ `'completed' | 'failed' | 'aborted'` by hand.
131
+
132
+ Streaming snapshots default **off** (finish is authoritative). Enable only when
133
+ partial-output durability is worth extra writes.
134
+
135
+ Resumes accepted in `onConfig` commit only at a success boundary (interrupt or
136
+ finish). A failed run leaves interrupts pending so the same resume batch can
137
+ retry.
138
+
139
+ ## Interrupt / resume flow
140
+
141
+ 1. Middleware records pending interrupts and **gates** new input: if pending
142
+ exist, the request must include a matching `resume` batch or `onConfig`
143
+ throws.
144
+ 2. On valid resume, middleware builds `resumeToolState` and clears
145
+ `config.resume` so the engine does not double-reconstruct from client
146
+ history (server owns transcript).
147
+ 3. On success boundary, interrupts are marked resolved/cancelled.
148
+
149
+ ## Hydrate a thread for the client (`reconstructChat`)
150
+
151
+ Server-authoritative clients load history by `threadId` (often `GET`):
152
+
153
+ ```ts
154
+ import { reconstructChat } from '@tanstack/ai-persistence'
155
+
156
+ export async function GET(request: Request) {
157
+ return reconstructChat(persistence, request, {
158
+ // Multi-user: required in production
159
+ authorize: async (threadId, req) => {
160
+ const userId = await sessionUserId(req)
161
+ return userOwnsThread(userId, threadId)
162
+ },
163
+ })
164
+ }
165
+ ```
166
+
167
+ Returns `{ messages, activeRun, interrupts }`:
168
+
169
+ - `messages` — UI messages for paint
170
+ - `activeRun` — `{ runId }` if a run is still generating (`runs.findActiveRun`)
171
+ - `interrupts` — pending human-in-the-loop state for re-prompt
172
+
173
+ **Without `authorize`, anyone who guesses `?threadId=` gets the transcript.**
174
+
175
+ ## Generation activities
176
+
177
+ `withGenerationPersistence(persistence)` tracks run records for non-chat
178
+ activities (image, audio, TTS, video, transcription). Do not fake
179
+ `threadId = requestId` on chat run stores — use the generation helper.
180
+
181
+ ## Common mistakes
182
+
183
+ ### CRITICAL: Posting a message delta as `messages`
184
+
185
+ Wipes the stored thread down to that delta. Always send full history or `[]`.
186
+
187
+ ### HIGH: Omitting `threadId` / `runId`
188
+
189
+ Persistence keys and resume need stable ids. Use `chatParamsFromRequest`.
190
+
191
+ ### HIGH: Interrupts without `runs`
192
+
193
+ `interrupts` requires `runs`; `withPersistence` throws otherwise.
194
+
195
+ ### HIGH: Typing a factory as bare `AIPersistence`
196
+
197
+ `AIPersistence` defaults to the sparse all-optional bag, so `withPersistence`
198
+ and `reconstructChat` reject the value. Return `ChatPersistence` (or
199
+ `ChatTranscriptPersistence`) instead.
200
+
201
+ ### MEDIUM: Expecting `withPersistence` to reconnect a dropped stream
202
+
203
+ That is delivery durability (resumable streams), not state persistence.
204
+
205
+ ## Cross-references
206
+
207
+ - **ai-persistence** — layers and recommended stack
208
+ - **ai-persistence/stores** — implement the store interfaces
209
+ - **ai-core/client-persistence** (`@tanstack/ai`) — browser half
210
+ - **ai-core/locks** — multi-instance coordination