/** * March Agent SDK - Vercel AI SDK Extension * * VercelAIMessageStore for persistent message history with Vercel AI SDK. * * @packageDocumentation */ import type { MarchAgentApp } from '../app.js' import { AgentStateClient } from '../agent-state-client.js' // Type definitions compatible with Vercel AI SDK's CoreMessage interface CoreMessage { role: 'system' | 'user' | 'assistant' | 'tool' content: string | Array<{ type: string;[key: string]: unknown }> [key: string]: unknown } /** * Persistent message store for Vercel AI SDK. * * Stores and retrieves AI SDK message history using the agent-state API. * Messages are serialized as JSON for full fidelity. * * @example * ```typescript * import { MarchAgentApp } from 'march-ai-sdk' * import { VercelAIMessageStore } from 'march-ai-sdk/extensions/vercel-ai' * import { streamText } from 'ai' * import { openai } from '@ai-sdk/openai' * * const app = new MarchAgentApp({ * gatewayUrl: 'agent-gateway:8080', * apiKey: 'your-key', * }) * * const store = new VercelAIMessageStore(app) * const agent = app.registerMe({ ... }) * * agent.onMessage(async (message, sender) => { * // Load message history * const history = await store.load(message.conversationId) * * // Add user message * const messages: CoreMessage[] = [ * ...history, * { role: 'user', content: message.content } * ] * * // Stream response * const streamer = agent.streamer(message) * * const result = await streamText({ * model: openai('gpt-4o'), * messages, * onChunk: ({ chunk }) => { * if (chunk.type === 'text-delta') { * streamer.stream(chunk.textDelta) * } * } * }) * * await streamer.finish() * * // Save updated history * await store.save(message.conversationId, [ * ...messages, * { role: 'assistant', content: result.text } * ]) * }) * * app.run() * ``` */ export class VercelAIMessageStore { private static readonly NAMESPACE = 'vercel_ai' private readonly client: AgentStateClient constructor(app: MarchAgentApp) { this.client = new AgentStateClient(app.gatewayClient.conversationStoreUrl) } /** * Load message history for a conversation. * * @param conversationId - The conversation ID to load history for * @returns Array of CoreMessage objects (empty array if no history) */ async load(conversationId: string): Promise { const result = await this.client.get(conversationId, VercelAIMessageStore.NAMESPACE) if (!result) { return [] } const state = result.state ?? {} const messages = state.messages as unknown[] if (!Array.isArray(messages)) { return [] } // Validate and return messages return messages.filter(this.isValidMessage) as CoreMessage[] } /** * Save message history for a conversation. * * @param conversationId - The conversation ID to save history for * @param messages - Array of CoreMessage objects to save */ async save(conversationId: string, messages: CoreMessage[]): Promise { await this.client.put(conversationId, VercelAIMessageStore.NAMESPACE, { messages: messages.map(this.serializeMessage), }) } /** * Clear message history for a conversation. * * @param conversationId - The conversation ID to clear history for */ async clear(conversationId: string): Promise { await this.client.delete(conversationId, VercelAIMessageStore.NAMESPACE) } /** * Append messages to existing history. * * @param conversationId - The conversation ID * @param newMessages - Messages to append */ async append(conversationId: string, newMessages: CoreMessage[]): Promise { const existing = await this.load(conversationId) await this.save(conversationId, [...existing, ...newMessages]) } /** * Get the last N messages from history. * * @param conversationId - The conversation ID * @param count - Number of messages to retrieve */ async getLastMessages(conversationId: string, count: number): Promise { const history = await this.load(conversationId) return history.slice(-count) } /** * Validate that an object is a valid message. */ private isValidMessage(msg: unknown): msg is CoreMessage { if (typeof msg !== 'object' || msg === null) { return false } const m = msg as Record const validRoles = ['system', 'user', 'assistant', 'tool'] return ( typeof m.role === 'string' && validRoles.includes(m.role) && (typeof m.content === 'string' || Array.isArray(m.content)) ) } /** * Serialize a message for storage. */ private serializeMessage(msg: CoreMessage): Record { // Return a plain object copy return { ...msg } } } export type { CoreMessage }