src/observability/normalize.ts
Rama main · réplica de lectura del repositorio original.
import { redactValue } from './redaction.ts';
import type { ObservableEvent, RawTraceEvent } from './types.ts';
function text(value: unknown, fallback = '') {
return typeof value === 'string' ? value : fallback;
}
function number(value: unknown) {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function object(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function detail(source: RawTraceEvent, keys: string[]) {
const selected = Object.fromEntries(keys.flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
return Object.keys(selected).length ? redactValue(selected) as Record<string, unknown> : undefined;
}
const MODEL_EVENTS = new Set(['model_request', 'provider_attempt', 'provider_retry', 'provider_error', 'model_response', 'model_step']);
const TOOL_EVENTS = new Set(['tool_start', 'tool_end']);
const GIT_EVENTS = new Set(['git', 'repair_commit', 'repair_push', 'repair_push_confirmed', 'conflict_repair_push_confirmed', 'gitlab_conflict_repair_push_confirmed']);
const VALIDATION_EVENTS = new Set(['validation', 'repair_verification', 'repair_verification_requested', 'ci_observation_wait', 'ci_observation_complete']);
const WORKSPACE_EVENTS = new Set(['workspace_change', 'workspace_evidence_captured', 'workspace_disposed', 'workspace_resumed', 'worktree_created',
'workspace_path_disposed', 'stale_workspace_disposed', 'repo_cache_initialized', 'repo_cache_fetched', 'repo_cache_remote_added', 'repo_cache_remote_updated']);
const ERROR_EVENTS = new Set(['provider_error', 'run_error', 'human_stop_closeout_failed', 'workspace_dispose_failed', 'workspace_retain_failed',
'stale_workspace_dispose_failed', 'superseded_workspace_dispose_failed', 'repair_closeout_error']);
const APPROVAL_EVENTS = new Set(['approval_plan_claim_claimed', 'approval_plan_claim_reconciled', 'approval_plan_claim_settled',
'conflict_approval_claimed', 'conflict_approval_interrupted']);
const PUBLICATION_EVENTS = new Set(['run_notice_published', 'run_notice_failed', 'run_notice_delayed_delivery_finalized',
'review_published', 'review_delayed_delivery_finalized', 'review_delayed_delivery_preserved', 'review_settled_resume',
'conversation_reply_published', 'conflict_proposal_published', 'conflict_repair_delayed_delivery_finalized',
'conflict_repair_recovery_state_finalized', 'conflict_repair_result_reused', 'close_refusal_notice_pending',
'close_refused_active_task', 'close_refusal_retire_degraded']);
const HUMAN_EVENTS = new Set(['human_help_requested', 'human_stop_closeout_started']);
const RESULT_EVENTS = new Set(['execution_completed', 'execution_paused', 'budget_exhausted', 'agent_final_answer_completed',
'agent_final_answer_exhausted', 'task_closeout_submitted', 'task_closeout_fallback', 'repair_closeout_started']);
const RUN_EVENTS = new Set(['execution_started', 'execution_resumed', 'recovery_started', 'resume_candidate_found', 'resume_candidate_rejected']);
const PHASE_EVENTS = new Set(['phase', 'task_turn_start', 'task_turn_end', 'agent_final_answer_started', 'budget_warning', 'budget_critical', 'repair_started']);
function eventKind(event: string): ObservableEvent['kind'] {
if (MODEL_EVENTS.has(event)) return event === 'provider_error' ? 'error' : 'model';
if (TOOL_EVENTS.has(event)) return 'tool';
if (GIT_EVENTS.has(event)) return 'git';
if (event === 'ci_poll') return 'ci_poll';
if (event === 'ci_failure_evidence') return 'ci_failure_evidence';
if (VALIDATION_EVENTS.has(event)) return 'validation';
if (WORKSPACE_EVENTS.has(event)) return event.endsWith('_failed') ? 'error' : 'workspace';
if (APPROVAL_EVENTS.has(event)) return 'approval';
if (PUBLICATION_EVENTS.has(event)) return 'publication';
if (HUMAN_EVENTS.has(event)) return 'human';
if (ERROR_EVENTS.has(event)) return 'error';
if (RESULT_EVENTS.has(event)) return 'result';
if (RUN_EVENTS.has(event)) return 'run';
if (PHASE_EVENTS.has(event)) return 'phase';
return 'warning';
}
function titleFor(event: string, source: RawTraceEvent) {
if (event === 'phase') return `PHASE ${text(source.phase, 'unknown')}`;
if (event === 'tool_start') return `TOOL ${text(source.tool, 'unknown')}`;
if (event === 'tool_end') {
const exitCode = number(source.exit_code);
return source.error || exitCode !== undefined && exitCode !== 0 ? `TOOL ERROR ${text(source.tool, 'unknown')}` : `TOOL RESULT ${text(source.tool, 'unknown')}`;
}
if (event === 'model_request') return 'MODEL REQUEST';
if (event === 'model_response') return 'MODEL RESPONSE';
if (event === 'provider_attempt') return 'PROVIDER ATTEMPT';
if (event === 'provider_retry') return 'PROVIDER RETRY';
if (event === 'provider_error') return 'PROVIDER ERROR';
if (event === 'model_step') return 'MODEL STEP';
if (event === 'validation') return 'VALIDATION';
if (event === 'ci_poll') return 'CI POLL';
if (event === 'ci_failure_evidence') return 'CI FAILURE EVIDENCE';
if (event === 'repair_commit') return 'COMMIT';
if (event === 'repair_push') return 'PUSH';
if (event === 'repair_push_confirmed' || event.endsWith('push_confirmed')) return 'REMOTE CONFIRMED';
if (event === 'git') return 'GIT';
if (event === 'execution_started') return 'RUN STARTED';
if (event === 'execution_completed') return 'RUN COMPLETED';
if (event === 'execution_paused') return 'RUN PAUSED';
if (event === 'execution_resumed') return 'RUN RESUMED';
if (event === 'run_error') return 'RUN ERROR';
if (event === 'task_turn_start') return `TASK TURN ${text(source.task, 'start')}`;
if (event === 'task_turn_end') return `TASK TURN ${text(source.task, 'end')}`;
if (event === 'run_notice_published') return 'RUN NOTICE PUBLISHED';
if (event === 'run_notice_failed') return 'RUN NOTICE FAILED';
if (event === 'review_published') return 'REVIEW PUBLISHED';
if (event === 'conversation_reply_published') return 'REPLY PUBLISHED';
if (event === 'approval_plan_claim_claimed') return 'APPROVAL CLAIMED';
if (event === 'approval_plan_claim_settled') return `APPROVAL ${text(source.phase, 'settled').toUpperCase()}`;
if (event === 'human_help_requested') return 'HUMAN HELP REQUESTED';
if (event === 'budget_exhausted') return 'BUDGET EXHAUSTED';
if (event === 'agent_final_answer_started') return 'FINAL ANSWER FALLBACK STARTED';
if (event === 'agent_final_answer_completed') return 'FINAL ANSWER FALLBACK COMPLETED';
if (event === 'agent_final_answer_exhausted') return 'FINAL ANSWER FALLBACK EXHAUSTED';
return event.replaceAll('_', ' ').toUpperCase();
}
function selectedDetail(event: string, source: RawTraceEvent) {
if (event === 'ci_poll') return detail(source, ['sha', 'state', 'items', 'workflowRuns']);
if (event === 'ci_failure_evidence') return detail(source, ['workflow', 'job', 'id', 'conclusion', 'steps', 'log', 'log_error_status']);
if (PUBLICATION_EVENTS.has(event)) return detail(source, ['task', 'phase', 'purpose', 'status', 'delivery_id', 'sequence', 'remote_id', 'remote_url', 'published_at', 'reused', 'remote_adopted',
'http_status', 'documentation_url', 'request_id', 'retry_after_ms', 'last_error', 'reason', 'message', 'code', 'category', 'classification', 'failure', 'failure_code', 'failure_category',
'sha', 'head_sha', 'previous_head', 'branch', 'source', 'workspace', 'execution_id', 'duration_ms', 'kind', 'plan_id', 'plan_revision', 'source_comment_id', 'recovered', 'role', 'attempts']);
const keys = event === 'tool_start' || event === 'tool_end'
? ['task', 'index', 'tool', 'args', 'args_chars', 'args_sha256', 'duration_ms', 'result_chars', 'output_excerpt', 'output_sha256', 'output_truncated', 'exit_code', 'truncated', 'error']
: event === 'model_request' || event === 'model_response' || event === 'provider_attempt' || event === 'provider_retry' || event === 'provider_error'
? ['task', 'provider', 'provider_id', 'model', 'request', 'attempt', 'status', 'duration_ms', 'body_chars', 'body_sha256', 'body_excerpt', 'output_budget', 'raw_chars', 'raw_sha256', 'raw_excerpt', 'raw_truncated', 'reason', 'code', 'retry_after_ms']
: event === 'git'
? ['args', 'exitCode', 'timedOut', 'stdout_chars', 'stdout', 'stdout_sha256', 'stderr_chars', 'stderr', 'truncated']
: event === 'validation'
? ['command', 'exitCode', 'timedOut', 'stdout_chars', 'stdout', 'stderr_chars', 'stderr', 'truncated']
: ['task', 'phase', 'status', 'reason', 'message', 'failure_code', 'failure_category', 'failure', 'sha', 'start_head', 'previous_head',
'head_sha', 'branch', 'source', 'workspace', 'execution_id', 'duration_ms', 'finish_reason', 'usage', 'kind'];
return detail(source, keys);
}
function explicitThinking(source: RawTraceEvent) {
for (const key of ['reasoning', 'thinking', 'reasoning_content', 'reasoning_summary']) {
const value = source[key];
if ((typeof value === 'string' && value.trim().length > 0)
|| (Array.isArray(value) && value.length > 0)
|| (value && typeof value === 'object' && Object.keys(value).length > 0)) {
return detail(source, [key]);
}
}
return undefined;
}
export function normalizeObservableEvent(source: unknown, runId: string): ObservableEvent | null {
const raw = object(source);
const event = text(raw.event);
if (!event) return null;
const time = text(raw.time, new Date(0).toISOString());
const executionId = number(raw.execution_id);
const thinking = explicitThinking(raw);
const kind = thinking ? 'thinking' : eventKind(event);
return { time, runId, ...(executionId === undefined ? {} : { executionId }), sourceEvent: event,
kind, title: thinking ? 'MODEL REASONING' : titleFor(event, raw),
...(text(raw.status) ? { status: text(raw.status) } : {}),
...(thinking ?? selectedDetail(event, raw) ? { detail: thinking ?? selectedDetail(event, raw) } : {}) };
}