80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
// 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);
|
||
}
|