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]) =>
{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}

} +
+ +
+ Open full H6 report + Download run JSON + Open project health +
+
+ ); +} + +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() { - + }>