@tanstack/ai-persistence 0.5.6 → 0.6.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 +1 -1
- package/dist/esm/memory.js +1 -1
- package/dist/esm/memory.js.map +1 -1
- package/dist/esm/middleware.d.ts +18 -20
- package/dist/esm/middleware.js +65 -4
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/reconstruct.d.ts +14 -0
- package/dist/esm/reconstruct.js +89 -4
- package/dist/esm/reconstruct.js.map +1 -1
- package/dist/esm/testkit/conformance.js +3 -1
- package/dist/esm/testkit/conformance.js.map +1 -1
- package/dist/esm/types.d.ts +36 -2
- package/dist/esm/types.js.map +1 -1
- package/package.json +3 -3
- package/skills/ai-persistence/build-cloudflare-artifact-store/SKILL.md +11 -0
- package/skills/ai-persistence/build-custom-adapter/SKILL.md +2 -2
- package/skills/ai-persistence/build-drizzle-adapter/SKILL.md +1 -1
- package/skills/ai-persistence/build-prisma-adapter/SKILL.md +1 -1
- package/skills/ai-persistence/server/SKILL.md +27 -11
- package/skills/ai-persistence/stores/SKILL.md +60 -25
- package/src/index.ts +1 -0
- package/src/memory.ts +4 -1
- package/src/middleware.ts +102 -27
- package/src/reconstruct.ts +136 -5
- package/src/testkit/conformance.ts +5 -1
- package/src/types.ts +39 -2
package/src/middleware.ts
CHANGED
|
@@ -47,6 +47,7 @@ import type {
|
|
|
47
47
|
GenerationMiddleware,
|
|
48
48
|
GenerationMiddlewareContext,
|
|
49
49
|
Interrupt,
|
|
50
|
+
ModelMessage,
|
|
50
51
|
PendingInterruptResumeRecord,
|
|
51
52
|
PersistedArtifactActivity,
|
|
52
53
|
PersistedArtifactRef,
|
|
@@ -66,6 +67,7 @@ import type {
|
|
|
66
67
|
ChatTranscriptStores,
|
|
67
68
|
InterruptCommitEntry,
|
|
68
69
|
InterruptRecord,
|
|
70
|
+
MessagePage,
|
|
69
71
|
RunStore,
|
|
70
72
|
} from './types'
|
|
71
73
|
import { artifactBlobKey } from './retrieve'
|
|
@@ -1907,26 +1909,78 @@ function detachableRun(ctx: ChatMiddlewareContext): boolean {
|
|
|
1907
1909
|
// Chat middleware
|
|
1908
1910
|
// ---------------------------------------------------------------------------
|
|
1909
1911
|
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1912
|
+
function threadMessages(
|
|
1913
|
+
loaded: Array<ModelMessage> | MessagePage,
|
|
1914
|
+
): Array<ModelMessage> {
|
|
1915
|
+
return Array.isArray(loaded) ? loaded : loaded.messages
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Empty incoming keeps stored. Non-empty: the last incoming id that already
|
|
1919
|
+
// exists in stored is a cutoff (reload drops the old assistant after that
|
|
1920
|
+
// user). Same id is replaced in place. New ids and messages with no id are
|
|
1921
|
+
// appended.
|
|
1922
|
+
function mergeStoredMessages(
|
|
1923
|
+
stored: ReadonlyArray<ModelMessage>,
|
|
1924
|
+
incoming: ReadonlyArray<ModelMessage>,
|
|
1925
|
+
) {
|
|
1926
|
+
if (incoming.length === 0) {
|
|
1927
|
+
return stored.slice()
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
let cutoff = stored.length
|
|
1931
|
+
for (let index = incoming.length - 1; index >= 0; index--) {
|
|
1932
|
+
const id = incoming[index]?.id
|
|
1933
|
+
if (id === undefined) continue
|
|
1934
|
+
const storedIndex = stored.findIndex((message) => message.id === id)
|
|
1935
|
+
if (storedIndex >= 0) {
|
|
1936
|
+
cutoff = storedIndex + 1
|
|
1937
|
+
break
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
const prefix = stored.slice(0, cutoff)
|
|
1941
|
+
|
|
1942
|
+
const incomingById = new Map<string, ModelMessage>()
|
|
1943
|
+
for (const message of incoming) {
|
|
1944
|
+
const id = message.id
|
|
1945
|
+
if (id) incomingById.set(id, message)
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
const storedIds = new Set<string>()
|
|
1949
|
+
const merged: Array<ModelMessage> = []
|
|
1950
|
+
for (const message of prefix) {
|
|
1951
|
+
const id = message.id
|
|
1952
|
+
if (id) {
|
|
1953
|
+
storedIds.add(id)
|
|
1954
|
+
merged.push(incomingById.get(id) ?? message)
|
|
1955
|
+
continue
|
|
1956
|
+
}
|
|
1957
|
+
merged.push(message)
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
for (let index = 0; index < incoming.length; index++) {
|
|
1961
|
+
const message = incoming[index]
|
|
1962
|
+
if (!message) continue
|
|
1963
|
+
const id = message.id
|
|
1964
|
+
if (id && storedIds.has(id)) continue
|
|
1965
|
+
// Attach/reload can post the stored transcript again with no ids. Keep the
|
|
1966
|
+
// prefix row instead of appending a second copy of the same turn.
|
|
1967
|
+
if (!id) {
|
|
1968
|
+
const existing = merged[index]
|
|
1969
|
+
if (
|
|
1970
|
+
existing &&
|
|
1971
|
+
existing.id === undefined &&
|
|
1972
|
+
existing.role === message.role &&
|
|
1973
|
+
existing.content === message.content
|
|
1974
|
+
) {
|
|
1975
|
+
continue
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
merged.push(message)
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
return merged
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1930
1984
|
export interface WithPersistenceOptions {
|
|
1931
1985
|
/**
|
|
1932
1986
|
* Also persist a throttled snapshot of the in-progress assistant reply while
|
|
@@ -1945,6 +1999,24 @@ export interface WithPersistenceOptions {
|
|
|
1945
1999
|
}
|
|
1946
2000
|
|
|
1947
2001
|
/**
|
|
2002
|
+
* Chat-only **state** persistence middleware. Provides durable transcript,
|
|
2003
|
+
* run records, and interrupts for `chat()`. Does **not** provide locks —
|
|
2004
|
+
* use `withLocks` from `@tanstack/ai` for multi-instance coordination.
|
|
2005
|
+
*
|
|
2006
|
+
* This middleware never mutates the chunk stream; delivery durability
|
|
2007
|
+
* (replaying a disconnected/reloaded stream) is a separate transport-layer
|
|
2008
|
+
* concern (see the resumable-streams docs).
|
|
2009
|
+
*
|
|
2010
|
+
* Requires `stores.messages`. When `stores.interrupts` is present,
|
|
2011
|
+
* `stores.runs` is also required.
|
|
2012
|
+
*
|
|
2013
|
+
* Incoming `messages` merge into the stored thread by id. An empty list loads
|
|
2014
|
+
* the stored thread. The last incoming id that already exists in stored is a
|
|
2015
|
+
* cutoff; stored messages after it are dropped (reload). If no incoming id is
|
|
2016
|
+
* in stored, every stored message stays. Same id: incoming wins. New ids and
|
|
2017
|
+
* messages with no id are appended. `saveThread` still replaces the thread
|
|
2018
|
+
* with that merged list.
|
|
2019
|
+
*
|
|
1948
2020
|
* @param persistence - Must satisfy {@link ChatTranscriptStores} (messages
|
|
1949
2021
|
* required). Known-absent `messages` or `interrupts` without `runs` fail at
|
|
1950
2022
|
* compile time; fully dynamic bags are checked at runtime.
|
|
@@ -2019,11 +2091,12 @@ export function withPersistence<TStores extends ChatTranscriptStores>(
|
|
|
2019
2091
|
// it behaves exactly as before. See `PendingTurnCapability`.
|
|
2020
2092
|
providePendingTurn(ctx, {
|
|
2021
2093
|
snapshot: async () => {
|
|
2022
|
-
const stored =
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
//
|
|
2026
|
-
|
|
2094
|
+
const stored = threadMessages(
|
|
2095
|
+
await messageStore.loadThread(ctx.threadId),
|
|
2096
|
+
)
|
|
2097
|
+
// Same merge as onConfig. saveThread replaces the thread, so a short
|
|
2098
|
+
// incoming list must not drop stored extras.
|
|
2099
|
+
const list = mergeStoredMessages(stored, ctx.messages)
|
|
2027
2100
|
await messageStore.saveThread(ctx.threadId, list)
|
|
2028
2101
|
},
|
|
2029
2102
|
})
|
|
@@ -2088,8 +2161,10 @@ export function withPersistence<TStores extends ChatTranscriptStores>(
|
|
|
2088
2161
|
if (state && storedUsage) state.usage = storedUsage
|
|
2089
2162
|
if (!state?.merged) {
|
|
2090
2163
|
if (state) state.merged = true
|
|
2091
|
-
const stored =
|
|
2092
|
-
|
|
2164
|
+
const stored = threadMessages(
|
|
2165
|
+
await messageStore.loadThread(ctx.threadId),
|
|
2166
|
+
)
|
|
2167
|
+
patch.messages = mergeStoredMessages(stored, config.messages)
|
|
2093
2168
|
}
|
|
2094
2169
|
|
|
2095
2170
|
return Object.keys(patch).length > 0 ? patch : undefined
|
package/src/reconstruct.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { modelMessagesToUIMessages } from '@tanstack/ai'
|
|
2
|
-
import type { UIMessage } from '@tanstack/ai'
|
|
2
|
+
import type { ModelMessage, UIMessage } from '@tanstack/ai'
|
|
3
3
|
import { validateReconstructChatStores } from './types'
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AIPersistence,
|
|
6
|
+
ChatTranscriptStores,
|
|
7
|
+
MessagePage,
|
|
8
|
+
MessageStore,
|
|
9
|
+
} from './types'
|
|
10
|
+
|
|
11
|
+
const MAX_PAGE_SIZE = 500
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* The JSON body `reconstructChat` returns and a server-authoritative client
|
|
@@ -15,6 +22,9 @@ import type { AIPersistence, ChatTranscriptStores } from './types'
|
|
|
15
22
|
* approvals, client-tool/generic waits) and the run they paused, or `null` —
|
|
16
23
|
* so a reload (or another device) re-prompts the approval from the SERVER, not
|
|
17
24
|
* from client storage. Resolved via `stores.interrupts.listPending`.
|
|
25
|
+
* `page` is set only when the GET included a valid `limit`. `truncated` is true
|
|
26
|
+
* when older UI messages exist. `cursor` is the opaque `before` token for the
|
|
27
|
+
* next older window.
|
|
18
28
|
*/
|
|
19
29
|
export interface ReconstructedChat {
|
|
20
30
|
messages: Array<UIMessage>
|
|
@@ -23,6 +33,7 @@ export interface ReconstructedChat {
|
|
|
23
33
|
runId: string
|
|
24
34
|
pending: Array<Record<string, unknown>>
|
|
25
35
|
} | null
|
|
36
|
+
page?: { truncated: false } | { truncated: true; cursor: string }
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
export interface ReconstructChatOptions {
|
|
@@ -63,6 +74,11 @@ export interface ReconstructChatOptions {
|
|
|
63
74
|
* a reload re-prompts the decision from the server. Resolved via the optional
|
|
64
75
|
* `stores.interrupts.listPending`; `null` when that store is absent.
|
|
65
76
|
*
|
|
77
|
+
* Paging is opt-in. A valid `?limit=` (positive integer, capped at 500) returns
|
|
78
|
+
* the newest window of UI messages plus `page`. `?before=` walks to an older
|
|
79
|
+
* window. Invalid `limit` (`0`, negative, NaN) is ignored and the full
|
|
80
|
+
* transcript is returned. `activeRun` and `interrupts` are never paged.
|
|
81
|
+
*
|
|
66
82
|
* Requires `stores.messages`. Returns an empty transcript with no active run
|
|
67
83
|
* and no interrupts when the thread id is missing or the thread is unknown, so
|
|
68
84
|
* the caller never has to special-case a first load.
|
|
@@ -94,8 +110,11 @@ export async function reconstructChat(
|
|
|
94
110
|
throw new Error('reconstructChat requires stores.messages.')
|
|
95
111
|
}
|
|
96
112
|
|
|
113
|
+
const requestUrl = new URL(request.url)
|
|
97
114
|
const param = options?.param ?? 'threadId'
|
|
98
|
-
const threadId =
|
|
115
|
+
const threadId = requestUrl.searchParams.get(param) ?? ''
|
|
116
|
+
const pageSize = parsePageSize(requestUrl.searchParams.get('limit'))
|
|
117
|
+
const before = parseBefore(requestUrl.searchParams.get('before'))
|
|
99
118
|
|
|
100
119
|
if (threadId && options?.authorize) {
|
|
101
120
|
const decision = await options.authorize(threadId, request)
|
|
@@ -122,7 +141,15 @@ export async function reconstructChat(
|
|
|
122
141
|
const active = threadId
|
|
123
142
|
? await persistence.stores.runs?.findActiveRun(threadId)
|
|
124
143
|
: null
|
|
125
|
-
const stored =
|
|
144
|
+
const stored =
|
|
145
|
+
threadId === ''
|
|
146
|
+
? []
|
|
147
|
+
: pageSize === undefined
|
|
148
|
+
? await messageStore.loadThread(threadId)
|
|
149
|
+
: await messageStore.loadThread(threadId, {
|
|
150
|
+
limit: pageSize + 1,
|
|
151
|
+
...(before === undefined ? {} : { before }),
|
|
152
|
+
})
|
|
126
153
|
// Pending interrupts for the thread, so a reload re-prompts the approval from
|
|
127
154
|
// the server. Each stored `payload` is the full interrupt descriptor the
|
|
128
155
|
// client hydrates; they share the run they paused.
|
|
@@ -130,8 +157,22 @@ export async function reconstructChat(
|
|
|
130
157
|
? ((await persistence.stores.interrupts?.listPending(threadId)) ?? [])
|
|
131
158
|
: []
|
|
132
159
|
const firstPending = pending[0]
|
|
160
|
+
const isPaging = pageSize !== undefined && threadId !== ''
|
|
161
|
+
const transcript = !isPaging
|
|
162
|
+
? {
|
|
163
|
+
messages: modelMessagesToUIMessages(threadMessages(stored)),
|
|
164
|
+
}
|
|
165
|
+
: Array.isArray(stored)
|
|
166
|
+
? await windowFromArray({
|
|
167
|
+
stored,
|
|
168
|
+
messageStore,
|
|
169
|
+
threadId,
|
|
170
|
+
pageSize,
|
|
171
|
+
before,
|
|
172
|
+
})
|
|
173
|
+
: windowFromMessagePage(stored, pageSize)
|
|
133
174
|
const body: ReconstructedChat = {
|
|
134
|
-
messages:
|
|
175
|
+
messages: transcript.messages,
|
|
135
176
|
activeRun: active ? { runId: active.runId } : null,
|
|
136
177
|
interrupts: firstPending
|
|
137
178
|
? {
|
|
@@ -139,6 +180,7 @@ export async function reconstructChat(
|
|
|
139
180
|
pending: pending.map((record) => record.payload),
|
|
140
181
|
}
|
|
141
182
|
: null,
|
|
183
|
+
...('page' in transcript ? { page: transcript.page } : {}),
|
|
142
184
|
}
|
|
143
185
|
return new Response(JSON.stringify(body), {
|
|
144
186
|
headers: {
|
|
@@ -147,3 +189,92 @@ export async function reconstructChat(
|
|
|
147
189
|
},
|
|
148
190
|
})
|
|
149
191
|
}
|
|
192
|
+
|
|
193
|
+
function parsePageSize(raw: string | null) {
|
|
194
|
+
if (raw == null) return
|
|
195
|
+
const pageSize = Number(raw)
|
|
196
|
+
const isValidPageSize = Number.isInteger(pageSize) && pageSize > 0
|
|
197
|
+
if (!isValidPageSize) return
|
|
198
|
+
return Math.min(pageSize, MAX_PAGE_SIZE)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseBefore(raw: string | null) {
|
|
202
|
+
if (raw == null || raw === '') return
|
|
203
|
+
return raw
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function threadMessages(
|
|
207
|
+
loaded: Array<ModelMessage> | MessagePage,
|
|
208
|
+
): Array<ModelMessage> {
|
|
209
|
+
return Array.isArray(loaded) ? loaded : loaded.messages
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function completePage() {
|
|
213
|
+
return { truncated: false as const }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function truncatedPage(cursor: string) {
|
|
217
|
+
return { truncated: true as const, cursor }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function pageFromCursor(cursor: string | undefined) {
|
|
221
|
+
if (cursor === undefined || cursor === '') {
|
|
222
|
+
return completePage()
|
|
223
|
+
}
|
|
224
|
+
return truncatedPage(cursor)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function newestUiWindow(messages: Array<UIMessage>, pageSize: number) {
|
|
228
|
+
const truncated = messages.length > pageSize
|
|
229
|
+
if (!truncated) {
|
|
230
|
+
return { messages, page: completePage() }
|
|
231
|
+
}
|
|
232
|
+
const uiWindow = messages.slice(messages.length - pageSize)
|
|
233
|
+
return {
|
|
234
|
+
messages: uiWindow,
|
|
235
|
+
page: pageFromCursor(uiWindow[0]?.id),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function uiBeforeCursor(messages: Array<UIMessage>, cursor: string) {
|
|
240
|
+
const cut = messages.findIndex((message) => message.id === cursor)
|
|
241
|
+
if (cut === -1) return
|
|
242
|
+
return messages.slice(0, cut)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function windowFromMessagePage(page: MessagePage, pageSize: number) {
|
|
246
|
+
const ui = modelMessagesToUIMessages(page.messages)
|
|
247
|
+
if (ui.length > pageSize) {
|
|
248
|
+
// Extra slice uses a library-minted cursor. Keeping the adapter cursor
|
|
249
|
+
// after dropping the oldest row would skip that row on the next GET.
|
|
250
|
+
return newestUiWindow(ui, pageSize)
|
|
251
|
+
}
|
|
252
|
+
if (page.truncated) {
|
|
253
|
+
return {
|
|
254
|
+
messages: ui,
|
|
255
|
+
page: pageFromCursor(page.cursor),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { messages: ui, page: completePage() }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function windowFromArray(input: {
|
|
262
|
+
stored: Array<ModelMessage>
|
|
263
|
+
messageStore: MessageStore
|
|
264
|
+
threadId: string
|
|
265
|
+
pageSize: number
|
|
266
|
+
before: string | undefined
|
|
267
|
+
}) {
|
|
268
|
+
const { stored, messageStore, threadId, pageSize, before } = input
|
|
269
|
+
if (before === undefined) {
|
|
270
|
+
return newestUiWindow(modelMessagesToUIMessages(stored), pageSize)
|
|
271
|
+
}
|
|
272
|
+
// Array adapters own no cursor. Apply `before` to the full transcript so an
|
|
273
|
+
// adapter that ignored the hint cannot return the same newest page forever.
|
|
274
|
+
const full = threadMessages(await messageStore.loadThread(threadId))
|
|
275
|
+
const older = uiBeforeCursor(modelMessagesToUIMessages(full), before)
|
|
276
|
+
if (older === undefined) {
|
|
277
|
+
return { messages: [], page: truncatedPage(before) }
|
|
278
|
+
}
|
|
279
|
+
return newestUiWindow(older, pageSize)
|
|
280
|
+
}
|
|
@@ -171,6 +171,8 @@ export function runPersistenceConformance(
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
describe('messages', () => {
|
|
174
|
+
// One-argument loadThread is the full-thread contract. Paging
|
|
175
|
+
// (`limit` / `before`) is an optional hint; this suite does not require it.
|
|
174
176
|
it('round-trips a thread and returns [] for unknown threads', async (ctx) => {
|
|
175
177
|
const store = resolveStore('messages')
|
|
176
178
|
if (!store) return ctx.skip('store not provided')
|
|
@@ -181,7 +183,9 @@ export function runPersistenceConformance(
|
|
|
181
183
|
{ role: 'user', content: 'hi' },
|
|
182
184
|
{ role: 'assistant', content: 'hello' },
|
|
183
185
|
])
|
|
184
|
-
|
|
186
|
+
const loaded = await store.loadThread('thread-msg')
|
|
187
|
+
expect(Array.isArray(loaded)).toBe(true)
|
|
188
|
+
expect(loaded).toEqual([
|
|
185
189
|
{ role: 'user', content: 'hi' },
|
|
186
190
|
{ role: 'assistant', content: 'hello' },
|
|
187
191
|
])
|
package/src/types.ts
CHANGED
|
@@ -53,6 +53,29 @@ export type { MetadataStore, Scope }
|
|
|
53
53
|
// **ISO-8601 strings**. The middleware performs the number→ISO conversion at
|
|
54
54
|
// the boundary; do not mix the two on a single field.
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* One page of a thread from {@link MessageStore.loadThread} when the caller
|
|
58
|
+
* passed a paging hint.
|
|
59
|
+
*
|
|
60
|
+
* Middleware omits the hint and always gets a full `Array<ModelMessage>`,
|
|
61
|
+
* never this shape.
|
|
62
|
+
*
|
|
63
|
+
* `truncated: true` requires `cursor`. Without a cursor the client cannot
|
|
64
|
+
* request the next older window, so `reconstructChat` treats that page as
|
|
65
|
+
* complete.
|
|
66
|
+
*/
|
|
67
|
+
export type MessagePage =
|
|
68
|
+
| {
|
|
69
|
+
messages: Array<ModelMessage>
|
|
70
|
+
truncated: false
|
|
71
|
+
cursor?: never
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
messages: Array<ModelMessage>
|
|
75
|
+
truncated: true
|
|
76
|
+
cursor: string
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
/**
|
|
57
80
|
* Durable store for a thread's full message transcript.
|
|
58
81
|
*
|
|
@@ -70,13 +93,27 @@ export type { MetadataStore, Scope }
|
|
|
70
93
|
*/
|
|
71
94
|
export interface MessageStore {
|
|
72
95
|
/**
|
|
73
|
-
* Return the
|
|
96
|
+
* Return the stored transcript for `threadId` ({@link Scope.threadId}),
|
|
74
97
|
* in insertion order.
|
|
75
98
|
*
|
|
99
|
+
* Call with only `threadId` (middleware, `onStart`, `onFinish`) and this
|
|
100
|
+
* MUST return the full transcript as an `Array<ModelMessage>`. Never a
|
|
101
|
+
* {@link MessagePage}.
|
|
102
|
+
*
|
|
103
|
+
* `options.limit` and `options.before` are an optional paging hint for
|
|
104
|
+
* hydrate. Adapters may ignore them and still return the full array. An
|
|
105
|
+
* adapter that pages returns a {@link MessagePage}.
|
|
106
|
+
*
|
|
76
107
|
* INVARIANT: returns an empty array (never `null`/`undefined`) for a thread
|
|
77
108
|
* that was never saved. Callers treat `[]` as "no history".
|
|
78
109
|
*/
|
|
79
|
-
loadThread:
|
|
110
|
+
loadThread: {
|
|
111
|
+
(threadId: string): Promise<Array<ModelMessage>>
|
|
112
|
+
(
|
|
113
|
+
threadId: string,
|
|
114
|
+
options: { limit?: number; before?: string },
|
|
115
|
+
): Promise<Array<ModelMessage> | MessagePage>
|
|
116
|
+
}
|
|
80
117
|
/**
|
|
81
118
|
* Overwrite the stored transcript for `threadId` with `messages`.
|
|
82
119
|
*
|