// Чистая спан-логика секретаря: границы спанов (реальные промпты владельца) → отрезки ходов. // Без I/O. Нумерация — номера ходов сырья (raw/.log). import { splitRawIntoTurns } from './secretary-layer1.mjs'; /** Нормализованный, отсортированный список уникальных границ. */ function norm(realPromptTurns) { return [...new Set((realPromptTurns || []).map(Number).filter((n) => Number.isFinite(n)))] .sort((a, b) => a - b); } /** Отрезки спанов: [b_i .. b_{i+1}-1]; последний — до lastTurn, open:true. */ export function computeSpans(realPromptTurns, lastTurn) { const b = norm(realPromptTurns); const out = []; for (let i = 0; i < b.length; i++) { const start = b[i]; const isLast = i === b.length - 1; const end = isLast ? lastTurn : b[i + 1] - 1; out.push({ start, end, open: isLast }); } return out; } /** Закрытые спаны с индексом строго больше курсора — их надо разобрать сейчас. * Курсор «ничего не разобрано» = -1. */ export function spansToDistill(realPromptTurns, lastTurn, spanCursor) { const cur = Number.isFinite(spanCursor) ? spanCursor : -1; return computeSpans(realPromptTurns, lastTurn) .map((s, index) => ({ ...s, index })) .filter((s) => !s.open && s.index > cur) .map(({ start, end, index }) => ({ start, end, index })); } /** Разбор одного блока хода сырья → {turn,user,assistant,actions}. Полное содержимое. * Формат (buildRawRecord): [ЮЗЕР]\n…\n[АССИСТЕНТ]\n…\n([ДЕЙСТВИЕ] tool in=…\n[ВЫДАЧА] tool\n…)* */ export function parseTurnBlock(block) { const s = String(block || ''); const turn = Number((s.match(/=== ХОД turn=(\d+)/) || [])[1]) || 0; const um = s.match(/\[ЮЗЕР\]\n([\s\S]*?)\n\[АССИСТЕНТ\]\n/); const am = s.match(/\[АССИСТЕНТ\]\n([\s\S]*?)(?:\n\[ДЕЙСТВИЕ\] |\n=== КОНЕЦ ХОДА ===)/); const actions = []; const re = /\[ДЕЙСТВИЕ\] (\S+) in=([\s\S]*?)\n\[ВЫДАЧА\] \S+\n([\s\S]*?)(?=\n\[ДЕЙСТВИЕ\] |\n=== КОНЕЦ ХОДА ===)/g; let m; while ((m = re.exec(s)) !== null) actions.push({ tool: m[1], input: m[2], result: m[3] }); return { turn, user: um ? um[1] : '', assistant: am ? am[1] : '', actions }; } /** Склейка обмена спана из сырья: user из хода-начала; assistant и actions — со всех ходов [start..end]. */ export function assembleSpan(rawText, { start, end }) { const blocks = splitRawIntoTurns(rawText).filter((p) => p.turn >= start && p.turn <= end); const parsed = blocks.map((p) => parseTurnBlock(p.block)); const startTurn = parsed.find((p) => p.turn === start) || parsed[0] || {}; const assistant = parsed.map((p) => p.assistant).filter(Boolean).join('\n'); const actions = parsed.flatMap((p) => p.actions); return { user: startTurn.user || '', assistant, actions }; }