// src/engine/reflection/activity-summarizer.ts export interface ActivityEvent { type: string; // 'tool_call' | 'tool_result' | 'tool_error' | 'transition' | 'system' | ... tool?: string; args?: unknown; error?: string; result?: unknown; from?: string; // for transitions to?: string; reason?: string; ts?: string; } const KEEP_ALWAYS = new Set(['tool_error', 'transition', 'system_warning', 'system_error']); function fmt(ev: ActivityEvent): string { switch (ev.type) { case 'tool_error': return `! ${ev.tool ?? '?'}: ${ev.error ?? ''}`; case 'transition': return `→ ${ev.from} -> ${ev.to}${ev.reason ? ` (${ev.reason})` : ''}`; case 'tool_call': // For complete() include result; for others just the name if (ev.tool === 'complete' || ev.tool === 'transition') { const a = ev.args ? JSON.stringify(ev.args).slice(0, 240) : ''; return `· ${ev.tool}${a ? ` ${a}` : ''}`; } return `· ${ev.tool ?? '?'}`; case 'system_warning': case 'system_error': return `! ${ev.type}: ${ev.reason ?? ''}`; default: return `· ${ev.type}`; } } export function summarizeActivityLog(events: ActivityEvent[], maxBytes: number): string { // Pass 1: keep important events; collapse runs of identical tool_call lines. const lines: string[] = []; let prev: string | null = null; let runCount = 0; for (const ev of events) { const keep = KEEP_ALWAYS.has(ev.type) || ev.tool === 'complete' || ev.tool === 'transition' || ev.type === 'tool_call'; if (!keep) continue; const line = fmt(ev); if (line === prev) { runCount++; continue; } if (runCount > 0 && prev) { lines[lines.length - 1] = `${prev} ×${runCount + 1}`; } lines.push(line); prev = line; runCount = 0; } if (runCount > 0 && prev) { lines[lines.length - 1] = `${prev} ×${runCount + 1}`; } // Pass 2: greedily fit into maxBytes, prioritizing errors + complete() at the tail. // Strategy: always include the tail (last 25%); fill the rest from the head. let out = lines.join('\n'); if (Buffer.byteLength(out, 'utf8') <= maxBytes) return out; const tailBudget = Math.floor(maxBytes * 0.4); const headBudget = maxBytes - tailBudget - 16; // reserve for "...truncated..." let head = ''; let tail = ''; for (const l of lines) { if (Buffer.byteLength(head + l + '\n', 'utf8') > headBudget) break; head += l + '\n'; } for (let i = lines.length - 1; i >= 0; i--) { if (Buffer.byteLength(lines[i] + '\n' + tail, 'utf8') > tailBudget) break; tail = lines[i] + '\n' + tail; } return `${head}\n...truncated...\n${tail}`.slice(0, maxBytes); }