ai-harness / src /cli /renderers /index.ts
stevenkhan's picture
Initial AI Harness - production-grade model-agnostic CLI agent runtime
908562b verified
// โ”€โ”€โ”€ CLI Renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Beautiful terminal output: streaming, panels, events, spinners, markdown.
import type { HarnessEvent } from '../../core/events/index.js';
// โ”€โ”€โ”€ ANSI color helpers (no dependency needed for basic colors) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const esc = (code: string) => `\x1b[${code}m`;
const reset = esc('0');
const bold = (s: string) => `${esc('1')}${s}${reset}`;
const dim = (s: string) => `${esc('2')}${s}${reset}`;
const green = (s: string) => `${esc('32')}${s}${reset}`;
const yellow = (s: string) => `${esc('33')}${s}${reset}`;
const blue = (s: string) => `${esc('34')}${s}${reset}`;
const magenta = (s: string) => `${esc('35')}${s}${reset}`;
const cyan = (s: string) => `${esc('36')}${s}${reset}`;
const red = (s: string) => `${esc('31')}${s}${reset}`;
const gray = (s: string) => `${esc('90')}${s}${reset}`;
// โ”€โ”€โ”€ Box Drawing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const BOX = { tl: 'โ•ญ', tr: 'โ•ฎ', bl: 'โ•ฐ', br: 'โ•ฏ', h: 'โ”€', v: 'โ”‚' };
function box(title: string, content: string, color: (s: string) => string = cyan, width = 72): string {
const innerW = width - 4;
const titleStr = ` ${title} `;
const topLen = Math.max(0, innerW - titleStr.length);
const top = color(`${BOX.tl}${BOX.h}${titleStr}${'โ”€'.repeat(topLen)}${BOX.tr}`);
const bot = color(`${BOX.bl}${'โ”€'.repeat(innerW + 2)}${BOX.br}`);
const lines = content.split('\n').map((l) => {
const trimmed = l.slice(0, innerW);
return `${color(BOX.v)} ${trimmed}${' '.repeat(Math.max(0, innerW - stripAnsi(trimmed).length))} ${color(BOX.v)}`;
});
return [top, ...lines, bot].join('\n');
}
function stripAnsi(s: string): string {
return s.replace(/\x1b\[[0-9;]*m/g, '');
}
// โ”€โ”€โ”€ Spinner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const SPINNER_FRAMES = ['โ ‹', 'โ ™', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ‡', 'โ '];
export class Spinner {
private frame = 0;
private interval: ReturnType<typeof setInterval> | null = null;
private message = '';
start(message: string): void {
this.message = message;
this.interval = setInterval(() => {
this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
process.stdout.write(`\r${cyan(SPINNER_FRAMES[this.frame]!)} ${this.message}`);
}, 80);
}
update(message: string): void {
this.message = message;
}
stop(finalMessage?: string): void {
if (this.interval) clearInterval(this.interval);
this.interval = null;
process.stdout.write(`\r${' '.repeat(this.message.length + 4)}\r`);
if (finalMessage) console.log(finalMessage);
}
}
// โ”€โ”€โ”€ Event Renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
export interface RendererOptions {
verbose: boolean;
compact: boolean;
}
export class EventRenderer {
private opts: RendererOptions;
private spinner = new Spinner();
private streamBuffer = '';
constructor(opts: Partial<RendererOptions> = {}) {
this.opts = { verbose: false, compact: false, ...opts };
}
render(event: HarnessEvent): void {
switch (event.type) {
case 'session.started':
console.log('\n' + box('Session', `${bold('Goal:')} ${event.goal}\n${gray(`ID: ${event.sessionId}`)}`, cyan));
break;
case 'session.completed':
this.flushStream();
console.log('\n' + green(`โœ“ ${bold('Session completed')} โ€” ${event.summary}`));
break;
case 'session.failed':
this.flushStream();
console.log('\n' + red(`โœ— ${bold('Session failed')} โ€” ${event.error}`));
break;
case 'plan.updated':
if (!this.opts.compact) {
const planStr = event.items.map((item) => {
const icon = item.status === 'completed' ? green('โœ“') : item.status === 'in_progress' ? yellow('โ–ถ') : item.status === 'failed' ? red('โœ—') : gray('โ—‹');
return ` ${icon} ${item.title}`;
}).join('\n');
console.log('\n' + box('Plan', planStr, magenta));
}
break;
case 'model.request.start':
this.spinner.start(`${event.provider}/${event.model} thinkingโ€ฆ`);
break;
case 'model.request.end':
this.spinner.stop();
if (this.opts.verbose) {
console.log(gray(` โฑ ${event.durationMs}ms | ${event.usage.totalTokens} tokens | ~$${(event.usage.estimatedCostUsd ?? 0).toFixed(4)}`));
}
break;
case 'model.stream.delta':
this.streamBuffer += event.text;
process.stdout.write(event.text);
break;
case 'model.stream.end':
this.flushStream();
break;
case 'tool.requested':
console.log(`\n${blue('โšก')} ${bold(event.toolCall.toolName)} ${gray(`[${event.toolCall.id.slice(0, 8)}]`)}`);
if (this.opts.verbose) {
console.log(gray(` Input: ${JSON.stringify(event.toolCall.input).slice(0, 200)}`));
}
break;
case 'tool.started':
this.spinner.start('Tool executingโ€ฆ');
break;
case 'tool.progress':
this.spinner.update(event.message);
break;
case 'tool.finished':
this.spinner.stop(green(` โœ“ Done`) + (this.opts.verbose ? gray(` (${event.durationMs}ms)`) : ''));
break;
case 'tool.failed':
this.spinner.stop(red(` โœ— Failed: ${event.error}`));
break;
case 'tool.denied':
console.log(yellow(` โš  Denied: ${event.reason}`));
break;
case 'evaluation.completed':
const r = event.report;
const icon = r.passed ? green('โœ“') : red('โœ—');
const checksStr = r.checks.map((c) => ` ${c.passed ? green('โœ“') : red('โœ—')} ${c.name}${c.message ? gray(` โ€” ${c.message}`) : ''}`).join('\n');
console.log('\n' + box('Evaluation', `${icon} ${r.summary}\n${checksStr}`, r.passed ? green : red));
break;
case 'artifact.created':
console.log(`${magenta('๐Ÿ“Ž')} Artifact: ${bold(event.artifact.title)} ${gray(`(${event.artifact.type})`)}`);
break;
case 'budget.warning':
console.log(yellow(`โš  Budget warning: ${event.usage.totalTokens} tokens used`));
break;
case 'error':
console.log(red(`โœ— Error: ${event.message}`));
break;
}
}
private flushStream(): void {
if (this.streamBuffer) {
// Ensure newline after streamed content
if (!this.streamBuffer.endsWith('\n')) process.stdout.write('\n');
this.streamBuffer = '';
}
}
}
// โ”€โ”€โ”€ Header / Banner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
export function renderBanner(): void {
console.log(`
${cyan(bold('โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ'))}
${cyan(bold('โ”‚'))} ${bold('โšก AI Harness')} ${gray('v0.1.0')} ${cyan(bold('โ”‚'))}
${cyan(bold('โ”‚'))} ${dim('model-agnostic CLI agent runtime')} ${cyan(bold('โ”‚'))}
${cyan(bold('โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ'))}
`);
}
// โ”€โ”€โ”€ Metrics Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
export function renderMetrics(metrics: {
modelCalls: number; toolCalls: number; totalTokens: number;
estimatedCostUsd: number; totalDurationMs: number; toolSuccessRate: number;
}): void {
const content = [
`${bold('Model calls:')} ${metrics.modelCalls}`,
`${bold('Tool calls:')} ${metrics.toolCalls} (${Math.round(metrics.toolSuccessRate * 100)}% success)`,
`${bold('Total tokens:')} ${metrics.totalTokens.toLocaleString()}`,
`${bold('Est. cost:')} $${metrics.estimatedCostUsd.toFixed(4)}`,
`${bold('Duration:')} ${(metrics.totalDurationMs / 1000).toFixed(1)}s`,
].join('\n');
console.log('\n' + box('Metrics', content, gray));
}