UNPKG

@tanstack/ai

Version:

Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.

748 lines (747 loc) 27.6 kB
import { tanstackMetadata } from "../../utilities/merge-metadata.js"; import { isContentPartArray, normalizeToolResult } from "../../utilities/tool-result.js"; import { isProviderExecutedToolCall } from "../../utilities/provider-executed.js"; //#region src/activities/chat/messages.ts function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } /** * Check if a MessagePart is a content part (text, image, audio, video, document) * that maps directly to a ModelMessage ContentPart. */ function isContentPart(part) { return part.type === "text" || part.type === "image" || part.type === "audio" || part.type === "video" || part.type === "document"; } function safeJsonStringify(value) { try { return JSON.stringify(value) ?? ""; } catch { return ""; } } function nonEmptyString(value) { return typeof value === "string" && value !== "" ? value : void 0; } function encryptedValueFrom(value) { if ("encryptedValue" in value) { const fromSpec = nonEmptyString(value.encryptedValue); if (fromSpec !== void 0) return fromSpec; } return nonEmptyString(tanstackMetadata(value)?.signature); } function toolCallFromWire(toolCall, bag) { const fromBag = bag != null && typeof bag === "object" && !Array.isArray(bag) ? bag : void 0; const encrypted = encryptedValueFrom(toolCall); if (bag === null && encrypted === void 0) return { ...toolCall, metadata: null }; if (fromBag === void 0 && encrypted === void 0) return toolCall; return { ...toolCall, metadata: { ...fromBag ?? {}, ...encrypted !== void 0 ? { thoughtSignature: encrypted } : {} } }; } function parseToolResultContent(content) { try { return JSON.parse(content); } catch { return content; } } /** * Collapse an array of ContentParts into the most compact ModelMessage content: * - Empty array → null * - All text parts → joined string (or null if empty) * - Mixed content → ContentPart array as-is */ function collapseContentParts(parts) { if (parts.length === 0) return null; if (parts.every((p) => p.type === "text")) return parts.map((p) => p.content).join("") || null; return parts; } /** * Extract text content from ModelMessage content (string, null, or ContentPart array). * Used when only the text portion is needed (e.g., tool result content). */ function getTextContent(content) { if (content === null || content === void 0) return ""; if (typeof content === "string") return content; return content.filter((part) => part.type === "text").map((part) => part.content).join(""); } function toolResultContent(content) { return Array.isArray(content) ? content : getTextContent(content); } /** * Convert UIMessages or ModelMessages to ModelMessages */ function convertMessagesToModelMessages(messages) { const anchoredToolCallIds = /* @__PURE__ */ new Set(); for (const msg of messages) if ("parts" in msg) { for (const part of msg.parts) if (part.type === "tool-result") anchoredToolCallIds.add(part.toolCallId); } const modelMessages = []; let pendingThinking = []; for (const msg of messages) { if ("parts" in msg) { modelMessages.push(...uiMessageToModelMessages(msg)); continue; } const modelMessage = restoreModelMessageCreatedAt(restoreToolResultOwnership(msg)); const role = modelMessage.role; if (role === "tool" && modelMessage.toolCallId && anchoredToolCallIds.has(modelMessage.toolCallId)) continue; if (role === "reasoning") { const content = msg.content; if (content) { const signature = encryptedValueFrom(msg); pendingThinking.push({ content, ...signature !== void 0 ? { signature } : {} }); } continue; } if (role === "activity") continue; if (role === "developer") { modelMessages.push({ role: "system", content: msg.content, ...optionalName(modelMessage), ...optionalCreatedAt(modelMessage), ...modelMessage.metadata !== void 0 && { metadata: modelMessage.metadata } }); continue; } if (role === "user" && Array.isArray(msg.content)) { const content = msg.content; if (!content.some((part) => part.type === "text" && "text" in part && !("content" in part))) { modelMessages.push(modelMessage); continue; } const contentParts = aguiUserContentToParts(content).filter(isContentPart); modelMessages.push({ role: "user", content: collapseContentParts(contentParts), ...msg.id !== void 0 && { id: msg.id }, ...optionalName(modelMessage), ...optionalCreatedAt(modelMessage), ...modelMessage.metadata !== void 0 && { metadata: modelMessage.metadata } }); continue; } if (role === "assistant") { const source = modelMessage; const toolCallMetadata = tanstackMetadata(msg)?.toolCallMetadata; const toolCalls = source.toolCalls?.map((toolCall) => toolCallFromWire(toolCall, toolCallMetadata?.[toolCall.id])); modelMessages.push({ ...source, ...toolCalls !== void 0 ? { toolCalls } : {}, ...pendingThinking.length > 0 ? { thinking: [...source.thinking ?? [], ...pendingThinking] } : {} }); pendingThinking = []; continue; } modelMessages.push(modelMessage); } return modelMessages; } function restoreModelMessageCreatedAt(message) { const createdAt = coerceCreatedAt(message.createdAt) ?? createdAtFromMetadata(message); if (createdAt === void 0) { if (message.createdAt === void 0) return message; const { createdAt: _invalid, ...rest } = message; return rest; } return Object.is(message.createdAt, createdAt) ? message : { ...message, createdAt }; } function restoreToolResultOwnership(message) { if (!("role" in message) || message.role !== "tool") return message; const source = message; const owned = tanstackMetadata(source)?.toolResult; if (!isRecord(owned)) return message; if ("content" in owned && !isContentPartArray(owned.content)) return message; const next = { ...message }; if (!("id" in owned)) Reflect.deleteProperty(next, "id"); if (!("createdAt" in owned)) Reflect.deleteProperty(next, "createdAt"); if (typeof owned.id === "string") Reflect.set(next, "id", owned.id); if (typeof owned.createdAt === "string") { const createdAt = coerceCreatedAt(owned.createdAt); if (createdAt) Reflect.set(next, "createdAt", createdAt); } if (isContentPartArray(owned.content)) Reflect.set(next, "content", owned.content); const sourceMetadata = Reflect.get(source, "metadata"); if (isRecord(sourceMetadata)) { const tanstack = sourceMetadata.tanstack; if (isRecord(tanstack)) { const { toolResult: _toolResult, ...restTanstack } = tanstack; const restMetadata = { ...sourceMetadata }; if (Object.keys(restTanstack).length) restMetadata.tanstack = restTanstack; else delete restMetadata.tanstack; if (Object.keys(restMetadata).length) Reflect.set(next, "metadata", restMetadata); else Reflect.deleteProperty(next, "metadata"); } } return next; } /** * Rebuild a `Date` from a live `Date` or from an ISO string. * `JSON.stringify` turns `Date` into a string, so persistence reload * and AG-UI wire both land here as strings. */ function coerceCreatedAt(value) { if (value instanceof Date) return Number.isNaN(value.getTime()) ? void 0 : value; if (typeof value !== "string") return void 0; const createdAt = new Date(value); return Number.isNaN(createdAt.getTime()) ? void 0 : createdAt; } function normalizeMessagePart(part) { if (part.type !== "tool-result") return part; const createdAt = coerceCreatedAt(part.createdAt); const { createdAt: _createdAt, ...rest } = part; return createdAt === void 0 ? rest : { ...rest, createdAt }; } function createdAtFromMetadata(source) { return coerceCreatedAt(tanstackMetadata(source)?.createdAt); } function optionalCreatedAt(source) { const createdAt = coerceCreatedAt(source.createdAt); return createdAt !== void 0 ? { createdAt } : {}; } function optionalName(source) { return source.name !== void 0 ? { name: source.name } : {}; } function isUiResourcePart(value) { if (value == null || typeof value !== "object" || Array.isArray(value)) return false; if (!("type" in value) || value.type !== "ui-resource") return false; if (!("toolCallId" in value) || typeof value.toolCallId !== "string") return false; if (!("toolName" in value) || typeof value.toolName !== "string") return false; if (!("resource" in value) || value.resource == null || typeof value.resource !== "object" || Array.isArray(value.resource)) return false; const resource = value.resource; return "uri" in resource && typeof resource.uri === "string" && "mimeType" in resource && typeof resource.mimeType === "string"; } function uiResourceKey(part) { return `${part.toolCallId}\0${part.toolName}\0${part.resource.uri}`; } function appendUiResources(ui, resources) { if (resources.length === 0) return ui; const seen = new Set(ui.parts.filter(isUiResourcePart).map((part) => uiResourceKey(part))); const extra = resources.filter((part) => !seen.has(uiResourceKey(part))); if (extra.length === 0) return ui; return { ...ui, parts: [...ui.parts, ...extra] }; } function assistantMetadata(uiMessage) { const fromParts = uiMessage.parts.filter(isUiResourcePart); const current = uiMessage.metadata ?? {}; const previous = tanstackMetadata(uiMessage); const tanstack = {}; if (previous?.model !== void 0) tanstack.model = previous.model; if (previous?.signature !== void 0) tanstack.signature = previous.signature; if (fromParts.length > 0) tanstack.uiResources = fromParts; const result = { ...current }; if (Object.keys(tanstack).length > 0) result.tanstack = tanstack; else delete result.tanstack; return Object.keys(result).length > 0 ? result : void 0; } /** * Convert a UIMessage to ModelMessage(s) * * Walks the parts array IN ORDER to preserve the interleaving of text, * tool calls, and tool results. This is critical for multi-round tool * flows where the model generates text, calls a tool, gets the result, * then generates more text and calls another tool. * * The output preserves the sequential structure: * text1 → toolCall1 → toolResult1 → text2 → toolCall2 → toolResult2 * becomes: * assistant: {content: "text1", toolCalls: [toolCall1]} * tool: toolResult1 * assistant: {content: "text2", toolCalls: [toolCall2]} * tool: toolResult2 * * @param uiMessage - The UIMessage to convert * @returns An array of ModelMessages preserving part ordering */ function uiMessageToModelMessages(uiMessage) { if (uiMessage.role === "system") return []; if (uiMessage.role !== "assistant") return [buildUserOrToolMessage(uiMessage)]; return buildAssistantMessages(uiMessage); } /** * Build a single ModelMessage for user messages (simple path). * Preserves ordering of text and multimodal content parts. */ function buildUserOrToolMessage(uiMessage) { const contentParts = []; for (const part of uiMessage.parts) if (isContentPart(part)) contentParts.push(part); return { id: uiMessage.id, role: uiMessage.role, content: collapseContentParts(contentParts), ...optionalName(uiMessage), ...optionalCreatedAt(uiMessage), ...uiMessage.metadata !== void 0 && { metadata: uiMessage.metadata } }; } function createSegment() { return { contentParts: [], toolCalls: [] }; } function isToolCallIncluded(part) { return part.state === "input-complete" || part.state === "complete" || part.state === "approval-requested" || part.state === "approval-responded" || part.state === "error" || part.output !== void 0; } /** * Build ModelMessages for an assistant UIMessage, preserving the * sequential interleaving of text, tool calls, and tool results. * * Walks parts in order. Text and tool-call parts accumulate into the * current "segment". When a tool-result part is encountered, the * current segment is flushed as an assistant message, then the tool * result is emitted as a tool message. */ function buildAssistantMessages(uiMessage) { const messageList = []; let current = createSegment(); let pendingThinking = []; const emittedToolResultIds = /* @__PURE__ */ new Set(); const identityFields = { id: uiMessage.id, ...optionalName(uiMessage), ...optionalCreatedAt(uiMessage) }; const metadata = assistantMetadata(uiMessage); const assistantFields = { ...identityFields, ...metadata !== void 0 && { metadata } }; function flushSegment(force = false) { const content = collapseContentParts(current.contentParts); const hasContent = content !== null; const hasToolCalls = current.toolCalls.length > 0; const hasThinking = pendingThinking.length > 0; if (force || hasContent || hasToolCalls || hasThinking) { messageList.push({ ...assistantFields, role: "assistant", content, ...hasToolCalls && { toolCalls: current.toolCalls }, ...hasThinking && { thinking: pendingThinking }, ...current.structuredOutput && { structuredOutput: current.structuredOutput } }); pendingThinking = []; } current = createSegment(); } for (const part of uiMessage.parts) switch (part.type) { case "text": case "image": case "audio": case "video": case "document": current.contentParts.push(part); break; case "tool-call": if (isToolCallIncluded(part)) current.toolCalls.push({ id: part.id, type: "function", function: { name: part.name, arguments: part.arguments }, ...part.metadata !== void 0 && { metadata: part.metadata } }); break; case "tool-result": flushSegment(messageList.length === 0); if ((part.state === "complete" || part.state === "error") && !emittedToolResultIds.has(part.toolCallId)) { messageList.push({ ...part.id !== void 0 && { id: part.id }, ...optionalCreatedAt(part), role: "tool", content: part.content, toolCallId: part.toolCallId, ...part.name !== void 0 && { name: part.name }, ...part.metadata !== void 0 && { metadata: part.metadata }, ...part.error !== void 0 && { error: part.error } }); emittedToolResultIds.add(part.toolCallId); } break; case "thinking": if (part.content) { if (current.toolCalls.some(isProviderExecutedToolCall)) flushSegment(); pendingThinking.push({ content: part.content, ...part.signature && { signature: part.signature } }); } break; case "structured-output": if (part.status === "complete") { const serialized = part.raw !== "" ? part.raw : part.data !== void 0 ? safeJsonStringify(part.data) : ""; if (serialized !== "") { current.contentParts.push({ type: "text", content: serialized }); current.structuredOutput = part; } } } flushSegment(); for (const part of uiMessage.parts) { if (part.type !== "tool-call") continue; if (part.output !== void 0 && !emittedToolResultIds.has(part.id)) { messageList.push({ role: "tool", content: normalizeToolResult(part.output), toolCallId: part.id }); emittedToolResultIds.add(part.id); } if (part.output === void 0 && part.state === "approval-responded" && part.approval?.approved !== void 0 && !emittedToolResultIds.has(part.id)) { const approved = part.approval.approved; messageList.push({ role: "tool", content: JSON.stringify({ approved, ...approved && { pendingExecution: true }, message: approved ? "User approved this action" : "User denied this action" }), toolCallId: part.id }); emittedToolResultIds.add(part.id); } } if (messageList.length === 0) messageList.push({ ...assistantFields, role: "assistant", content: null }); return messageList; } /** * Convert a ModelMessage to UIMessage * * This conversion creates a parts-based structure: * - content field → TextPart * - toolCalls array → ToolCallPart[] * - role="tool" messages should be converted separately and merged * * @param modelMessage - The ModelMessage to convert * @param id - Optional ID for the UIMessage (generated if not provided) * @returns A UIMessage with parts */ function modelMessageToUIMessage(modelMessage, id) { const parts = []; const createdAt = coerceCreatedAt(modelMessage.createdAt); if (modelMessage.role === "assistant" && modelMessage.thinking?.length) for (const thinking of modelMessage.thinking) { if (!thinking.content) continue; parts.push({ type: "thinking", content: thinking.content, ...thinking.signature && { signature: thinking.signature } }); } const structuredOutput = modelMessage.structuredOutput ?? snapshotStructuredOutput(tanstackMetadata(modelMessage)?.structuredOutput); if (modelMessage.role === "assistant" && structuredOutput) { if (typeof modelMessage.content === "string" && modelMessage.content && modelMessage.content !== structuredOutput.raw) { const suffix = structuredOutput.raw; const text = suffix !== "" && modelMessage.content.endsWith(suffix) ? modelMessage.content.slice(0, -suffix.length) : modelMessage.content; if (text) parts.push({ type: "text", content: text }); } parts.push(structuredOutput); } else if (modelMessage.role === "tool" && modelMessage.toolCallId) parts.push({ type: "tool-result", toolCallId: modelMessage.toolCallId, content: toolResultContent(modelMessage.content), state: modelMessage.error === void 0 ? "complete" : "error", ...modelMessage.id !== void 0 && { id: modelMessage.id }, ...modelMessage.name !== void 0 && { name: modelMessage.name }, ...modelMessage.metadata !== void 0 && { metadata: modelMessage.metadata }, ...createdAt !== void 0 && { createdAt }, ...modelMessage.error !== void 0 && { error: modelMessage.error } }); else if (Array.isArray(modelMessage.content)) for (const part of modelMessage.content) parts.push(part); else { const textContent = getTextContent(modelMessage.content); if (textContent) parts.push({ type: "text", content: textContent }); } if (modelMessage.toolCalls && modelMessage.toolCalls.length > 0) for (const toolCall of modelMessage.toolCalls) { let input; try { input = JSON.parse(toolCall.function.arguments); } catch { input = void 0; } parts.push({ type: "tool-call", id: toolCall.id, name: toolCall.function.name, arguments: toolCall.function.arguments, state: "input-complete", ...input !== void 0 && { input }, ...toolCall.metadata !== void 0 && { metadata: toolCall.metadata } }); } const ui = { id: id || generateMessageId(), role: modelMessage.role === "tool" ? "assistant" : modelMessage.role, parts, ...optionalName(modelMessage), ...createdAt !== void 0 && { createdAt }, ...modelMessage.metadata !== void 0 && { metadata: modelMessage.metadata } }; const storedResources = tanstackMetadata(modelMessage)?.uiResources; return appendUiResources(ui, Array.isArray(storedResources) ? storedResources.filter(isUiResourcePart) : []); } /** * Normalize a single AG-UI `MESSAGES_SNAPSHOT` message into a `UIMessage`. * * AG-UI snapshot messages use the wire shape `{ id, role, content }` and have * no `parts` array. Casting them directly to `UIMessage` is unsafe: any code * that later reads `message.parts` (e.g. the devtools `onToolCallStateChange` * handler) crashes with "Cannot read properties of undefined (reading 'find')". * * Each role is mapped to the canonical `UIMessage` shape, reusing * `modelMessageToUIMessage` for the roles that share `ModelMessage`'s structure. * The original AG-UI `id` is preserved so later `TEXT_MESSAGE_CONTENT` / * `TOOL_CALL_*` events still route by `messageId` (falling back to a generated * id only when the snapshot omits one). Messages that already carry `parts` * (e.g. a TanStack server echoing `UIMessage`s back over the wire) pass through * unchanged apart from ensuring an id. */ function aguiSnapshotMessageToUIMessage(message) { if ("parts" in message) return applySnapshotMetadata(message, { ...message, id: message.id || generateMessageId() }); const id = message.id || generateMessageId(); switch (message.role) { case "user": return applySnapshotMetadata(message, { id, role: "user", parts: aguiUserContentToParts(message.content) }); case "assistant": { const metadata = tanstackMetadata(message); const toolCallMetadata = metadata?.toolCallMetadata; const structuredOutput = snapshotStructuredOutput(metadata?.structuredOutput); const toolCalls = message.toolCalls?.map((toolCall) => { const callMetadata = toolCallMetadata != null && typeof toolCallMetadata === "object" ? toolCallMetadata[toolCall.id] : void 0; return callMetadata !== void 0 ? { ...toolCall, metadata: callMetadata } : toolCall; }); return applySnapshotMetadata(message, modelMessageToUIMessage({ role: "assistant", content: message.content ?? null, ...optionalName(message), ...toolCalls && { toolCalls }, ...structuredOutput && { structuredOutput } }, id)); } case "tool": { message = restoreToolResultOwnership(message); const createdAt = coerceCreatedAt("createdAt" in message ? message.createdAt : void 0) ?? createdAtFromMetadata(message); return applySnapshotMetadata(message, modelMessageToUIMessage({ role: "tool", content: message.content, toolCallId: message.toolCallId, ..."name" in message && typeof message.name === "string" ? { name: message.name } : {}, ..."id" in message && typeof message.id === "string" ? { id: message.id } : {}, ..."metadata" in message && message.metadata != null ? { metadata: message.metadata } : {}, ...createdAt !== void 0 ? { createdAt } : {}, ...message.error !== void 0 && { error: message.error } }, id)); } case "system": case "developer": return applySnapshotMetadata(message, { id, role: "system", parts: message.content ? [{ type: "text", content: message.content }] : [] }); case "reasoning": { const signature = encryptedValueFrom(message); return applySnapshotMetadata(message, { id, role: "assistant", parts: message.content ? [{ type: "thinking", content: message.content, ...signature !== void 0 ? { signature } : {} }] : [] }); } default: return applySnapshotMetadata(message, { id, role: "assistant", parts: [] }); } } /** Copy snapshot metadata when it is a record. Rebuild createdAt from tanstack.createdAt. */ function applySnapshotMetadata(source, ui) { const normalizedParts = ui.parts.map((part) => normalizeMessagePart(part)); if (normalizedParts !== ui.parts) ui = { ...ui, parts: normalizedParts }; const name = "name" in source && typeof source.name === "string" ? source.name : void 0; let next = name !== void 0 ? { ...ui, name } : ui; let metadata; if ("metadata" in source) { const raw = source.metadata; if (raw != null && typeof raw === "object" && !Array.isArray(raw)) { metadata = raw; next = { ...next, metadata }; } } const createdAt = coerceCreatedAt(next.createdAt) ?? (metadata !== void 0 ? createdAtFromMetadata(metadata) : void 0); if (createdAt !== void 0 && !Object.is(createdAt, next.createdAt)) next = { ...next, createdAt }; else if (createdAt === void 0 && next.createdAt !== void 0) { const { createdAt: _invalid, ...rest } = next; next = rest; } const uiResources = tanstackMetadata(metadata)?.uiResources; const resources = Array.isArray(uiResources) ? uiResources.filter(isUiResourcePart) : []; return appendUiResources(next, resources); } function snapshotStructuredOutput(value) { if (value == null || value.status !== "streaming" && value.status !== "complete" && value.status !== "error" || typeof value.raw !== "string") return; return { type: "structured-output", status: value.status, raw: value.raw, ...value.partial !== void 0 ? { partial: value.partial } : {}, ...value.data !== void 0 ? { data: value.data } : {}, ...value.reasoning ? { reasoning: value.reasoning } : {}, ...value.errorMessage !== void 0 ? { errorMessage: value.errorMessage } : {} }; } /** * Convert AG-UI user message content into `UIMessage` parts. * * AG-UI user content is either a plain string or a multimodal array whose text * entries use `{ type: 'text', text }` (vs. TanStack's `{ type: 'text', content }`). * Text entries are rewritten to the TanStack shape; image/audio/video/document * entries already match `ContentPart` and pass through. `binary` entries have no * TanStack equivalent and are dropped. */ function aguiUserContentToParts(content) { if (typeof content === "string") return content ? [{ type: "text", content }] : []; const parts = []; for (const part of content) if (part.type === "text") parts.push({ type: "text", content: part.text }); else if (part.type !== "binary") parts.push(part); return parts; } /** * Convert an array of ModelMessages to UIMessages * * This handles merging tool result messages with their corresponding assistant messages * * @param modelMessages - Array of ModelMessages to convert * @returns Array of UIMessages */ function modelMessagesToUIMessages(modelMessages) { const uiMessages = []; let currentAssistantMessage = null; for (const msg of modelMessages) if (msg.role === "tool") { if (msg.toolCallId !== void 0 && currentAssistantMessage && currentAssistantMessage.role === "assistant") { const content = toolResultContent(msg.content); const toolCallPart = currentAssistantMessage.parts.find((part) => part.type === "tool-call" && part.id === msg.toolCallId); if (toolCallPart) { toolCallPart.output = typeof content === "string" ? parseToolResultContent(content) : content; toolCallPart.state = msg.error === void 0 ? "complete" : "error"; } currentAssistantMessage.parts.push({ type: "tool-result", toolCallId: msg.toolCallId, content, state: msg.error === void 0 ? "complete" : "error", ...msg.id !== void 0 && msg.id !== currentAssistantMessage.id && { id: msg.id }, ...msg.name !== void 0 && { name: msg.name }, ...msg.metadata !== void 0 && { metadata: msg.metadata }, ...optionalCreatedAt(msg), ...msg.error !== void 0 && { error: msg.error } }); } else { const toolResultUIMessage = modelMessageToUIMessage(msg, msg.id); uiMessages.push(toolResultUIMessage); } } else { const uiMessage = modelMessageToUIMessage(msg, msg.id); uiMessages.push(uiMessage); if (msg.role === "assistant") currentAssistantMessage = uiMessage; else currentAssistantMessage = null; } return uiMessages; } /** * Normalize a message (UIMessage or ModelMessage) to a UIMessage * Ensures the message has an ID and createdAt timestamp * * @param message - Either a UIMessage or ModelMessage * @param generateId - Function to generate a message ID if needed * @returns A UIMessage with guaranteed id and createdAt */ function normalizeToUIMessage(message, generateId) { if ("parts" in message) { const parts = message.parts.map((part) => normalizeMessagePart(part)); return { ...message, parts, id: message.id || generateId(), createdAt: coerceCreatedAt(message.createdAt) ?? /* @__PURE__ */ new Date() }; } else return { ...modelMessageToUIMessage(message, generateId()), createdAt: coerceCreatedAt(message.createdAt) ?? /* @__PURE__ */ new Date() }; } /** * Generate a unique message ID */ function generateMessageId() { return `msg-${Date.now()}-${Math.random().toString(36).substring(7)}`; } //#endregion export { aguiSnapshotMessageToUIMessage, coerceCreatedAt, convertMessagesToModelMessages, generateMessageId, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, restoreToolResultOwnership, safeJsonStringify, uiMessageToModelMessages }; //# sourceMappingURL=messages.js.map