import { appendFile, mkdir } from 'fs/promises' import { join } from 'path' import type { SourceAgentEvent, TaskRun } from './types.js' export type RunEventType = | 'run_started' | 'agent_step_started' | 'agent_step_finished' | 'agent_step_error' | 'agent_recovery_started' | 'agent_recovery_finished' | 'agent_event' | 'submission_validation_failed' | 'submission_validation_passed' | 'run_warning' | 'judge_started' | 'judge_finished' | 'run_finished' export type RunEvent = { timestamp: string run_id: string task_id: string type: RunEventType judge_round?: number agent_step?: number message?: string details?: unknown } export type RunEventLoggerInput = { taskRun: TaskRun verbose?: boolean } function shorten(value: string, maxLength = 240): string { return value.length <= maxLength ? value : `${value.slice(0, maxLength)}...` } export function summarizeAgentEvent(event: SourceAgentEvent): Record { if (event.type === 'assistant_text') { return { type: event.type, text_length: event.text.length } } if (event.type === 'tool_call') { return { type: event.type, tool: event.tool, toolUseId: event.toolUseId } } if (event.type === 'tool_result') { return { type: event.type, ok: event.ok, toolUseId: event.toolUseId, text_tail: event.text?.slice(-1000), } } if (event.type === 'policy_deny') { return { type: event.type, tool: event.tool, reason: event.reason } } if (event.type === 'trajectory_warning') { return { type: event.type, code: event.code, message: event.message, details: event.details, } } if (event.type === 'run_warning') { return { type: event.type, code: event.code, message: event.message, details: event.details, } } if (event.type === 'submission_validation_failed') { return { type: event.type, ok: event.result.ok, issue_count: event.result.issues.length, normalized_files: event.result.normalizedFiles, issues: event.result.issues, } } if (event.type === 'submission_validation_passed') { return { type: event.type, ok: event.result.ok, normalized_files: event.result.normalizedFiles, } } if (event.type === 'agent_result') { return { type: event.type, subtype: event.subtype, stopReason: event.stopReason, isError: event.isError, durationMs: event.durationMs, errors: event.errors, } } return { type: event.type, summary: event.summary, files: event.files } } function formatConsoleEvent(event: RunEvent): string { const prefix = `[${event.timestamp}] ${event.type}` const round = event.judge_round ? ` round=${event.judge_round}` : '' const step = event.agent_step ? ` step=${event.agent_step}` : '' const message = event.message ? ` ${shorten(event.message)}` : '' return `${prefix}${round}${step}${message}` } export class RunEventLogger { readonly path: string private readonly taskRun: TaskRun private readonly verbose: boolean constructor(input: RunEventLoggerInput) { this.taskRun = input.taskRun this.verbose = Boolean(input.verbose) this.path = join(input.taskRun.logsDir, 'run_events.jsonl') } async log( type: RunEventType, event: Omit, 'timestamp' | 'run_id' | 'task_id' | 'type'> = {}, ): Promise { const record: RunEvent = { timestamp: new Date().toISOString(), run_id: this.taskRun.runId, task_id: this.taskRun.taskId, type, ...event, } await mkdir(this.taskRun.logsDir, { recursive: true }) await appendFile(this.path, `${JSON.stringify(record)}\n`, 'utf8') if (this.verbose) { process.stderr.write(`${formatConsoleEvent(record)}\n`) } } }