feat: add live H1-H7 trace explorer

This commit is contained in:
thanhnv
2026-07-10 23:17:23 +09:00
parent 7cb592a32f
commit 1345d4c930
11 changed files with 496 additions and 28 deletions
@@ -1,6 +1,7 @@
// Read-only Ops Console API. Every handler returns the standard ok() envelope. No writes,
// no auth (loopback-bound, "Đọc ≠ Ghi"); management/RBAC is Plan-13 Track 2/3 (future).
import { Controller, Get, Inject, Param, Query } from '@nestjs/common';
import { Controller, Get, Inject, Param, Query, Req, Res } from '@nestjs/common';
import type { Request, Response } from 'express';
import { ok } from '../common/api-response.js';
import { TelemetryService } from './telemetry.service.js';
@@ -27,6 +28,38 @@ export class TelemetryController {
return ok(this.svc.run(traceId));
}
@Get('runs/:traceId/graph')
traceGraph(@Param('traceId') traceId: string) {
return ok(this.svc.traceGraph(traceId));
}
@Get('runs/:traceId/events')
traceEvents(@Param('traceId') traceId: string, @Req() req: Request, @Res() res: Response) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
let previous = '';
const publish = () => {
const graph = this.svc.traceGraph(traceId);
const serialized = JSON.stringify(graph);
if (serialized !== previous) {
res.write(`event: trace\ndata: ${serialized}\n\n`);
previous = serialized;
} else {
res.write(': heartbeat\n\n');
}
};
const timer = setInterval(publish, 750);
const close = () => {
clearInterval(timer);
if (!res.writableEnded) res.end();
};
req.on('close', close);
publish();
}
@Get('governance')
governance() {
return ok(this.svc.governance());
@@ -9,6 +9,36 @@ import { APP_ROOT, PATHS, isStale, metricsAgeSeconds, STALE_AFTER_S } from '../c
import { readJsonl, readJson, readHead, readTrace } from './telemetry.reader.js';
type Row = Record<string, any>;
export type HarnessGateStatus = 'queued' | 'running' | 'pass' | 'warning' | 'blocked' | 'error' | 'skipped';
export interface HarnessTraceEvent {
timestamp: string;
trace_id: string;
gate_id: string;
status: HarnessGateStatus;
reason: string;
evidence: Record<string, unknown>;
}
export interface HarnessGateNode {
id: string;
title: string;
description: string;
status: HarnessGateStatus;
reason: string;
updated_at: string | null;
evidence: Record<string, unknown>;
events: HarnessTraceEvent[];
}
const HARNESS_GATES = [
{ id: 'H1-context', title: 'H1 · Context', description: 'Prompt contract, mode and risk classification' },
{ id: 'H2-tool', title: 'H2 · Tool', description: 'Allowlisted source and tool preparation' },
{ id: 'H3-eval', title: 'H3 · Eval', description: 'Grounded synthesis and quality evaluation' },
{ id: 'H4-security', title: 'H4 · Security', description: 'Input and output security boundary' },
{ id: 'H5-governance', title: 'H5 · Governance', description: 'Decision policy and append-only audit' },
{ id: 'H6-agentops', title: 'H6 · AgentOps', description: 'Runtime, token, cost and failure telemetry' },
{ id: 'H7-orchestration', title: 'H7 · Orchestration', description: 'Final governed outcome and certification' },
] as const;
type ChatReplayResult = {
ok: boolean;
decision: string;
@@ -27,6 +57,22 @@ const recent = (rows: Row[], n: number) => rows.slice(-n).reverse();
const arr = (v: any): any[] => (Array.isArray(v) ? v : []);
const pct = (part: number, total: number) => (total > 0 ? Math.round((part / total) * 1000) / 10 : 0);
function gateStatus(value: unknown): HarnessGateStatus {
const status = String(value ?? '').toLowerCase();
if (['success', 'pass', 'passed', 'allow', 'allowed', 'answered'].includes(status)) return 'pass';
if (['warn', 'warning'].includes(status)) return 'warning';
if (['block', 'blocked', 'deny', 'denied'].includes(status)) return 'blocked';
if (['fail', 'failed', 'error'].includes(status)) return 'error';
if (status === 'running') return 'running';
if (status === 'skipped') return 'skipped';
return 'queued';
}
function safeTraceId(traceId: string): string {
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(traceId)) return '';
return traceId;
}
function artifact(path: string, source: string, verifiedWhenPresent = true) {
const present = existsSync(path);
const runAt = present ? statSync(path).mtime.toISOString() : null;
@@ -227,6 +273,82 @@ export class TelemetryService {
return trace ? { found: true, trace } : { found: false, trace: null };
}
traceGraph(traceId: string) {
const safeId = safeTraceId(traceId);
const events = safeId
? readJsonl<HarnessTraceEvent>(join(PATHS.traceEventDir, `${safeId}.jsonl`))
.filter((event) => event.trace_id === safeId && HARNESS_GATES.some((gate) => gate.id === event.gate_id))
.map((event) => ({
timestamp: String(event.timestamp ?? ''),
trace_id: safeId,
gate_id: String(event.gate_id ?? ''),
status: gateStatus(event.status),
reason: String(event.reason ?? ''),
evidence: event.evidence && typeof event.evidence === 'object' ? event.evidence : {},
}))
: [];
if (safeId && events.length === 0) {
const legacyTrace = readTrace(PATHS.traceDir, safeId);
if (legacyTrace) {
const gateId = String(legacyTrace.harness ?? '');
if (HARNESS_GATES.some((gate) => gate.id === gateId)) {
events.push({
timestamp: String(legacyTrace.timestamp ?? ''),
trace_id: safeId,
gate_id: gateId,
status: gateStatus(legacyTrace.status ?? legacyTrace.action),
reason: `Legacy ${gateId} trace`,
evidence: {
mode: legacyTrace.mode ?? null,
action: legacyTrace.action ?? null,
risk_level: legacyTrace.risk_level ?? null,
},
});
}
}
for (const metric of readJsonl<Row>(PATHS.metrics).filter((row) => row.trace_id === safeId)) {
events.push({
timestamp: String(metric.timestamp ?? ''),
trace_id: safeId,
gate_id: 'H6-agentops',
status: gateStatus(metric.status),
reason: String(metric.step ?? 'Legacy runtime metric'),
evidence: {
latency_ms: metric.latency_ms ?? null,
total_tokens: metric.total_tokens ?? null,
cost_estimate: metric.cost_estimate ?? null,
synthesis_mode: metric.synthesis_mode ?? null,
},
});
}
}
const nodes: HarnessGateNode[] = HARNESS_GATES.map((gate) => {
const gateEvents = events.filter((event) => event.gate_id === gate.id);
const latest = gateEvents[gateEvents.length - 1];
return {
...gate,
status: latest?.status ?? 'queued',
reason: latest?.reason ?? 'Waiting for evidence',
updated_at: latest?.timestamp || null,
evidence: latest?.evidence ?? {},
events: gateEvents,
};
});
const lastEvent = events[events.length - 1];
const outcome = nodes.find((node) => node.id === 'H7-orchestration');
return {
found: events.length > 0,
trace_id: safeId || traceId,
updated_at: lastEvent?.timestamp || null,
terminal: outcome?.status === 'pass' || outcome?.status === 'blocked' || outcome?.status === 'error',
progress: nodes.filter((node) => node.status !== 'queued').length,
nodes,
events,
};
}
governance() {
const audit = readJsonl(PATHS.audit);
const byDecision: Record<string, number> = {};