import type { DebugTranscriptRow, DebugTranscriptTextRole, DebugTranscriptTextRow, DebugTranscriptToolRow, DebugTranscriptToolStatus, } from './types'; let rowCounter = 0; const createRowId = (prefix: string) => `${prefix}_${Date.now()}_${++rowCounter}`; const isTextRow = ( row: DebugTranscriptRow | undefined | null ): row is DebugTranscriptTextRow => row?.kind === 'text'; const isToolRow = ( row: DebugTranscriptRow | undefined | null ): row is DebugTranscriptToolRow => row?.kind === 'tool'; const findRowIndexById = (rows: DebugTranscriptRow[], rowId?: string | null) => rowId ? rows.findIndex((row) => row.id === rowId) : -1; const findAssistantRowIndexByResponseId = (rows: DebugTranscriptRow[], responseId?: string) => { if (!responseId) return -1; return rows.findIndex( (row) => isTextRow(row) && row.role === 'assistant' && row.responseId === responseId ); }; const findLastAssistantCandidateIndex = ( rows: DebugTranscriptRow[], responseId?: string ) => { for (let i = rows.length - 1; i >= 0; i -= 1) { const row = rows[i]; if (isTextRow(row) && row.role === 'assistant') { if (responseId && row.responseId && row.responseId !== responseId) { break; } return i; } if (isTextRow(row) && row.role === 'user') { break; } } return -1; }; const createTextRow = ({ role, text, turnId, utteranceId, responseId, ttfbMs, isStreaming, }: { role: DebugTranscriptTextRole; text: string; turnId?: string; utteranceId?: string; responseId?: string; ttfbMs?: number; isStreaming?: boolean; }): DebugTranscriptTextRow => ({ kind: 'text', id: createRowId(role), role, text, ...(turnId ? { turnId } : {}), ...(utteranceId ? { utteranceId } : {}), ...(responseId ? { responseId } : {}), ...(typeof ttfbMs === 'number' ? { ttfbMs } : {}), ...(typeof isStreaming === 'boolean' ? { isStreaming } : {}), }); const createToolRow = ({ toolCallId, toolName, toolDisplayName, executor, turnId, utteranceId, responseId, source, status, args, result, error, rawCall, rawResult, }: { toolCallId: string; toolName: string; toolDisplayName: string; executor: string; turnId?: string; utteranceId?: string; responseId?: string; source?: string; status: DebugTranscriptToolStatus; args?: unknown; result?: unknown; error?: unknown; rawCall?: unknown; rawResult?: unknown; }): DebugTranscriptToolRow => ({ kind: 'tool', id: toolCallId || createRowId('tool'), toolCallId, toolName, toolDisplayName, executor, ...(turnId ? { turnId } : {}), ...(utteranceId ? { utteranceId } : {}), ...(responseId ? { responseId } : {}), ...(source ? { source } : {}), status, ...(args !== undefined ? { args } : {}), ...(result !== undefined ? { result } : {}), ...(error !== undefined ? { error } : {}), ...(rawCall !== undefined ? { rawCall } : {}), ...(rawResult !== undefined ? { rawResult } : {}), }); export const resetTranscriptRows = (): DebugTranscriptRow[] => []; export const appendTextRow = ( rows: DebugTranscriptRow[], { role, text, turnId, utteranceId, responseId, ttfbMs, isStreaming, }: { role: DebugTranscriptTextRole; text: string; turnId?: string; utteranceId?: string; responseId?: string; ttfbMs?: number; isStreaming?: boolean; } ): DebugTranscriptRow[] => [ ...rows, createTextRow({ role, text, turnId, utteranceId, responseId, ttfbMs, isStreaming }), ]; export const appendNoticeRow = (rows: DebugTranscriptRow[], text: string) => appendTextRow(rows, { role: 'notice', text, isStreaming: false }); export const updateUserDraftRow = ( rows: DebugTranscriptRow[], { draftRowId, text, turnId, utteranceId, }: { draftRowId?: string | null; text: string; turnId?: string; utteranceId?: string; } ): { rows: DebugTranscriptRow[]; draftRowId: string } => { const rowIndex = findRowIndexById(rows, draftRowId); if (rowIndex !== -1) { const row = rows[rowIndex]; if (isTextRow(row) && row.role === 'user') { const nextRows = [...rows]; nextRows[rowIndex] = { ...row, text, turnId: row.turnId || turnId, utteranceId: row.utteranceId || utteranceId, isStreaming: true, }; return { rows: nextRows, draftRowId: row.id }; } } const nextRow = createTextRow({ role: 'user', text, turnId, utteranceId, isStreaming: true, }); return { rows: [...rows, nextRow], draftRowId: nextRow.id, }; }; export const finalizeUserDraftRow = ( rows: DebugTranscriptRow[], { draftRowId, text, turnId, utteranceId, }: { draftRowId?: string | null; text: string; turnId?: string; utteranceId?: string; } ): { rows: DebugTranscriptRow[]; draftRowId: null } => { const rowIndex = findRowIndexById(rows, draftRowId); if (rowIndex !== -1) { const row = rows[rowIndex]; if (isTextRow(row) && row.role === 'user') { const nextRows = [...rows]; nextRows[rowIndex] = { ...row, text: text || row.text, turnId: row.turnId || turnId, utteranceId: row.utteranceId || utteranceId, isStreaming: false, }; return { rows: nextRows, draftRowId: null }; } } if (!text) { return { rows, draftRowId: null }; } return { rows: appendTextRow(rows, { role: 'user', text, turnId, utteranceId, isStreaming: false, }), draftRowId: null, }; }; export const updateAssistantDeltaRow = ( rows: DebugTranscriptRow[], { draftRowId, delta, turnId, utteranceId, responseId, ttfbMs, }: { draftRowId?: string | null; delta: string; turnId?: string; utteranceId?: string; responseId?: string; ttfbMs?: number; } ): { rows: DebugTranscriptRow[]; draftRowId: string } => { let rowIndex = findRowIndexById(rows, draftRowId); if ( rowIndex !== -1 && (!isTextRow(rows[rowIndex]) || rows[rowIndex].role !== 'assistant') ) { rowIndex = -1; } if (rowIndex === -1) { rowIndex = findAssistantRowIndexByResponseId(rows, responseId); } if (rowIndex === -1) { rowIndex = findLastAssistantCandidateIndex(rows, responseId); } if (rowIndex === -1) { const lastRow = rows[rows.length - 1]; if ( isTextRow(lastRow) && lastRow.role === 'assistant' && lastRow.text === delta && lastRow.responseId === responseId ) { return { rows, draftRowId: lastRow.id, }; } const nextRow = createTextRow({ role: 'assistant', text: delta, turnId, utteranceId, responseId, ttfbMs, isStreaming: true, }); return { rows: [...rows, nextRow], draftRowId: nextRow.id, }; } const row = rows[rowIndex]; if (!isTextRow(row) || row.role !== 'assistant') { return { rows, draftRowId: draftRowId || createRowId('assistant'), }; } const nextRows = [...rows]; nextRows[rowIndex] = { ...row, text: row.text + delta, turnId: row.turnId || turnId, utteranceId: row.utteranceId || utteranceId, responseId: row.responseId || responseId, ttfbMs: typeof row.ttfbMs === 'number' ? row.ttfbMs : ttfbMs, isStreaming: true, }; return { rows: nextRows, draftRowId: row.id, }; }; export const finalizeAssistantTextRow = ( rows: DebugTranscriptRow[], { draftRowId, text, turnId, utteranceId, responseId, ttfbMs, }: { draftRowId?: string | null; text: string; turnId?: string; utteranceId?: string; responseId?: string; ttfbMs?: number; } ): { rows: DebugTranscriptRow[]; draftRowId: null } => { let rowIndex = findRowIndexById(rows, draftRowId); if ( rowIndex !== -1 && (!isTextRow(rows[rowIndex]) || rows[rowIndex].role !== 'assistant') ) { rowIndex = -1; } if (rowIndex === -1) { rowIndex = findAssistantRowIndexByResponseId(rows, responseId); } if (rowIndex === -1) { rowIndex = findLastAssistantCandidateIndex(rows, responseId); } if (rowIndex !== -1) { const row = rows[rowIndex]; if (isTextRow(row) && row.role === 'assistant') { const nextRows = [...rows]; nextRows[rowIndex] = { ...row, text: text || row.text, turnId: row.turnId || turnId, utteranceId: row.utteranceId || utteranceId, responseId: row.responseId || responseId, ttfbMs: typeof row.ttfbMs === 'number' ? row.ttfbMs : ttfbMs, isStreaming: false, }; return { rows: nextRows, draftRowId: null, }; } } if (!text) { return { rows, draftRowId: null }; } const lastRow = rows[rows.length - 1]; if ( isTextRow(lastRow) && lastRow.role === 'assistant' && (!responseId || !lastRow.responseId || lastRow.responseId === responseId) ) { if (lastRow.text === text) { return { rows, draftRowId: null }; } if (text.startsWith(lastRow.text) || lastRow.text.startsWith(text)) { const nextRows = [...rows]; nextRows[nextRows.length - 1] = { ...lastRow, text, turnId: lastRow.turnId || turnId, utteranceId: lastRow.utteranceId || utteranceId, responseId: lastRow.responseId || responseId, ttfbMs: typeof lastRow.ttfbMs === 'number' ? lastRow.ttfbMs : ttfbMs, isStreaming: false, }; return { rows: nextRows, draftRowId: null, }; } } return { rows: appendTextRow(rows, { role: 'assistant', text, turnId, utteranceId, responseId, ttfbMs, isStreaming: false, }), draftRowId: null, }; }; export const attachAssistantTtfb = ( rows: DebugTranscriptRow[], { responseId, ttfbMs, }: { responseId?: string; ttfbMs: number; } ) => { const rowIndex = findAssistantRowIndexByResponseId(rows, responseId) !== -1 ? findAssistantRowIndexByResponseId(rows, responseId) : findLastAssistantCandidateIndex(rows, responseId); if (rowIndex === -1) { return rows; } const row = rows[rowIndex]; if (!isTextRow(row) || row.role !== 'assistant') { return rows; } const nextRows = [...rows]; nextRows[rowIndex] = { ...row, ttfbMs, }; return nextRows; }; export const trimInterruptedResponseRows = ( rows: DebugTranscriptRow[], responseId?: string ) => { if (!responseId) return rows; return rows.filter((row) => row.responseId !== responseId); }; export const upsertToolCallRow = ( rows: DebugTranscriptRow[], { toolCallId, toolName, toolDisplayName, executor, turnId, utteranceId, responseId, args, rawCall, }: { toolCallId: string; toolName: string; toolDisplayName: string; executor: string; turnId?: string; utteranceId?: string; responseId?: string; args?: unknown; rawCall?: unknown; } ) => { const rowIndex = rows.findIndex( (row) => isToolRow(row) && row.toolCallId === toolCallId ); if (rowIndex === -1) { return [ ...rows, createToolRow({ toolCallId, toolName, toolDisplayName, executor, turnId, utteranceId, responseId, status: 'pending', args, rawCall, }), ]; } const row = rows[rowIndex]; if (!isToolRow(row)) { return rows; } const nextRows = [...rows]; nextRows[rowIndex] = { ...row, toolName, toolDisplayName, executor, turnId: row.turnId || turnId, utteranceId: row.utteranceId || utteranceId, responseId: row.responseId || responseId, status: 'pending', args: args !== undefined ? args : row.args, rawCall: rawCall !== undefined ? rawCall : row.rawCall, }; return nextRows; }; export const normalizeToolStatus = ( statusCode?: number, statusMessage?: string ): DebugTranscriptToolStatus => { if (statusCode === 504 || String(statusMessage || '').toLowerCase().includes('timeout')) { return 'timeout'; } if (typeof statusCode === 'number' && statusCode >= 200 && statusCode < 300) { return 'success'; } return 'error'; }; export const resolveToolResultRow = ( rows: DebugTranscriptRow[], { toolCallId, toolName, toolDisplayName, executor, turnId, utteranceId, responseId, source, status, args, result, error, rawCall, rawResult, }: { toolCallId: string; toolName: string; toolDisplayName: string; executor?: string; turnId?: string; utteranceId?: string; responseId?: string; source?: string; status: DebugTranscriptToolStatus; args?: unknown; result?: unknown; error?: unknown; rawCall?: unknown; rawResult?: unknown; } ) => { const rowIndex = rows.findIndex( (row) => isToolRow(row) && row.toolCallId === toolCallId ); if (rowIndex === -1) { return [ ...rows, createToolRow({ toolCallId, toolName, toolDisplayName, executor: executor || 'server', turnId, utteranceId, responseId, source, status, args, result, error, rawCall, rawResult, }), ]; } const row = rows[rowIndex]; if (!isToolRow(row)) { return rows; } const nextRows = [...rows]; nextRows[rowIndex] = { ...row, toolName: toolName || row.toolName, toolDisplayName: toolDisplayName || row.toolDisplayName, executor: executor || row.executor, turnId: row.turnId || turnId, utteranceId: row.utteranceId || utteranceId, responseId: row.responseId || responseId, source: source || row.source, status, args: args !== undefined ? args : row.args, result: result !== undefined ? result : row.result, error, rawCall: rawCall !== undefined ? rawCall : row.rawCall, rawResult: rawResult !== undefined ? rawResult : row.rawResult, }; return nextRows; };