feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user