src/observability/renderer.ts
Rama main · réplica de lectura del repositorio original.
import { cleanJson, redactText, redactValue } from './redaction.ts';
import type { ObservableEvent } from './types.ts';
import type { PublicationRecord, PublicationView } from './publication.ts';
import type { RunSummary } from './run-summary.ts';
import type { ListedRun } from './run-resolver.ts';
export type RenderMode = 'readable' | 'compact' | 'verbose' | 'json';
const MAX_RENDERED_VALUE = 1200;
const MAX_RENDERED_OBJECT_KEYS = 32;
const MAX_RENDERED_ARRAY_ITEMS = 20;
function clock(value: string) {
return value.length >= 19 && value[10] === 'T' ? value.slice(11, 19) : value;
}
function icon(event: ObservableEvent) {
if (event.kind === 'error') return '✖';
if (event.kind === 'warning') return '⚠';
if (event.kind === 'result') return event.status?.includes('completed') ? '✓' : '◆';
if (event.kind === 'tool') {
if (event.sourceEvent === 'tool_end' && (event.title.startsWith('TOOL ERROR') || event.detail?.error || event.detail?.exit_code && event.detail.exit_code !== 0)) return '✖';
return event.sourceEvent === 'tool_end' ? '✓' : '●';
}
if (event.kind === 'git') return event.sourceEvent?.endsWith('confirmed') ? '✓' : '📦';
if (event.kind === 'validation') return '🧪';
if (event.kind === 'thinking') return '💭';
if (event.kind === 'model') return '🤖';
if (event.kind === 'phase') return '◆';
return '▶';
}
function bounded(value: unknown, depth = 0): unknown {
if (typeof value === 'string') {
const text = redactText(value);
return text.length > MAX_RENDERED_VALUE ? `${text.slice(0, MAX_RENDERED_VALUE)}…[TRUNCATED]` : text;
}
if (Array.isArray(value)) {
const items = value.slice(0, MAX_RENDERED_ARRAY_ITEMS).map(item => bounded(item, depth + 1));
if (value.length > MAX_RENDERED_ARRAY_ITEMS) items.push(`[${value.length - MAX_RENDERED_ARRAY_ITEMS} items truncated]`);
return items;
}
if (depth < 4 && value && typeof value === 'object') {
const entries = Object.entries(value).slice(0, MAX_RENDERED_OBJECT_KEYS).map(([key, child]) => [key, bounded(child, depth + 1)]);
if (Object.keys(value).length > MAX_RENDERED_OBJECT_KEYS) entries.push(['_truncated', true]);
return Object.fromEntries(entries);
}
if (depth >= 4 && value && typeof value === 'object') return '[OBJECT TRUNCATED]';
return value;
}
function scalar(value: unknown) {
if (typeof value === 'string') return redactText(value.replaceAll('\n', '\\n')).slice(0, MAX_RENDERED_VALUE);
if (value === null || typeof value === 'number' || typeof value === 'boolean') return String(value);
return redactText(JSON.stringify(bounded(redactValue(value))) ?? 'null');
}
function boundedText(value: unknown) {
return String(bounded(value));
}
function publicationLine(record: PublicationRecord) {
const parts = [`${record.purpose}: ${record.state}`];
if (record.status && record.status !== record.state) parts.push(`status=${record.status}`);
parts.push(`classification=${record.classification}`);
if (record.httpStatus !== null && record.httpStatus !== undefined) parts.push(`http=${record.httpStatus}`);
if (record.errorName) parts.push(`error=${record.errorName}`);
if (record.errorCategory) parts.push(`category=${record.errorCategory}`);
if (record.retryAfterMs !== null && record.retryAfterMs !== undefined) parts.push(`retry_after_ms=${record.retryAfterMs}`);
if (record.remoteId !== undefined) parts.push(`remote=${record.remoteId}`);
return parts.join(' ');
}
function publicationLines(publication: PublicationView, verbose = false) {
if (!verbose) {
const notice = (publication.notice?.state !== 'delivered' ? publication.notice : undefined)
?? publication.records.find(record => record.state !== 'delivered')
?? publication.notice
?? publication.records.at(-1);
if (!notice) return [];
const parts = [publicationLine(notice)];
if (notice.httpStatus !== null && notice.httpStatus !== undefined) parts.push(`HTTP ${notice.httpStatus}`);
if (notice.errorMessage) parts.push(`reason: ${boundedText(notice.errorMessage)}`);
if (notice.documentationUrl) parts.push(`documentation: ${boundedText(notice.documentationUrl)}`);
if (notice.requestId) parts.push(`request: ${boundedText(notice.requestId)}`);
const lines = [parts.join(' · ')];
if (publication.blockedWithoutDelivery) lines.push('TASK TERMINAL, DELIVERY NOT CONFIRMED — the run ended but the user was not reached.');
return lines;
}
const lines = ['', 'Publication'];
for (const record of publication.records) {
lines.push(` ${boundedText(publicationLine(record))}`);
if (record.errorMessage) lines.push(` reason: ${boundedText(record.errorMessage)}`);
if (record.documentationUrl) lines.push(` documentation: ${boundedText(record.documentationUrl)}`);
if (record.requestId) lines.push(` request: ${boundedText(record.requestId)}`);
lines.push(` artifacts: ${boundedText(record.artifacts.join(', ') || 'none')}`);
if (record.events.length) lines.push(` events: ${boundedText(record.events.join(', '))}`);
}
if (publication.blockedWithoutDelivery) lines.push(' TASK TERMINAL, DELIVERY NOT CONFIRMED — the run ended but the user was not reached.');
return lines;
}
function manifestRepo(manifest: ListedRun['manifest']) {
for (const key of ['repository', 'repository_path', 'path_with_namespace', 'full_name', 'repo']) {
if (typeof manifest?.[key] === 'string') return manifest[key];
}
return undefined;
}
function details(event: ObservableEvent, verbose: boolean) {
if (!event.detail) return '';
const entries = Object.entries(event.detail).filter(([, value]) => value !== undefined);
const selected = verbose ? entries : entries.filter(([key]) => !['task', 'index', 'provider_id', 'request', 'attempt', 'body_sha256', 'raw_sha256', 'output_sha256', 'args_sha256'].includes(key));
return selected.slice(0, verbose ? 32 : 12).map(([key, value]) => `${key}=${scalar(value)}`).join(' ');
}
export function renderObservableEvent(event: ObservableEvent, mode: RenderMode = 'readable') {
const safe = redactValue(event) as ObservableEvent;
if (mode === 'json') return cleanJson(bounded(safe));
const detail = details(safe, mode === 'verbose');
if (mode === 'compact') return `${clock(boundedText(safe.time))} ${boundedText(safe.title)}${detail ? ` ${detail}` : ''}`;
return `${clock(boundedText(safe.time))} ${icon(safe)} ${boundedText(safe.title)}${detail ? `\n ${detail}` : ''}`;
}
function summaryLines(summary: RunSummary) {
return [
'PatchPaw Agent Status',
'',
`Run ${boundedText(summary.runId)}`,
`Repo/PR ${boundedText(summary.repo ? `${summary.repo}#${summary.prNumber ?? '?'}` : 'unknown')}`,
`Execution ${summary.executionId ?? 'unknown'}`,
`Worker ${summary.worker}`,
`Phase ${boundedText(summary.phase ?? 'unknown')}`,
`Task ${boundedText(summary.task ?? 'unknown')}`,
`Permission ${boundedText(summary.permission ?? 'unknown')}`,
`Status ${boundedText(summary.status)}`,
`Current ${boundedText(summary.currentActivity)}`,
`Started ${boundedText(summary.startedAt ?? 'unknown')}`,
`Last event ${boundedText(summary.lastEventAt ?? 'unknown')}`,
'',
`Model ${boundedText(summary.model ?? 'unknown')}`,
`Provider ${boundedText(summary.provider ?? 'unknown')}`,
`Requests ${summary.requests}`,
`Attempts ${summary.attempts}`,
`Retries ${summary.retries}`,
`Tools ${summary.tools}`,
`Tool errors ${summary.toolErrors}`,
`Errors ${summary.errors}`,
`Validations ${summary.validations}`,
`CI polls ${summary.ciPolls}`,
`Commits ${summary.commits}`,
`Pushes ${summary.pushes}`,
`Confirmed ${summary.remoteConfirmations}`,
`Workspace ${boundedText(summary.workspace ?? 'unknown')}`,
`Head ${boundedText(summary.head ?? 'unknown')}`,
...(summary.lastCommit ? [`Last commit ${boundedText(summary.lastCommit)}`] : []),
...(summary.lastPush ? [`Last push ${boundedText(summary.lastPush)}`] : []),
...(summary.result ? ['', `Result ${scalar(resultProjection(summary.result))}`] : []),
];
}
function resultProjection(result: Record<string, unknown>) {
const projection: Record<string, unknown> = {};
for (const key of ['status', 'run_id', 'failed_phase', 'failure_code', 'final_head_sha', 'duration_ms', 'message']) {
if (result[key] !== undefined) projection[key] = result[key];
}
const failure = result.failure;
if (typeof failure === 'string') projection.failure = failure;
else if (failure && typeof failure === 'object') {
const value = failure as Record<string, unknown>;
projection.failure = Object.fromEntries(['code', 'category', 'message', 'phase'].filter(key => value[key] !== undefined).map(key => [key, value[key]]));
}
return projection;
}
export function renderSummary(summary: RunSummary, mode: RenderMode = 'readable') {
const safe = redactValue(summary) as RunSummary;
if (mode === 'json') return cleanJson(bounded({ ...safe, result: safe.result ? resultProjection(safe.result) : undefined }));
if (mode === 'compact') return `${boundedText(safe.runId)} ${boundedText(safe.status)} phase=${boundedText(safe.phase ?? 'unknown')} current=${boundedText(safe.currentActivity)} tools=${safe.tools} errors=${safe.errors}`;
const lines = summaryLines(safe);
if (safe.publication) lines.push(...publicationLines(safe.publication, mode === 'verbose'));
return lines.join('\n');
}
export function renderObserverHeader(runId: string, mode: RenderMode = 'readable') {
if (mode === 'json') return '';
if (mode === 'compact') return `PatchPaw Agent Observer run=${boundedText(runId)}`;
return `PatchPaw Agent Observer\nrun=${boundedText(runId)}\n${'─'.repeat(60)}`;
}
export function renderRunList(runs: ListedRun[], mode: RenderMode = 'readable') {
if (mode === 'json') return runs.map(run => cleanJson(bounded({
runId: run.runId,
startedAt: run.startedAt,
repo: manifestRepo(run.manifest),
changeNumber: run.manifest?.pr_number,
task: Array.isArray(run.manifest?.task_chain) ? run.manifest.task_chain.at(-1) : undefined,
status: typeof run.result?.status === 'string' ? run.result.status : 'active/unknown',
error: run.error,
})));
if (mode === 'compact') return runs.map(run => {
const task = Array.isArray(run.manifest?.task_chain) && typeof run.manifest.task_chain.at(-1) === 'string' ? run.manifest.task_chain.at(-1) : 'unknown';
const repo = manifestRepo(run.manifest);
const target = typeof repo === 'string' ? `${repo}#${run.manifest?.pr_number ?? '?'}` : 'corrupt manifest';
return `${boundedText(run.startedAt ?? 'unknown')} ${boundedText(run.runId ?? 'corrupt')} task=${boundedText(task)} target=${boundedText(target)} status=${boundedText(typeof run.result?.status === 'string' ? run.result.status : 'active/unknown')}`;
});
const rows = ['TIME RUN TASK STATUS REPO / PR'];
for (const run of runs) {
const task = Array.isArray(run.manifest?.task_chain) && typeof run.manifest.task_chain.at(-1) === 'string' ? boundedText(run.manifest.task_chain.at(-1)) : 'unknown';
const status = typeof run.result?.status === 'string' ? run.result.status : 'active/unknown';
const repoValue = manifestRepo(run.manifest);
const repo = typeof repoValue === 'string' ? `${repoValue}#${run.manifest?.pr_number ?? '?'}` : 'corrupt manifest';
rows.push(`${boundedText(run.startedAt ?? 'unknown')} ${boundedText(run.runId ?? 'corrupt')} ${String(task).padEnd(10)} ${boundedText(status).padEnd(12)} ${boundedText(repo)}`);
if (run.error) rows.push(` warning: ${boundedText(redactText(run.error))}`);
}
return [rows.join('\n')];
}
export function renderMalformed(line: number, message: string, excerpt: string, mode: RenderMode = 'readable') {
const safeMessage = boundedText(redactText(message));
const safeExcerpt = boundedText(redactText(excerpt));
const value = { kind: 'warning', title: 'MALFORMED TRACE LINE', line, message: safeMessage, excerpt: safeExcerpt };
return mode === 'json' ? cleanJson(value) : `⚠ MALFORMED TRACE LINE line=${line} message=${safeMessage} excerpt=${safeExcerpt}`;
}
export const DETACHED_MESSAGE = 'Observer detached. PatchPaw Agent was not stopped.';
export function renderObserverNotice(kind: 'waiting' | 'detached', mode: RenderMode) {
const title = kind === 'waiting' ? 'WAITING' : 'DETACHED';
const message = kind === 'waiting' ? 'No active local run. Waiting for the next PatchPaw run...' : DETACHED_MESSAGE;
return mode === 'json' ? cleanJson({ kind: 'observer', title, message }) : message;
}