diff --git a/packages/casan-control-panel/frontend/src/components/goals/ModelInteractionDiagram.tsx b/packages/casan-control-panel/frontend/src/components/goals/ModelInteractionDiagram.tsx
index 2277a83..132fe95 100644
--- a/packages/casan-control-panel/frontend/src/components/goals/ModelInteractionDiagram.tsx
+++ b/packages/casan-control-panel/frontend/src/components/goals/ModelInteractionDiagram.tsx
@@ -1,5 +1,6 @@
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';
interface ModelInteractionDiagramProps {
@@ -7,6 +8,10 @@ interface ModelInteractionDiagramProps {
localStage?: GoalStage;
cloudStage?: GoalStage;
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'];
@@ -23,6 +28,20 @@ function formatReason(reason: string): string {
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 }: {
label: 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
{Array.from({ length: 6 }, (_, index) =>
)}
;
+ }
+
+ if (error || !runReport) {
+ return Run telemetry could not be loaded here. Open the full H6 report to inspect the source status.
;
+ }
+
+ 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 (
+
+
+
+
Run gate
+
{goal.trace_id.slice(0, 12)}…
+
+
+
Project health
+
{goal.project}
+
{projectReport?.summary.runs ?? 0} matching telemetry record(s)
+
+
+
+
{metrics.map(([label, value]) =>
)}
+
+
+
Run telemetry verdict
{runReport.summary.runs} matching record(s) · data quality {runReport.data_quality.status}
+
Provider/model: {provider}
+ {runReport.findings[0] &&
{runReport.findings[0].message}
}
+
+
+
+
+ );
+}
+
+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 evidence = Object.entries(node.evidence).filter(([, value]) => value !== null && value !== undefined).slice(0, 3);
return (
@@ -71,6 +160,7 @@ function GateDetail({ node }: { node: HarnessGateNode }) {
{node.description}
Observed impact
{formatReason(node.reason)}
{evidence.length > 0 && {evidence.map(([key, value]) => {key}: {typeof value === 'string' ? value : JSON.stringify(value)} )}
}
+ {node.id === 'H6-agentops' && }
);
}
@@ -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 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);
@@ -114,7 +204,7 @@ export function ModelInteractionDiagram({ goal, localStage, cloudStage, trace }:
Control spine · H1 → H7
{nodes.map((node) => setFocusedGate(node.id)} />)}
- {focusedNode &&
}
+ {focusedNode &&
}
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.
diff --git a/packages/casan-control-panel/frontend/src/pages/Goals.tsx b/packages/casan-control-panel/frontend/src/pages/Goals.tsx
index f67a995..79f9f03 100644
--- a/packages/casan-control-panel/frontend/src/pages/Goals.tsx
+++ b/packages/casan-control-panel/frontend/src/pages/Goals.tsx
@@ -203,6 +203,20 @@ export function Goals() {
enabled: Boolean(selectedQuery.data?.trace_id),
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({
mutationFn: () => {
if (!actor) throw new Error('Authenticated session is unavailable.');
@@ -353,7 +367,16 @@ export function Goals() {
-
+
}>