Files
maestro/src/engine/reflection/activity-summarizer.ts
T

80 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}