// Чистый разбор хвоста стенограммы: последний обмен (user + assistant + действия). // Схема сверена с observer-transcript-parser: entry.message.role / entry.message.content // (строка или массив блоков text/tool_use{name,input}). function parseLines(text) { const entries = []; for (const line of String(text || '').split(/\r?\n/)) { const t = line.trim(); if (!t) continue; try { entries.push(JSON.parse(t)); } catch { /* битую строку пропускаем */ } } return entries; } // Настоящий промпт пользователя (НЕ tool_result): content — строка или массив с text-блоком. // В формате Anthropic tool_result — это сообщения role:user, их пропускаем, иначе теряются // и настоящий промпт, и все действия ассистента до него. function isRealUserPrompt(msg) { if (!msg || msg.role !== 'user') return false; const c = msg.content; if (typeof c === 'string') return true; if (Array.isArray(c)) return c.some((b) => b && b.type === 'text'); return false; } // Текст результата инструмента: строка как есть; массив блоков → склейка text-блоков. const MAX_RESULT_CHARS = 1200; function resultText(content) { if (typeof content === 'string') return content; if (Array.isArray(content)) { return content.filter((b) => b && b.type === 'text' && typeof b.text === 'string') .map((b) => b.text).join('\n'); } return ''; } function truncateResult(s) { const t = String(s ?? ''); return t.length > MAX_RESULT_CHARS ? t.slice(0, MAX_RESULT_CHARS) + '…' : t; } /** Последний обмен из стенограммы: { user, assistant, actions:[{tool,input,result?}] }. * result привязывается к действию по tool_use.id === tool_result.tool_use_id (усечён до предела); * без совпадения действие остаётся прежней формы {tool,input} — без ключа result. */ export function parseLastExchange(transcriptText) { const entries = parseLines(transcriptText); let u = -1; for (let i = entries.length - 1; i >= 0; i--) { if (entries[i] && isRealUserPrompt(entries[i].message)) { u = i; break; } } const userContent = u >= 0 ? entries[u].message.content : ''; const user = typeof userContent === 'string' ? userContent : (Array.isArray(userContent) ? userContent.filter((b) => b && b.type === 'text').map((b) => b.text).join('\n') : ''); let assistant = ''; const raw = []; // {id, tool, input} — вызовы инструментов const results = {}; // tool_use_id -> текст результата (из tool_result в сообщениях role:user) for (let i = u + 1; i < entries.length; i++) { const m = entries[i] && entries[i].message; if (!m) continue; const c = m.content; if (m.role === 'assistant') { if (Array.isArray(c)) { for (const b of c) { if (b && b.type === 'text' && b.text) assistant += (assistant ? '\n' : '') + b.text; if (b && b.type === 'tool_use') raw.push({ id: b.id, tool: b.name, input: JSON.stringify(b.input ?? {}) }); } } else if (typeof c === 'string') { assistant += (assistant ? '\n' : '') + c; } } else if (m.role === 'user' && Array.isArray(c)) { for (const b of c) { if (b && b.type === 'tool_result' && b.tool_use_id != null) results[b.tool_use_id] = resultText(b.content); } } } const actions = raw.map((a) => { const out = { tool: a.tool, input: a.input }; if (a.id != null && results[a.id] != null) out.result = truncateResult(results[a.id]); return out; }); return { user, assistant, actions }; }