UNPKG

@tanstack/ai-persistence

Version:

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

80 lines (79 loc) 3.71 kB
import { UIMessage } from '@tanstack/ai'; import { AIPersistence, ChatTranscriptStores } from './types.js'; /** * The JSON body `reconstructChat` returns and a server-authoritative client * hydrates from on mount. * * `messages` is the stored transcript as UI messages (ready to paint). * `activeRun` is a cursor to a run still generating for the thread, or `null` — * resolved from the STABLE thread id via `stores.runs.findActiveRun`, so the * client learns "there is a live run to tail" without ever handling a run id. * `interrupts` is the thread's pending human-in-the-loop interrupts (tool * approvals, client-tool/generic waits) and the run they paused, or `null` — * so a reload (or another device) re-prompts the approval from the SERVER, not * from client storage. Resolved via `stores.interrupts.listPending`. */ export interface ReconstructedChat { messages: Array<UIMessage>; activeRun: { runId: string; } | null; interrupts: { runId: string; pending: Array<Record<string, unknown>>; } | null; } export interface ReconstructChatOptions { /** Query parameter carrying the thread id. Defaults to `threadId`. */ param?: string; /** * Authorize access to the requested thread before loading history. * * ⚠️ Without this, any caller who knows or guesses `?threadId=` receives the * full transcript. Multi-user / multi-tenant deployments **must** supply * an authorization check (session → owned threads) or resolve a validated * thread id in the route and pass it via a custom `param` that only your * server sets. * * Return: * - `true` to allow the load * - `false` for a default `403` response * - a `Response` to return as-is (e.g. `401` with a body) */ authorize?: (threadId: string, request: Request) => boolean | Response | Promise<boolean | Response>; } /** * 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)) * }, * }) * } * ``` */ export declare function reconstructChat(persistence: AIPersistence<ChatTranscriptStores>, request: Request, options?: ReconstructChatOptions): Promise<Response>;