feat(control-panel): embed H6 telemetry in goal view
This commit is contained in:
+94
-4
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import type { GoalJob, GoalStage, HarnessGateNode, HarnessTraceGraph } from '../../lib/api';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { h6ReportExportUrl, type GoalJob, type GoalStage, type H6Report, type HarnessGateNode, type HarnessTraceGraph } from '../../lib/api';
|
||||||
import { StatusBadge } from '../ui/Card';
|
import { StatusBadge } from '../ui/Card';
|
||||||
|
|
||||||
interface ModelInteractionDiagramProps {
|
interface ModelInteractionDiagramProps {
|
||||||
@@ -7,6 +8,10 @@ interface ModelInteractionDiagramProps {
|
|||||||
localStage?: GoalStage;
|
localStage?: GoalStage;
|
||||||
cloudStage?: GoalStage;
|
cloudStage?: GoalStage;
|
||||||
trace?: HarnessTraceGraph;
|
trace?: HarnessTraceGraph;
|
||||||
|
h6RunReport?: H6Report;
|
||||||
|
h6ProjectReport?: H6Report;
|
||||||
|
h6Loading?: boolean;
|
||||||
|
h6Error?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GATE_ORDER = ['H1-context', 'H2-tool', 'H3-eval', 'H4-security', 'H5-governance', 'H6-agentops', 'H7-orchestration'];
|
const GATE_ORDER = ['H1-context', 'H2-tool', 'H3-eval', 'H4-security', 'H5-governance', 'H6-agentops', 'H7-orchestration'];
|
||||||
@@ -23,6 +28,20 @@ function formatReason(reason: string): string {
|
|||||||
return reason.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
return reason.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDuration(milliseconds: number): string {
|
||||||
|
if (!milliseconds) return '0 ms';
|
||||||
|
if (milliseconds < 1_000) return `${Math.round(milliseconds)} ms`;
|
||||||
|
const totalSeconds = Math.round(milliseconds / 1_000);
|
||||||
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
return `${minutes}m ${totalSeconds % 60}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCost(value: number): string {
|
||||||
|
if (!value) return '$0.0000';
|
||||||
|
return `$${value.toFixed(value < 0.01 ? 6 : 4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function AgentAvatar({ label, role, model, status, selected, onClick }: {
|
function AgentAvatar({ label, role, model, status, selected, onClick }: {
|
||||||
label: string;
|
label: string;
|
||||||
role: string;
|
role: string;
|
||||||
@@ -62,7 +81,77 @@ function ControlGate({ node, selected, onClick }: { node: HarnessGateNode; selec
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GateDetail({ node }: { node: HarnessGateNode }) {
|
function H6Summary({ node, goal, runReport, projectReport, loading, error }: {
|
||||||
|
node: HarnessGateNode;
|
||||||
|
goal: GoalJob;
|
||||||
|
runReport?: H6Report;
|
||||||
|
projectReport?: H6Report;
|
||||||
|
loading: boolean;
|
||||||
|
error: boolean;
|
||||||
|
}) {
|
||||||
|
const reportLink = `/reports/h6?run=${encodeURIComponent(goal.trace_id)}`;
|
||||||
|
const projectLink = `/reports/h6?project=${encodeURIComponent(goal.project)}`;
|
||||||
|
|
||||||
|
if (loading && !runReport) {
|
||||||
|
return <div className="mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3" aria-label="Loading H6 run telemetry">{Array.from({ length: 6 }, (_, index) => <div key={index} className="h-16 animate-pulse rounded-xl bg-white/5 motion-reduce:animate-none" />)}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !runReport) {
|
||||||
|
return <div className="mt-4 rounded-xl border border-amber-300/30 bg-amber-300/10 p-3 text-xs leading-5 text-amber-100">Run telemetry could not be loaded here. <Link to={reportLink} className="font-semibold underline decoration-amber-300/50 underline-offset-4 hover:text-white focus:outline-none focus:ring-2 focus:ring-cyan-300">Open the full H6 report</Link> to inspect the source status.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenTotal = runReport.summary.tokens.provider_total || runReport.summary.tokens.total;
|
||||||
|
const actualCost = runReport.summary.cost_usd.provider_actual;
|
||||||
|
const displayedCost = actualCost || runReport.summary.cost_usd.estimated;
|
||||||
|
const provider = runReport.details.by_provider.slice(0, 2).map((row) => row.key).join(', ') || 'Not attributed';
|
||||||
|
const metrics = [
|
||||||
|
['Max runtime', formatDuration(runReport.summary.latency_ms.max)],
|
||||||
|
['Tokens', tokenTotal.toLocaleString()],
|
||||||
|
[actualCost ? 'Actual cost' : 'Estimated cost', formatCost(displayedCost)],
|
||||||
|
['Retries', runReport.summary.retries.toLocaleString()],
|
||||||
|
['Provider calls', runReport.summary.provider_calls.toLocaleString()],
|
||||||
|
['Freshness', runReport.freshness.status],
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 space-y-3 border-t border-white/10 pt-4">
|
||||||
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
|
<div className="rounded-xl border border-white/10 bg-slate-950/35 p-3">
|
||||||
|
<div className="text-[10px] font-bold uppercase tracking-[0.12em] text-slate-500">Run gate</div>
|
||||||
|
<div className="mt-2 flex items-center justify-between gap-2"><span className="font-mono text-[10px] text-slate-300">{goal.trace_id.slice(0, 12)}…</span><StatusBadge value={node.status} /></div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/10 bg-slate-950/35 p-3">
|
||||||
|
<div className="text-[10px] font-bold uppercase tracking-[0.12em] text-slate-500">Project health</div>
|
||||||
|
<div className="mt-2 flex items-center justify-between gap-2"><span className="truncate text-[11px] font-medium text-slate-300">{goal.project}</span><StatusBadge value={projectReport?.verdict ?? 'no data'} /></div>
|
||||||
|
<div className="mt-1 text-[10px] text-slate-500">{projectReport?.summary.runs ?? 0} matching telemetry record(s)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">{metrics.map(([label, value]) => <div key={label} className="rounded-xl border border-white/10 bg-slate-950/35 px-3 py-2.5"><div className="text-[9px] font-bold uppercase tracking-[0.1em] text-slate-500">{label}</div><div className="mt-1 truncate font-mono text-xs font-semibold capitalize text-white">{value}</div></div>)}</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-white/10 bg-slate-950/35 p-3">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2"><div><div className="text-[10px] font-bold uppercase tracking-[0.12em] text-slate-500">Run telemetry verdict</div><div className="mt-1 text-xs text-slate-300">{runReport.summary.runs} matching record(s) · data quality {runReport.data_quality.status}</div></div><StatusBadge value={runReport.verdict} /></div>
|
||||||
|
<div className="mt-2 truncate text-[10px] text-slate-400" title={provider}>Provider/model: {provider}</div>
|
||||||
|
{runReport.findings[0] && <p className="mt-2 text-[11px] leading-5 text-amber-100">{runReport.findings[0].message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Link to={reportLink} className="rounded-lg bg-cyan-300 px-3 py-2 text-xs font-semibold text-slate-950 transition hover:bg-cyan-200 focus:outline-none focus:ring-2 focus:ring-cyan-300 focus:ring-offset-2 focus:ring-offset-slate-900">Open full H6 report</Link>
|
||||||
|
<a href={h6ReportExportUrl({ run: goal.trace_id }, 'json')} download className="rounded-lg border border-white/15 bg-white/5 px-3 py-2 text-xs font-semibold text-slate-100 transition hover:border-white/30 hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-cyan-300">Download run JSON</a>
|
||||||
|
<Link to={projectLink} className="rounded-lg border border-white/15 bg-white/5 px-3 py-2 text-xs font-semibold text-slate-300 transition hover:border-white/30 hover:bg-white/10 hover:text-white focus:outline-none focus:ring-2 focus:ring-cyan-300">Open project health</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GateDetail({ node, goal, h6RunReport, h6ProjectReport, h6Loading = false, h6Error = false }: {
|
||||||
|
node: HarnessGateNode;
|
||||||
|
goal: GoalJob;
|
||||||
|
h6RunReport?: H6Report;
|
||||||
|
h6ProjectReport?: H6Report;
|
||||||
|
h6Loading?: boolean;
|
||||||
|
h6Error?: boolean;
|
||||||
|
}) {
|
||||||
const tone = statusTone(node.status);
|
const tone = statusTone(node.status);
|
||||||
const evidence = Object.entries(node.evidence).filter(([, value]) => value !== null && value !== undefined).slice(0, 3);
|
const evidence = Object.entries(node.evidence).filter(([, value]) => value !== null && value !== undefined).slice(0, 3);
|
||||||
return (
|
return (
|
||||||
@@ -71,6 +160,7 @@ function GateDetail({ node }: { node: HarnessGateNode }) {
|
|||||||
<p className="mt-3 text-sm leading-6 text-slate-200">{node.description}</p>
|
<p className="mt-3 text-sm leading-6 text-slate-200">{node.description}</p>
|
||||||
<div className="mt-3 rounded-xl bg-slate-950/40 p-3"><div className="text-[10px] font-bold uppercase tracking-[0.12em] text-slate-500">Observed impact</div><div className="mt-1 text-sm font-medium text-white">{formatReason(node.reason)}</div></div>
|
<div className="mt-3 rounded-xl bg-slate-950/40 p-3"><div className="text-[10px] font-bold uppercase tracking-[0.12em] text-slate-500">Observed impact</div><div className="mt-1 text-sm font-medium text-white">{formatReason(node.reason)}</div></div>
|
||||||
{evidence.length > 0 && <div className="mt-3 flex flex-wrap gap-2">{evidence.map(([key, value]) => <span key={key} className="max-w-full truncate rounded-lg border border-white/10 bg-slate-950/40 px-2 py-1 font-mono text-[10px] text-slate-300">{key}: {typeof value === 'string' ? value : JSON.stringify(value)}</span>)}</div>}
|
{evidence.length > 0 && <div className="mt-3 flex flex-wrap gap-2">{evidence.map(([key, value]) => <span key={key} className="max-w-full truncate rounded-lg border border-white/10 bg-slate-950/40 px-2 py-1 font-mono text-[10px] text-slate-300">{key}: {typeof value === 'string' ? value : JSON.stringify(value)}</span>)}</div>}
|
||||||
|
{node.id === 'H6-agentops' && <H6Summary node={node} goal={goal} runReport={h6RunReport} projectReport={h6ProjectReport} loading={h6Loading} error={h6Error} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -93,7 +183,7 @@ export function GovernedOutcomePulse({ goal, trace }: { goal: GoalJob; trace?: H
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModelInteractionDiagram({ goal, localStage, cloudStage, trace }: ModelInteractionDiagramProps) {
|
export function ModelInteractionDiagram({ goal, localStage, cloudStage, trace, h6RunReport, h6ProjectReport, h6Loading = false, h6Error = false }: ModelInteractionDiagramProps) {
|
||||||
const nodes = useMemo(() => trace?.nodes ?? GATE_ORDER.map((id) => ({ id, title: id.replace('-', ' · '), description: 'Waiting for trace evidence', status: 'queued' as const, reason: 'Waiting for evidence', updated_at: null, evidence: {}, events: [] })), [trace]);
|
const nodes = useMemo(() => trace?.nodes ?? GATE_ORDER.map((id) => ({ id, title: id.replace('-', ' · '), description: 'Waiting for trace evidence', status: 'queued' as const, reason: 'Waiting for evidence', updated_at: null, evidence: {}, events: [] })), [trace]);
|
||||||
const preferred = [...nodes].reverse().find((node) => node.status === 'running' || node.status === 'error' || node.status === 'blocked' || node.status === 'warning')?.id ?? [...nodes].reverse().find((node) => node.status !== 'queued')?.id ?? 'H1-context';
|
const preferred = [...nodes].reverse().find((node) => node.status === 'running' || node.status === 'error' || node.status === 'blocked' || node.status === 'warning')?.id ?? [...nodes].reverse().find((node) => node.status !== 'queued')?.id ?? 'H1-context';
|
||||||
const [focusedGate, setFocusedGate] = useState(preferred);
|
const [focusedGate, setFocusedGate] = useState(preferred);
|
||||||
@@ -114,7 +204,7 @@ export function ModelInteractionDiagram({ goal, localStage, cloudStage, trace }:
|
|||||||
<div className="min-w-0 rounded-2xl border border-white/10 bg-slate-900/60 p-4">
|
<div className="min-w-0 rounded-2xl border border-white/10 bg-slate-900/60 p-4">
|
||||||
<div className="mb-3 flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.14em] text-slate-400"><span className="goal-control-signal h-2 w-2 rounded-full bg-cyan-300" /> Control spine · H1 → H7</div>
|
<div className="mb-3 flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.14em] text-slate-400"><span className="goal-control-signal h-2 w-2 rounded-full bg-cyan-300" /> Control spine · H1 → H7</div>
|
||||||
<div className="relative flex flex-wrap gap-2 before:absolute before:left-4 before:right-4 before:top-5 before:h-px before:bg-gradient-to-r before:from-indigo-500/30 before:via-cyan-300/50 before:to-emerald-400/30">{nodes.map((node) => <ControlGate key={node.id} node={node} selected={node.id === focusedGate} onClick={() => setFocusedGate(node.id)} />)}</div>
|
<div className="relative flex flex-wrap gap-2 before:absolute before:left-4 before:right-4 before:top-5 before:h-px before:bg-gradient-to-r before:from-indigo-500/30 before:via-cyan-300/50 before:to-emerald-400/30">{nodes.map((node) => <ControlGate key={node.id} node={node} selected={node.id === focusedGate} onClick={() => setFocusedGate(node.id)} />)}</div>
|
||||||
{focusedNode && <div className="mt-4"><GateDetail node={focusedNode} /></div>}
|
{focusedNode && <div className="mt-4"><GateDetail node={focusedNode} goal={goal} h6RunReport={h6RunReport} h6ProjectReport={h6ProjectReport} h6Loading={h6Loading} h6Error={h6Error} /></div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-white/10 bg-slate-900/80 px-5 py-3 text-xs text-slate-400">Color, motion and progress come from the live H1–H7 trace. When a gate stops, the outcome stops at that evidence boundary without inventing downstream failures.</div>
|
<div className="border-t border-white/10 bg-slate-900/80 px-5 py-3 text-xs text-slate-400">Color, motion and progress come from the live H1–H7 trace. When a gate stops, the outcome stops at that evidence boundary without inventing downstream failures.</div>
|
||||||
|
|||||||
@@ -203,6 +203,20 @@ export function Goals() {
|
|||||||
enabled: Boolean(selectedQuery.data?.trace_id),
|
enabled: Boolean(selectedQuery.data?.trace_id),
|
||||||
refetchInterval: (query) => query.state.data?.terminal ? false : 1200,
|
refetchInterval: (query) => query.state.data?.terminal ? false : 1200,
|
||||||
});
|
});
|
||||||
|
const h6RunQuery = useQuery({
|
||||||
|
queryKey: ['reports', 'h6', 'goal-run', selectedQuery.data?.trace_id, selectedQuery.data?.updated_at],
|
||||||
|
queryFn: () => api.h6Report({ run: selectedQuery.data!.trace_id, limit: 20 }),
|
||||||
|
enabled: Boolean(selectedQuery.data?.trace_id),
|
||||||
|
retry: false,
|
||||||
|
refetchInterval: selectedQuery.data && !TERMINAL.has(selectedQuery.data.status) ? 2500 : false,
|
||||||
|
});
|
||||||
|
const h6ProjectQuery = useQuery({
|
||||||
|
queryKey: ['reports', 'h6', 'goal-project', selectedQuery.data?.project],
|
||||||
|
queryFn: () => api.h6Report({ project: selectedQuery.data!.project, limit: 20 }),
|
||||||
|
enabled: Boolean(selectedQuery.data?.project),
|
||||||
|
retry: false,
|
||||||
|
staleTime: 15_000,
|
||||||
|
});
|
||||||
const start = useMutation({
|
const start = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
if (!actor) throw new Error('Authenticated session is unavailable.');
|
if (!actor) throw new Error('Authenticated session is unavailable.');
|
||||||
@@ -353,7 +367,16 @@ export function Goals() {
|
|||||||
<StatusBadge value={selected.status} />
|
<StatusBadge value={selected.status} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ModelInteractionDiagram goal={selected} localStage={localStage} cloudStage={cloudStage} trace={traceQuery.data} />
|
<ModelInteractionDiagram
|
||||||
|
goal={selected}
|
||||||
|
localStage={localStage}
|
||||||
|
cloudStage={cloudStage}
|
||||||
|
trace={traceQuery.data}
|
||||||
|
h6RunReport={h6RunQuery.data}
|
||||||
|
h6ProjectReport={h6ProjectQuery.data}
|
||||||
|
h6Loading={h6RunQuery.isLoading}
|
||||||
|
h6Error={h6RunQuery.isError}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card className="!rounded-[1.35rem]" title="Governed outcome" right={<StatusBadge value={selected.status} />}>
|
<Card className="!rounded-[1.35rem]" title="Governed outcome" right={<StatusBadge value={selected.status} />}>
|
||||||
<GovernedOutcomePulse goal={selected} trace={traceQuery.data} />
|
<GovernedOutcomePulse goal={selected} trace={traceQuery.data} />
|
||||||
|
|||||||
Reference in New Issue
Block a user