export type OutputContentPart = { type?: string; text?: unknown; [key: string]: unknown; }; export type OutputItem = { type?: string; id?: string; call_id?: string; name?: string; status?: string; arguments?: unknown; content?: OutputContentPart[]; summary?: OutputContentPart[]; output?: OutputContentPart[]; files?: unknown; embeds?: unknown; code?: string; lang?: string; duration?: number | string | null; action?: Record; actions?: Array>; queries?: unknown[]; [key: string]: unknown; }; export type OutputDetailToken = { summary: string; text: string; attributes: { type: string; id?: string; name?: string; done?: string; duration?: string; arguments?: string; files?: string; embeds?: string; output?: string; }; }; export type OutputDisplayItem = | { type: 'message'; id: string; text: string; } | { type: 'detail_single'; id: string; token: OutputDetailToken; } | { type: 'detail_group'; id: string; tokens: OutputDetailToken[]; }; const GROUPABLE_OUTPUT_TYPES = new Set([ 'reasoning', 'function_call', 'open_webui:code_interpreter', 'web_search_call', 'file_search_call', 'computer_call' ]); const OPENAI_TOOL_NAMES: Record = { web_search_call: 'Web Search', file_search_call: 'File Search', computer_call: 'Computer Use' }; function getTextFromParts(parts: OutputContentPart[] = []): string { return parts .map((part) => { if (part?.text === undefined || part?.text === null) { return ''; } return typeof part.text === 'string' ? part.text : String(part.text); }) .join(''); } function stringifyAttribute(value: unknown): string { if (value === undefined || value === null) { return ''; } if (typeof value === 'string') { return value; } try { return JSON.stringify(value); } catch { return String(value); } } function isDoneStatus(status?: string): boolean { return status === 'completed' || status === 'failed' || status === 'incomplete'; } function getMessageText(item: OutputItem): string { return getTextFromParts(item.content ?? []); } function getReasoningText(item: OutputItem): string { const summary = Array.isArray(item.summary) && item.summary.length ? item.summary : null; return getTextFromParts(summary ?? item.content ?? []); } function getToolResultText(item?: OutputItem): string { return (item?.output ?? []) .filter((part) => part?.type !== 'input_image') .map((part) => { if (part?.text === undefined || part?.text === null) { return ''; } return typeof part.text === 'string' ? part.text : String(part.text); }) .join(''); } function buildToolCallToken(item: OutputItem, toolOutputByCallId: Record) { const callId = item.call_id ?? ''; const resultItem = toolOutputByCallId[callId]; const isDone = isDoneStatus(item.status) || !!resultItem; let name = item.name ?? ''; if (name === 'delegate_task') { try { const args = typeof item.arguments === 'string' ? JSON.parse(item.arguments || '{}') : (item.arguments ?? {}); const task = typeof args.task === 'string' && args.task ? args.task : '?'; const label = args.background ? 'Background sub-agent' : 'Sub-agent'; name = `${label}: "${task.length > 60 ? `${task.slice(0, 60)}...` : task}"`; } catch { name = 'Sub-agent'; } } return { summary: isDone ? 'Tool Executed' : 'Executing...', text: getToolResultText(resultItem), attributes: { type: 'tool_calls', id: callId, name, done: isDone ? 'true' : 'false', arguments: stringifyAttribute(item.arguments ?? ''), files: stringifyAttribute(resultItem?.files), embeds: stringifyAttribute(resultItem?.embeds) } }; } function buildReasoningToken(item: OutputItem, isLastItem: boolean) { const duration = item.duration ?? ''; const isDone = isDoneStatus(item.status) || item.duration !== undefined || !isLastItem; const text = getReasoningText(item) .split('\n') .map((line) => (line.startsWith('>') ? line : `> ${line}`)) .join('\n'); return { summary: isDone ? `Thought for ${duration || 0} seconds` : 'Thinking...', text, attributes: { type: 'reasoning', done: isDone ? 'true' : 'false', duration: String(duration) } }; } function buildCodeInterpreterToken(item: OutputItem, isLastItem: boolean) { const duration = item.duration ?? ''; const isDone = isDoneStatus(item.status) || item.duration !== undefined || !isLastItem; const code = item.code ?? ''; const lang = item.lang ?? 'python'; return { summary: isDone ? 'Analyzed' : 'Analyzing...', text: code ? `\`\`\`${lang}\n${code}\n\`\`\`` : '', attributes: { type: 'code_interpreter', done: isDone ? 'true' : 'false', duration: String(duration), output: stringifyAttribute(item.output) } }; } function getOpenAIToolSummary(item: OutputItem): string { if (item.type === 'web_search_call') { const action = item.action ?? {}; const actionType = action.type; if (actionType === 'search') { const queries = Array.isArray(action.queries) ? action.queries : []; const query = typeof action.query === 'string' ? action.query : ''; return queries.length ? `Search: ${queries.join(', ')}` : query ? `Search: ${query}` : ''; } if (actionType === 'open_page' && typeof action.url === 'string') { return `Open page: ${action.url}`; } if (actionType === 'find_in_page' && typeof action.pattern === 'string') { return `Find in page: ${action.pattern}`; } } if (item.type === 'file_search_call') { const queries = item.queries ?? []; return queries.length ? `Queries: ${queries.join(', ')}` : ''; } if (item.type === 'computer_call') { if (item.action?.type) { return `Action: ${item.action.type}`; } if (Array.isArray(item.actions) && item.actions.length) { return `Actions: ${item.actions.map((action) => action.type ?? '?').join(', ')}`; } } return ''; } function buildOpenAIToolToken(item: OutputItem, isLastItem: boolean) { const isDone = isDoneStatus(item.status) || !isLastItem; return { summary: isDone ? 'Tool Executed' : 'Executing...', text: getOpenAIToolSummary(item), attributes: { type: 'tool_calls', id: item.id ?? '', name: OPENAI_TOOL_NAMES[item.type ?? ''] ?? item.type ?? '', done: isDone ? 'true' : 'false', arguments: '' } }; } function buildDetailToken( item: OutputItem, isLastItem: boolean, toolOutputByCallId: Record ): OutputDetailToken | null { if (item.type === 'function_call') { return buildToolCallToken(item, toolOutputByCallId); } if (item.type === 'reasoning') { return buildReasoningToken(item, isLastItem); } if (item.type === 'open_webui:code_interpreter') { return buildCodeInterpreterToken(item, isLastItem); } if (item.type && OPENAI_TOOL_NAMES[item.type]) { return buildOpenAIToolToken(item, isLastItem); } return null; } export function buildOutputDisplayItems(output: OutputItem[] = []): OutputDisplayItem[] { const displayItems: OutputDisplayItem[] = []; const currentDetailTokens: OutputDetailToken[] = []; const toolOutputByCallId: Record = {}; for (const item of output) { if (item?.type === 'function_call_output' && item.call_id) { toolOutputByCallId[item.call_id] = item; } } const flushDetails = () => { if (currentDetailTokens.length > 1) { displayItems.push({ type: 'detail_group', id: `detail-group-${displayItems.length}`, tokens: [...currentDetailTokens] }); } else if (currentDetailTokens.length === 1) { displayItems.push({ type: 'detail_single', id: `detail-${displayItems.length}`, token: currentDetailTokens[0] }); } currentDetailTokens.length = 0; }; output.forEach((item, index) => { if (item?.type === 'function_call_output') { return; } if (item?.type && GROUPABLE_OUTPUT_TYPES.has(item.type)) { const token = buildDetailToken(item, index === output.length - 1, toolOutputByCallId); if (token) { currentDetailTokens.push(token); } return; } if (item?.type === 'message') { const text = getMessageText(item); if (text.trim()) { flushDetails(); displayItems.push({ type: 'message', id: item.id ?? `message-${index}`, text }); } return; } const fallbackText = getMessageText(item); if (fallbackText.trim()) { flushDetails(); displayItems.push({ type: 'message', id: item.id ?? `output-${index}`, text: fallbackText }); } }); flushDetails(); return displayItems; } export function getOutputText(output?: OutputItem[] | null): string { return (output ?? []) .filter((item) => item?.type === 'message') .map(getMessageText) .filter((text) => text.trim()) .join('\n'); } export function replaceOutputMessageText( output: OutputItem[] = [], oldContent: string, newContent: string ): OutputItem[] { if (!oldContent) { return output; } let replaced = false; const nextOutput = output.map((item) => { if (replaced || item?.type !== 'message' || !Array.isArray(item.content)) { return item; } const partIndex = item.content.findIndex( (part) => typeof part.text === 'string' && part.text.includes(oldContent) ); if (partIndex === -1) { return item; } replaced = true; const nextContent = [...item.content]; const part = nextContent[partIndex]; nextContent[partIndex] = { ...part, text: (part.text as string).replace(oldContent, newContent) }; return { ...item, content: nextContent }; }); return replaced ? nextOutput : output; }