UNPKG

@tanstack/ai-persistence

Version:

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

89 lines (80 loc) • 2.76 kB
import type { ModelMessage } from '@tanstack/ai' // Empty incoming keeps stored. Non-empty: the last incoming id that already // exists in stored is a cutoff (reload drops the old assistant after that // user). Same id is replaced in place, but a stored mid-conversation record // stays when the incoming copy has none. New ids and messages with no id are // appended. /** How many stored messages the merge keeps. Less than `stored.length` on a reload. */ export function storedCutoff( stored: ReadonlyArray<ModelMessage>, incoming: ReadonlyArray<ModelMessage>, ) { for (let index = incoming.length - 1; index >= 0; index--) { const id = incoming[index]?.id if (id === undefined) continue const storedIndex = stored.findIndex((message) => message.id === id) if (storedIndex >= 0) return storedIndex + 1 } return stored.length } export function mergeStoredMessages( stored: ReadonlyArray<ModelMessage>, incoming: ReadonlyArray<ModelMessage>, ) { if (incoming.length === 0) { return stored.slice() } const prefix = stored.slice(0, storedCutoff(stored, incoming)) const incomingById = new Map<string, ModelMessage>() for (const message of incoming) { const id = message.id if (id) incomingById.set(id, message) } const storedIds = new Set<string>() const merged: Array<ModelMessage> = [] for (const message of prefix) { const id = message.id if (id) { storedIds.add(id) const replacement = incomingById.get(id) merged.push( replacement ? keepStoredRecord(replacement, message) : message, ) continue } merged.push(message) } for (let index = 0; index < incoming.length; index++) { const message = incoming[index] if (!message) continue const id = message.id if (id && storedIds.has(id)) continue // Attach/reload can post the stored transcript again with no ids. Keep the // prefix row instead of appending a second copy of the same turn. if (!id) { const existing = merged[index] if ( existing && existing.id === undefined && existing.role === message.role && existing.content === message.content ) { continue } } merged.push(message) } return merged } // A client never sends the engine's mid-conversation record (UIMessages have // none), so keep the stored one. Without it, each turn starts a new start // point and the prompt cache breaks once per turn. function keepStoredRecord( incoming: ModelMessage, stored: ModelMessage, ): ModelMessage { if (incoming.midConversationChange || !stored.midConversationChange) { return incoming } return { ...incoming, midConversationChange: stored.midConversationChange } }