UNPKG

@tanstack/ai-persistence

Version:

Composable state persistence for TanStack AI messages, runs, interrupts, metadata, and locks.

75 lines (74 loc) 3.21 kB
import { validateReconstructChatStores } from "./types.js"; import { modelMessagesToUIMessages } from "@tanstack/ai"; //#region src/reconstruct.ts /** * Build the JSON `Response` a server-authoritative client hydrates from on load * (see the client-persistence guide). Reads the thread id from the request query * (`?threadId=` by default) and returns `{ messages, activeRun, interrupts }` * ({@link ReconstructedChat}): * * - `messages` — the stored transcript as UI messages. * - `activeRun` — `{ runId }` if a run is still generating for the thread (so the * client tails it via the durability stream), else `null`. Resolved via the * required `stores.runs.findActiveRun`; `null` when the `runs` store is absent. * - `interrupts` — `{ runId, pending }` if the thread has pending human-in-the-loop * interrupts (a paused approval / wait) and the run they paused, else `null`, so * a reload re-prompts the decision from the server. Resolved via the optional * `stores.interrupts.listPending`; `null` when that store is absent. * * Requires `stores.messages`. Returns an empty transcript with no active run * and no interrupts when the thread id is missing or the thread is unknown, so * the caller never has to special-case a first load. * * This helper does **not** enforce tenancy by itself. Pass * {@link ReconstructChatOptions.authorize} (or wrap the call in your own * session gate) before exposing it on a public route. * * ```ts * export async function GET(request: Request) { * return reconstructChat(persistence, request, { * authorize: async (threadId, req) => { * const userId = await getSessionUserId(req) * return userId != null && (await userOwnsThread(userId, threadId)) * }, * }) * } * ``` */ async function reconstructChat(persistence, request, options) { validateReconstructChatStores(persistence); const messageStore = persistence.stores.messages; if (!messageStore) throw new Error("reconstructChat requires stores.messages."); const param = options?.param ?? "threadId"; const threadId = new URL(request.url).searchParams.get(param) ?? ""; if (threadId && options?.authorize) { const decision = await options.authorize(threadId, request); if (decision instanceof Response) return decision; if (!decision) return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403, headers: { "content-type": "application/json", "cache-control": "no-store" } }); } const active = threadId ? await persistence.stores.runs?.findActiveRun(threadId) : null; const stored = threadId ? await messageStore.loadThread(threadId) : []; const pending = threadId ? await persistence.stores.interrupts?.listPending(threadId) ?? [] : []; const firstPending = pending[0]; const body = { messages: modelMessagesToUIMessages(stored), activeRun: active ? { runId: active.runId } : null, interrupts: firstPending ? { runId: firstPending.runId, pending: pending.map((record) => record.payload) } : null }; return new Response(JSON.stringify(body), { headers: { "content-type": "application/json", "cache-control": "no-store" } }); } //#endregion export { reconstructChat }; //# sourceMappingURL=reconstruct.js.map