import type { StatelessChatRequest } from '@/lib/types/chat'; // ==================== Message Conversion ==================== /** * Convert UI messages to OpenAI format * Includes tool call information so the model knows what actions were taken */ export function convertMessagesToOpenAI( messages: StatelessChatRequest['messages'], currentAgentId?: string, ): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> { return messages .filter((msg) => msg.role === 'user' || msg.role === 'assistant') .map((msg) => { if (msg.role === 'assistant') { // Assistant messages use JSON array format to serve as few-shot examples // that match the expected output format from the system prompt const items: Array<{ type: string; [key: string]: string }> = []; if (msg.parts) { for (const part of msg.parts) { const p = part as Record; if (p.type === 'text' && p.text) { items.push({ type: 'text', content: p.text as string }); } else if ((p.type as string)?.startsWith('action-') && p.state === 'result') { const actionName = (p.actionName || (p.type as string).replace('action-', '')) as string; const output = p.output as Record | undefined; const isSuccess = output?.success === true; const resultSummary = isSuccess ? output?.data ? `result: ${JSON.stringify(output.data).slice(0, 100)}` : 'success' : (output?.error as string) || 'failed'; items.push({ type: 'action', name: actionName, result: resultSummary, }); } } } const content = items.length > 0 ? JSON.stringify(items) : ''; const msgAgentId = msg.metadata?.agentId; // When currentAgentId is provided and this message is from a DIFFERENT agent, // convert to user role with agent name attribution if (currentAgentId && msgAgentId && msgAgentId !== currentAgentId) { const agentName = msg.metadata?.senderName || msgAgentId; return { role: 'user' as const, content: content ? `[${agentName}]: ${content}` : '', }; } return { role: 'assistant' as const, content, }; } // User messages: keep plain text concatenation const contentParts: string[] = []; if (msg.parts) { for (const part of msg.parts) { const p = part as Record; if (p.type === 'text' && p.text) { contentParts.push(p.text as string); } else if ((p.type as string)?.startsWith('action-') && p.state === 'result') { const actionName = (p.actionName || (p.type as string).replace('action-', '')) as string; const output = p.output as Record | undefined; const isSuccess = output?.success === true; const resultSummary = isSuccess ? output?.data ? `result: ${JSON.stringify(output.data).slice(0, 100)}` : 'success' : (output?.error as string) || 'failed'; contentParts.push(`[Action ${actionName}: ${resultSummary}]`); } } } // Extract speaker name from metadata (e.g. other agents' messages in discussion) const senderName = msg.metadata?.senderName; let content = contentParts.join('\n'); if (senderName) { content = `[${senderName}]: ${content}`; } // Annotate interrupted messages so the LLM knows context was cut short const isInterrupted = (msg as unknown as Record).metadata && ((msg as unknown as Record).metadata as Record) ?.interrupted; return { role: 'user' as const, content: isInterrupted ? `${content}\n[This response was interrupted — do NOT continue it. Start a new JSON array response.]` : content, }; }) .filter((msg) => { // Drop empty messages and messages with only dots/ellipsis/whitespace // (produced by failed agent streams) const stripped = msg.content.replace(/[.\s…]+/g, ''); return stripped.length > 0; }); }