// ========== Traces page ==========
const { useState: tUseState, useEffect: tUseEffect, useMemo: tUseMemo, useRef: tUseRef } = React;
// ----- helpers -----
function dateKey(date) {
const pad = (value) => String(value).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
function fmtTraceTime(timestamp) {
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) return "—";
const pad = (value) => String(value).padStart(2, "0");
return `${String(date.getFullYear()).slice(-2)}${pad(date.getMonth() + 1)}${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function fmtCost(c) {
if (c === null || c === undefined || c === "") return "—";
const value = Number(c);
if (!Number.isFinite(value)) return "—";
return `$${value.toFixed(value > 0 && value < 0.0001 ? 8 : 4)}`;
}
function fmtTokens(n) {
if (!Number.isFinite(Number(n))) return "—";
n = Number(n);
if (n >= 1000) return (n / 1000).toFixed(n >= 10000 ? 0 : 1) + "k";
return String(n);
}
function modelUsageValue(usage, ...keys) {
for (const key of keys) {
const value = usage?.[key];
if (value !== undefined && value !== null) return value;
}
return "—";
}
// ----- summary strip (above list) -----
function SummaryStrip({ traces }) {
const total = traces.length;
const errs = traces.filter((t) => t.status === "error").length;
const errRate = total ? (errs / total * 100) : 0;
const totalCost = traces.reduce((s, t) => s + t.costUSD, 0);
const p95Latency = (() => {
if (!total) return 0;
const sorted = [...traces].map((t) => t.durationMs).sort((a, b) => a - b);
return sorted[Math.floor(sorted.length * 0.95)] || 0;
})();
return (
Error rate
5 ? "#b91c1c" : "var(--text)" }}>
{errRate.toFixed(1)}%
p95 latency
{fmtMs(p95Latency)}
Cost
{fmtCost(totalCost)}
);
}
// ----- filter dropdown -----
function FilterChip({ label, value, options, onChange }) {
const [open, setOpen] = tUseState(false);
const ref = tUseRef(null);
tUseEffect(() => {
const fn = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener("mousedown", fn);
return () => document.removeEventListener("mousedown", fn);
}, []);
const set = !!value;
const display = options.find((o) => o.id === value)?.label || "Any";
return (
setOpen(!open)}>
{label}
{display}
{set ? (
{ e.stopPropagation(); onChange(null); }}>
) : }
{open && (
{ onChange(null); setOpen(false); }}>
Any
{options.map((o) => (
{ onChange(o.id); setOpen(false); }}>
{o.color && }
{o.label}
))}
)}
);
}
function SelectFilter({ label, value, options, onChange }) {
return (
{label}
onChange(event.target.value)}>
{options.map((option) => {option.label} )}
);
}
// ----- trace list row -----
function TraceRow({ trace, active, onClick }) {
const dotCls = trace.status === "ok" ? "ok" : trace.status === "warn" ? "warn" : "err";
// bar represents latency relative to 3s = 100%
const ratio = Math.min(1, trace.durationMs / 3000);
return (
{trace.agentInitials}
{trace.agentName}
{fmtTraceTime(trace.startedAt)}
{trace.userInput}
{trace.model}
{trace.llmRounds}
{trace.toolCalls}
{fmtTokens(trace.tokensIn)}↑ {fmtTokens(trace.tokensOut)}↓
{trace.userId && user {trace.userId} }
{fmtMs(trace.durationMs)}
{fmtCost(trace.costUSD)}
);
}
// ----- Turn execution -----
function traceNumber(...values) {
for (const value of values) {
if (value === null || value === undefined || value === "") continue;
const number = Number(value);
if (Number.isFinite(number)) return number;
}
return null;
}
function traceUsage(trace) {
const usage = trace?._detail?.run?.usage || trace?._detail?.summary?.usage || {};
return {
inputTokens: traceNumber(usage.input_tokens, usage.prompt_tokens, usage.prompt_token_count, trace?.tokensIn),
outputTokens: traceNumber(usage.output_tokens, usage.completion_tokens, usage.candidates_token_count, trace?.tokensOut),
costUsd: traceNumber(usage.cost_usd, trace?.costUSD),
};
}
function traceTimelineSteps(trace) {
const events = trace?._events || [];
const modelIO = trace?.modelIO || [];
const modelIOByRound = new Map(modelIO.map((call) => [call.round_index, call]));
const steps = [];
const completedRounds = new Set();
const completedToolCalls = new Set();
for (const event of events) {
const payload = event.payload || {};
if (event.type === "llm.completed") {
const roundIndex = payload.round_index || completedRounds.size + 1;
const call = modelIOByRound.get(roundIndex);
completedRounds.add(roundIndex);
const model = payload.model || call?.model || trace.model;
steps.push({
kind: "llm",
label: `LLM · round ${roundIndex}${model ? ` (${model})` : ""}`,
durMs: traceNumber(payload.duration_ms, call?.duration_ms) || 1,
});
continue;
}
if (event.type !== "tool.completed" && event.type !== "tool.failed" && event.type !== "TOOL_CALL_RESULT") continue;
const callId = payload["tool.call_id"] || payload.tool_call_id || payload.call_id || payload.toolCallId;
const fallbackId = `${event.type}:${steps.length}`;
const toolKey = callId || fallbackId;
if (completedToolCalls.has(toolKey)) continue;
completedToolCalls.add(toolKey);
const isError = Boolean(payload.is_error || payload.error || event.type === "tool.failed" || payload.content?.is_error);
steps.push({
kind: "tool",
label: payload.tool_name || payload["tool.name"] || payload.toolCallName || "tool",
durMs: traceNumber(payload.duration_ms, payload["tool.duration_ms"], payload.content?.duration_ms) || 1,
isError,
});
}
// Historical traces can have model I/O retained while their event buffer has
// been pruned. Keep the round count visible instead of rendering an empty run.
if (!completedRounds.size && modelIO.length) {
for (const call of modelIO) {
const roundIndex = call.round_index || steps.length + 1;
steps.push({
kind: "llm",
label: `LLM · round ${roundIndex}${call.model ? ` (${call.model})` : ""}`,
durMs: traceNumber(call.duration_ms) || 1,
});
}
}
if (!steps.length && trace?.spans?.length) {
for (const span of trace.spans.filter((item) => item.kind === "llm" || item.kind === "tool")) {
steps.push({
kind: span.kind,
label: span.name,
durMs: span.durMs || 1,
isError: Boolean(span.error),
});
}
}
steps.push({
kind: "final",
label: `RUN FINISHED · ${trace?._rawStatus || (trace?.status === "ok" ? "completed" : trace?.status || "unknown")}`,
durMs: 1,
});
return steps;
}
function TraceTimelineRow({ step }) {
return (
{step.kind}
{step.label}
{fmtMs(step.durMs)}
);
}
function TraceExecutionCard({ trace, turnNumber }) {
const [outputOpen, setOutputOpen] = tUseState(false);
const usage = traceUsage(trace);
const steps = traceTimelineSteps(trace);
const rounds = steps.filter((step) => step.kind === "llm").length;
const tools = steps.filter((step) => step.kind === "tool").length;
const output = trace.finalOutput || "No assistant text was recorded for this run.";
return (
Turn {turnNumber}
thread {trace.threadId}
run {trace.runId}
in {fmtTokens(usage.inputTokens)} · out {fmtTokens(usage.outputTokens)} · {fmtCost(usage.costUsd)} · {rounds} rounds · {tools} tools
{fmtMs(trace.durationMs)}
{!trace._detailLoaded && trace._live
?
Loading run execution…
: steps.map((step, index) =>
)}
setOutputOpen((open) => !open)} aria-expanded={outputOpen}>
Final output
{outputOpen &&
{output} }
);
}
// ----- waterfall -----
function Waterfall({ trace, selectedSpanId, onSelect }) {
const total = Math.max(trace.durationMs, 1);
// visible spans = all non-root
const spans = trace.spans.filter((s) => s.kind !== "run");
// make 5 axis ticks
const ticks = [];
const step = total / 4;
for (let i = 0; i <= 4; i++) ticks.push(Math.round(step * i));
return (
{spans.length} spans
·
total {fmtMs(trace.durationMs)}
LLM
Tool
MCP
Error
Span
{ticks.map((t, i) => (
{fmtMs(t)}
))}
Dur
{spans.map((s) => {
const leftPct = (s.startMs / total) * 100;
const widthPct = Math.max(0.5, (s.durMs / total) * 100);
const kindCls = s.error ? "err" : s.kind;
return (
onSelect(s.id)}
>
{s.kind}
{s.kind === "llm" ? :
s.kind === "tool" || s.kind === "mcp" ? :
}
{s.name}
{ticks.map((_, i) => (
))}
{fmtMs(s.durMs)}
);
})}
);
}
// ----- inspector -----
function Inspector({ trace, span }) {
if (!span) {
// overview when no span selected
return (
RUN
Run overview
{fmtMs(trace.durationMs)}
{trace.finalOutput &&
}
{trace.statusReason && (
)}
);
}
const kindCls = span.error ? "err" : span.kind;
return (
{span.kind}
{span.name}
{fmtMs(span.durMs)}
{span.payload?.args && (
)}
{span.payload?.result && (
)}
{span.kind === "llm" && (
<>
{span.payload?.prompt_tokens != null && (
{span.payload.prompt_tokens != null &&
}
{span.payload.completion_tokens != null &&
}
{span.payload.messages_in != null &&
}
)}
{span.name.includes("respond")
? (trace.finalOutput || SAMPLE_FULL_OUTPUT)
: "Decide next action: based on the user's question, calling the appropriate tool to gather missing information."}
>
)}
{span.error && span.payload?.message && (
)}
);
}
function Section({ title, copy, children }) {
const [copied, setCopied] = tUseState(false);
const onCopy = () => {
if (copy) {
window.copyText?.(copy);
setCopied(true);
setTimeout(() => setCopied(false), 1200);
}
};
return (
{title}
{copy && {copied ? "copied" : "copy"} }
{children}
);
}
function Row({ k, v }) {
return {k} {v}
;
}
// ----- detail header -----
function DetailHeader({ trace, onReplay, onDownloadModelIO, downloadBusy, onAddToDataset }) {
const sCls = trace.status === "ok" ? "ok" : trace.status === "warn" ? "warn" : "err";
const copyRunId = () => {
window.copyText?.(trace.runId);
};
return (
{trace.agentInitials}
{trace.userInput}
Copy ID
Add to dataset
Replay
{downloadBusy ? "Preparing…" : "Download"}
run {trace.runId}
thread {trace.threadId}
{trace.userId && user {trace.userId} }
started {new Date(trace.startedAt).toLocaleString()}
{trace.status === "ok" ? : }
{trace.status}
{trace.agentName}
{trace.model}
{fmtMs(trace.durationMs)}
{trace.llmRounds} LLM
{trace.toolCalls} tools
{fmtTokens(trace.tokensIn + trace.tokensOut)} tok
{fmtCost(trace.costUSD)}
);
}
// ----- dashboard tab -----
function Sparkline({ buckets, valueFn = (b) => b.count, color = "#0d0d0d", errLine = false }) {
const w = 100, h = 50;
const vals = buckets.map(valueFn);
const max = Math.max(1, ...vals);
const pts = vals.map((v, i) => `${(i / (vals.length - 1)) * w},${h - (v / max) * (h - 6) - 2}`);
const area = `M0,${h} L${pts.join(" L")} L${w},${h} Z`;
const line = `M${pts.join(" L")}`;
const errVals = buckets.map((b) => b.errors);
const errPts = errVals.map((v, i) => `${(i / (vals.length - 1)) * w},${h - (v / max) * (h - 6) - 2}`);
const errLinePath = `M${errPts.join(" L")}`;
return (
{errLine && }
);
}
function TopBars({ rows, valueLabel = "" }) {
const max = Math.max(1, ...rows.map((r) => r.value));
return (
{rows.map((r, i) => (
{r.label}
{r.display ?? r.value}{valueLabel}
))}
);
}
function Dashboard({ traces }) {
const buckets = bucketByHour(traces, 24);
const okCount = traces.filter((t) => t.status === "ok").length;
const errCount = traces.filter((t) => t.status === "error").length;
const errRate = traces.length ? (errCount / traces.length * 100) : 0;
const totalCost = traces.reduce((s, t) => s + t.costUSD, 0);
const avgLat = traces.length ? traces.reduce((s, t) => s + t.durationMs, 0) / traces.length : 0;
const p95 = (() => {
const sorted = traces.map((t) => t.durationMs).sort((a, b) => a - b);
return sorted[Math.floor(sorted.length * 0.95)] || 0;
})();
// top tools
const toolStats = {};
traces.forEach((t) => {
t.spans.forEach((s) => {
if (s.kind !== "tool" && s.kind !== "mcp") return;
const key = s.name.split(".")[0];
toolStats[key] = toolStats[key] || { name: key, calls: 0, totalMs: 0, errs: 0 };
toolStats[key].calls += 1;
toolStats[key].totalMs += s.durMs;
if (s.error) toolStats[key].errs += 1;
});
});
const topByCalls = Object.values(toolStats).sort((a, b) => b.calls - a.calls).slice(0, 6)
.map((t) => ({ label: t.name, value: t.calls, color: "#2563eb", display: t.calls }));
const topByLatency = Object.values(toolStats).sort((a, b) => (b.totalMs / b.calls) - (a.totalMs / a.calls)).slice(0, 6)
.map((t) => ({ label: t.name, value: Math.round(t.totalMs / t.calls), color: "#c026d3", display: fmtMs(t.totalMs / t.calls) }));
// by agent
const agentStats = {};
traces.forEach((t) => {
agentStats[t.agentName] = agentStats[t.agentName] || { name: t.agentName, color: t.agentColor, runs: 0, cost: 0 };
agentStats[t.agentName].runs += 1;
agentStats[t.agentName].cost += t.costUSD;
});
const topAgentsByRuns = Object.values(agentStats).sort((a, b) => b.runs - a.runs)
.map((a) => ({ label: a.name, value: a.runs, color: a.color, display: a.runs }));
const topAgentsByCost = Object.values(agentStats).sort((a, b) => b.cost - a.cost)
.map((a) => ({ label: a.name, value: a.cost, color: a.color, display: fmtCost(a.cost) }));
return (
Runs (24h)
{traces.length}
↑ 12% vs prev day
Error rate
5 ? "#b91c1c" : "var(--text)" }}>
{errRate.toFixed(1)}%
{errCount} errors / {traces.length} runs
b.errors} color="#ef4444" />
p95 latency
{fmtMs(p95)}
avg {fmtMs(avgLat)}
b.count ? b.totalLatency / b.count : 0}
color="#d97706"
/>
Total cost
{fmtCost(totalCost)}
{Math.round(traces.reduce((s, t) => s + t.tokensIn + t.tokensOut, 0) / 1000)}k tokens
b.count}
color="#10a37f"
/>
);
}
function mergeLiveTraceSummaries(prev, next) {
const prevById = new Map((prev || []).map((t) => [t.id, t]));
return (next || []).map((fresh) => {
const old = prevById.get(fresh.id);
if (!old || !old._detailLoaded) return fresh;
return {
...fresh,
spans: old.spans,
finalOutput: old.finalOutput,
modelIO: old.modelIO,
_events: old._events,
_detail: old._detail,
_detailLoaded: true,
};
});
}
function isActiveTrace(trace) {
return trace?._rawStatus === "running" || trace?._rawStatus === "queued";
}
// ----- main app -----
function TracesApp() {
// Real backend traces (PR 14b). Falls back to mock generator only when the
// API call fails *and* the tenant has no real runs yet — in that case the
// page shows the mock with a visible "Demo data" banner so the user knows
// they're not looking at live state.
const [allTraces, setAllTraces] = tUseState([]);
const [liveAgents, setLiveAgents] = tUseState([]); // populated from /v1/agents on mount
const [loadingList, setLoadingList] = tUseState(true);
const [loadError, setLoadError] = tUseState(null);
// PR-S.17d · topbar 上 Users 入口 admin-gate
const [isAdmin, setIsAdmin] = tUseState(false);
tUseEffect(() => {
window.api.authMe()
.then((m) => setIsAdmin(!!m.is_admin))
.catch(() => {});
}, []);
const [usingMock, setUsingMock] = tUseState(false);
const [view, setView] = tUseState("list"); // list | dashboard
const [search, setSearch] = tUseState("");
const [fAgent, setFAgent] = tUseState(null);
const [fModel, setFModel] = tUseState(null);
const [fStatus, setFStatus] = tUseState(null);
const [fTime, setFTime] = tUseState("today");
const [fDate, setFDate] = tUseState(() => dateKey(new Date()));
const [fScope, setFScope] = tUseState("me");
const [liveTail, setLiveTail] = tUseState(true);
const [selectedId, setSelectedId] = tUseState(null);
const [selectedSpanId, setSelectedSpanId] = tUseState(null);
const [detailTab, setDetailTab] = tUseState("runs"); // runs | events | model | raw
const [addToDsTrace, setAddToDsTrace] = tUseState(null);
const [replayBusy, setReplayBusy] = tUseState(false);
const [downloadBusy, setDownloadBusy] = tUseState(false);
const userPinnedTraceRef = tUseRef(false);
// Load real traces from /v1/traces on mount + when liveTail flips on.
tUseEffect(() => {
let cancelled = false;
async function load() {
setLoadingList(true);
setLoadError(null);
try {
// Fetch agents + traces in parallel. agents may fail (older server
// / wrong tenant) without breaking trace load; in that case we
// fall back to MOCK_TRACE_AGENTS / agent_id string for display.
const [items, agentsResult] = await Promise.all([
window.api.listTraces({ limit: 200, scope: isAdmin ? fScope : "me" }),
window.api.listAgents().catch(() => []),
]);
if (cancelled) return;
const agents = (agentsResult && agentsResult.length)
? agentsResult
: (window.INITIAL_AGENTS || window.MOCK_TRACE_AGENTS || []);
setLiveAgents(agents);
const traces = items.map((s) => traceFromApiSummary(s, agents));
if (traces.length === 0) {
// No real traces yet — show mock with visible banner so the page
// isn't blank but the user knows it's not real.
setAllTraces(generateTraces(60));
setUsingMock(true);
} else {
setAllTraces((prev) => mergeLiveTraceSummaries(prev, traces));
setUsingMock(false);
}
} catch (err) {
if (cancelled) return;
setLoadError(String(err && err.message || err));
setAllTraces(generateTraces(60));
setUsingMock(true);
} finally {
if (!cancelled) setLoadingList(false);
}
}
load();
let timer = null;
if (liveTail) {
timer = setInterval(load, 10000);
}
return () => { cancelled = true; if (timer) clearInterval(timer); };
}, [liveTail, fScope, isAdmin]);
// filter
const filtered = tUseMemo(() => {
const q = search.trim().toLowerCase();
const selectedDate = fTime === "today" ? dateKey(new Date()) : fDate;
return allTraces.filter((t) => {
if (q && !(t.userInput.toLowerCase().includes(q) || t.runId.includes(q) || t.threadId.includes(q) || t.agentName.toLowerCase().includes(q) || (t.userId || "").toLowerCase().includes(q))) return false;
if (fAgent && t.agentId !== fAgent) return false;
if (fModel && t.model !== fModel) return false;
if (fStatus && t.status !== fStatus) return false;
if (selectedDate && dateKey(new Date(t.startedAt)) !== selectedDate) return false;
return true;
});
}, [allTraces, search, fAgent, fModel, fStatus, fTime, fDate]);
tUseEffect(() => {
if (filtered.length && !filtered.find((t) => t.id === selectedId)) {
setSelectedId(filtered[0].id);
setSelectedSpanId(null);
userPinnedTraceRef.current = false;
}
}, [filtered, selectedId]);
const selected = filtered.find((t) => t.id === selectedId) || filtered[0];
const selectedTurnNumber = tUseMemo(() => {
if (!selected) return 1;
const turns = allTraces
.filter((trace) => trace.threadId === selected.threadId)
.sort((left, right) => left.startedAt - right.startedAt);
return Math.max(1, turns.findIndex((trace) => trace.id === selected.id) + 1);
}, [allTraces, selected?.id, selected?.threadId]);
// Live tail should actually follow a run while it is running. Without this,
// a newly-created run can appear at the top of the list while detail polling
// keeps refreshing an older selected trace, so Model IO never updates round
// by round unless the user clicks the new row manually.
tUseEffect(() => {
if (!liveTail || userPinnedTraceRef.current || !filtered.length) return;
const active = filtered.find(isActiveTrace);
if (active && active.id !== selected?.id && !isActiveTrace(selected)) {
setSelectedId(active.id);
setSelectedSpanId(null);
}
}, [filtered, liveTail, selected?.id, selected?._rawStatus]);
// Lazy-load detail (events + spans + finalOutput) when a live trace is
// selected and we haven't fetched its detail yet. Mock traces already have
// `spans` populated by generateTraces() so the detail call is skipped.
tUseEffect(() => {
if (!selected || !selected._live || selected._detailLoaded) return;
let cancelled = false;
(async () => {
try {
const detail = await window.api.getTraceDetail(selected.runId);
if (cancelled) return;
setAllTraces((prev) => prev.map((t) =>
t.id === selected.id ? hydrateTraceFromDetail(t, detail) : t
));
} catch (err) {
if (!cancelled) console.warn("getTraceDetail failed", err);
}
})();
return () => { cancelled = true; };
}, [selected?.id, selected?._live, selected?._detailLoaded]);
// While live tailing, keep the selected trace detail fresh too. The list
// refresh alone only updates summaries; Model IO records are stored in the
// detail payload, including the "running" record written before the model
// response returns.
tUseEffect(() => {
if (!liveTail || !selected || !selected._live) return;
let cancelled = false;
async function loadSelectedDetail() {
try {
const detail = await window.api.getTraceDetail(selected.runId);
if (cancelled) return;
setAllTraces((prev) => prev.map((t) =>
t.id === selected.id ? hydrateTraceFromDetail(t, detail) : t
));
} catch (err) {
if (!cancelled) console.warn("getTraceDetail live refresh failed", err);
}
}
loadSelectedDetail();
const timer = setInterval(loadSelectedDetail, 1500);
return () => { cancelled = true; clearInterval(timer); };
}, [liveTail, selected?.id, selected?._live]);
async function doReplay(trace) {
if (!trace || replayBusy) return;
if (!trace._live) {
window.alert("This is mock data — replay only works on live traces.");
return;
}
setReplayBusy(true);
try {
const result = await window.api.replayTrace(trace.runId);
const newRunId = result?.run?.run_id || result?.run_id || "(no run_id)";
window.alert(`Replay enqueued — new run ${newRunId}. Open Playground to watch it.`);
} catch (err) {
window.alert("Replay failed: " + (err && err.message || err));
} finally {
setReplayBusy(false);
}
}
async function doDownloadModelIO(trace) {
if (!trace || downloadBusy) return;
setDownloadBusy(true);
try {
await window.api.downloadTraceModelIO(trace.runId);
window.showToast?.("Model IO archive downloaded");
} catch (err) {
window.alert("Model IO download failed: " + (err && err.message || err));
} finally {
setDownloadBusy(false);
}
}
// Agent source priority: live /v1/agents response (post-PR-1 fix —
// shows real Agent.name like "agent_default → 'Default Agent'") →
// window.INITIAL_AGENTS (set by manage-app.jsx if user navigated from
// there in same SPA) → MOCK_TRACE_AGENTS (last-resort placeholders).
const agentSource = liveAgents.length
? liveAgents
: ((INITIAL_AGENTS && INITIAL_AGENTS.length) ? INITIAL_AGENTS : (window.MOCK_TRACE_AGENTS || []));
const agentOpts = agentSource.map((a) => ({ id: a.id, label: a.name, color: a.color }));
const modelOpts = [...new Set(allTraces.map((t) => t.model))].map((m) => ({ id: m, label: m }));
const statusOpts = [
{ id: "ok", label: "ok", color: "#10a37f" },
{ id: "error", label: "error", color: "#ef4444" },
{ id: "warn", label: "warn", color: "#d97706" },
];
return (
setView("list")}
> List
setView("dashboard")}
> Dashboard
Export
{(usingMock || loadError) && (
{loadError
? `⚠ Failed to load live traces (${loadError}). Showing mock data.`
: "ⓘ Demo data — no live traces for this tenant yet. Run something in Playground to see real traces here."}
)}
{view === "dashboard" ? (
) : (
{filtered.length === 0 ? (
No traces match
Adjust your filters or expand the time window.
) : filtered.map((t) => (
{
userPinnedTraceRef.current = true;
setSelectedId(t.id);
setSelectedSpanId(null);
}}
/>
))}
{!selected ? (
) : (
<>
doReplay(selected)}
onDownloadModelIO={() => doDownloadModelIO(selected)}
downloadBusy={downloadBusy}
onAddToDataset={() => setAddToDsTrace(selected)}
/>
setDetailTab("runs")}>
Runs {selected.llmRounds} rounds
setDetailTab("events")}>
AG-UI events
setDetailTab("model")}>
Model IO {selected.modelIO?.length || 0}
setDetailTab("raw")}>
Raw JSON
{detailTab === "runs" && (
)}
{detailTab === "events" && (
)}
{detailTab === "model" && (
)}
{detailTab === "raw" && (
)}
>
)}
)}
{addToDsTrace && (
setAddToDsTrace(null)}
/>
)}
);
}
function navLinkStyle() {
return {
background: "transparent", border: 0, padding: "6px 10px", borderRadius: 6,
color: "var(--text-muted)", fontSize: 13, textDecoration: "none", cursor: "pointer",
};
}
function EventsList({ trace }) {
if (trace._events?.length) {
const events = [];
events.push({ type: "RUN_STARTED", threadId: trace.threadId, runId: trace.runId, timestamp: trace.startedAt });
const msgId = "msg_" + trace.runId.slice(-6);
events.push({ type: "TEXT_MESSAGE_START", messageId: msgId, role: "assistant", timestamp: trace.startedAt + 10 });
for (const ev of trace._events || []) {
const payload = ev.payload || {};
const ts = ev.timestamp ? Math.floor(ev.timestamp * 1000) : Date.now();
if (ev.type === "tool.started") {
const callId = payload["tool.call_id"] || payload.tool_call_id || payload.call_id;
events.push({
type: "TOOL_CALL_START",
toolCallId: callId,
toolCallName: payload["tool.name"] || payload.tool_name,
parentMessageId: msgId,
timestamp: ts,
});
events.push({
type: "TOOL_CALL_ARGS",
toolCallId: callId,
toolCallName: payload["tool.name"] || payload.tool_name,
delta: JSON.stringify(payload["tool.args"] || payload.arguments || {}),
timestamp: ts + 1,
});
events.push({
type: "TOOL_CALL_END",
toolCallId: callId,
timestamp: ts + 2,
});
} else if (ev.type === "tool.completed") {
events.push({
type: "TOOL_CALL_RESULT",
messageId: `tool_${payload["tool.call_id"] || payload.tool_call_id || payload.call_id}`,
toolCallId: payload["tool.call_id"] || payload.tool_call_id || payload.call_id,
role: "tool",
content: {
result: payload.result_text || payload.result || "",
result_ref: payload.result_ref || null,
duration_ms: payload.duration_ms || 0,
is_error: !!payload.is_error,
},
timestamp: ts,
});
} else if (ev.type === "text.delta") {
events.push({ type: "TEXT_MESSAGE_CONTENT", messageId: msgId, delta: payload.delta || "", timestamp: ts });
} else if (ev.type === "thinking.started") {
events.push({
type: "REASONING_START",
messageId: payload.thinking_id,
parentMessageId: msgId,
timestamp: ts,
});
events.push({
type: "REASONING_MESSAGE_START",
messageId: payload.thinking_id,
role: "assistant",
parentMessageId: msgId,
timestamp: ts + 1,
});
} else if (ev.type === "thinking.delta") {
events.push({
type: "REASONING_MESSAGE_CONTENT",
messageId: payload.thinking_id,
delta: payload.delta || "",
timestamp: ts,
});
} else if (ev.type === "thinking.completed") {
events.push({
type: "REASONING_MESSAGE_END",
messageId: payload.thinking_id,
text: payload.text || "",
timestamp: ts,
});
events.push({
type: "REASONING_END",
messageId: payload.thinking_id,
timestamp: ts + 1,
});
} else if (ev.type === "run.awaiting_input") {
events.push({ type: "CUSTOM", name: payload.event_name || "hil_interruption", value: payload, timestamp: ts });
}
}
events.push({ type: "TEXT_MESSAGE_END", messageId: msgId, timestamp: trace.startedAt + trace.durationMs - 5 });
events.push({ type: "RUN_FINISHED", threadId: trace.threadId, runId: trace.runId,
timestamp: trace.startedAt + trace.durationMs, output: { text: trace.finalOutput } });
return ;
}
// Reconstruct an AG-UI events list from spans
const events = [];
events.push({ type: "RUN_STARTED", threadId: trace.threadId, runId: trace.runId, timestamp: trace.startedAt });
const msgId = "msg_" + trace.runId.slice(-6);
events.push({ type: "TEXT_MESSAGE_START", messageId: msgId, role: "assistant", timestamp: trace.startedAt + 10 });
trace.spans.filter((s) => s.kind !== "run").forEach((s) => {
if (s.kind === "tool" || s.kind === "mcp") {
events.push({ type: "TOOL_CALL_START", toolCallId: s.id, toolCallName: s.name, parentMessageId: msgId,
timestamp: trace.startedAt + s.startMs });
if (s.payload?.args) events.push({ type: "TOOL_CALL_ARGS", toolCallId: s.id, toolCallName: s.name, delta: JSON.stringify(s.payload.args),
timestamp: trace.startedAt + s.startMs + 2 });
events.push({ type: "TOOL_CALL_END", toolCallId: s.id, timestamp: trace.startedAt + s.startMs + 3 });
events.push({ type: "TOOL_CALL_RESULT", messageId: `tool_${s.id}`, toolCallId: s.id,
role: "tool", content: { result: s.payload?.result, duration_ms: Math.round(s.durMs) },
timestamp: trace.startedAt + s.startMs + s.durMs });
} else if (s.kind === "llm") {
events.push({ type: "TEXT_MESSAGE_CONTENT", messageId: msgId, delta: "(chunk)",
timestamp: trace.startedAt + s.startMs + s.durMs / 2 });
} else if (s.kind === "err") {
events.push({ type: "ERROR", message: s.payload?.message, timestamp: trace.startedAt + s.startMs });
}
});
events.push({ type: "TEXT_MESSAGE_END", messageId: msgId, timestamp: trace.startedAt + trace.durationMs - 5 });
events.push({ type: "RUN_FINISHED", threadId: trace.threadId, runId: trace.runId,
timestamp: trace.startedAt + trace.durationMs, output: { text: trace.finalOutput } });
return ;
}
function RenderedEvents({ events, trace }) {
return (
{events.map((ev, i) => (
{ev.type}
+{fmtMs(ev.timestamp - trace.startedAt)}
))}
);
}
function RawJson({ trace }) {
return (
);
}
function countModelTools(tools) {
if (!Array.isArray(tools)) return 0;
return tools.reduce((count, tool) => {
const decls = tool?.function_declarations;
return count + (Array.isArray(decls) ? decls.length : 1);
}, 0);
}
function modelInputMessages(input) {
if (input?.messages != null) return input.messages;
if (input?.preview) {
return [{ truncated: true, reason: "legacy whole-input trace was truncated", preview: input.preview }];
}
return [];
}
function modelSystemInstruction(input) {
return input?.system_instruction ?? input?.config?.system_instruction ?? "";
}
function modelInputTools(input) {
if (input?.tools != null) return input.tools;
if (input?.preview) {
return { truncated: true, reason: "legacy whole-input trace was truncated", preview: input.preview };
}
return [];
}
function safeStringify(value, space = 2) {
try {
return JSON.stringify(value, null, space);
} catch (err) {
return String(value ?? "");
}
}
function fullText(value) {
return typeof value === "string" ? value : safeStringify(value);
}
function modelPartText(part) {
if (!part) return "";
if (typeof part.text === "string") return part.text;
if (part.text != null) return fullText(part.text);
if (part.function_call) return fullText(part.function_call);
if (part.function_response) return fullText(part.function_response);
return fullText(part);
}
function flattenModelTools(tools) {
if (!Array.isArray(tools)) return [];
const out = [];
tools.forEach((tool, toolIndex) => {
const decls = tool?.function_declarations;
if (Array.isArray(decls) && decls.length) {
decls.forEach((decl, declIndex) => out.push({
...decl,
_toolIndex: toolIndex,
_declIndex: declIndex,
}));
} else {
out.push({
name: tool?.name || `tool_${toolIndex + 1}`,
description: tool?.description,
parameters: tool?.parameters || tool,
_toolIndex: toolIndex,
_declIndex: 0,
});
}
});
return out;
}
function JsonBlock({ value }) {
return {fullText(value)} ;
}
function ModelMessages({ messages }) {
if (!Array.isArray(messages) || !messages.length) {
return No input messages captured
;
}
return (
{messages.map((msg, index) => {
const parts = Array.isArray(msg?.parts) ? msg.parts : [];
return (
Message {index + 1}
{msg?.role || "unknown"}
{
window.copyText?.(safeStringify(msg));
}}>copy
{parts.length ? parts.map((part, partIndex) => (
{modelPartText(part)}
)) : (
)}
);
})}
);
}
function ModelSystemInstruction({ value }) {
if (!value) {
return No system instruction captured
;
}
return (
);
}
function ModelTools({ tools }) {
const flat = flattenModelTools(tools);
if (!flat.length) {
return No tools advertised to the model
;
}
return (
{flat.map((tool, index) => (
{index + 1}. {tool.name || "tool"}
{
window.copyText?.(safeStringify(tool));
}}>copy
{tool.description && {tool.description} }
))}
);
}
function ModelIOList({ trace }) {
const calls = trace.modelIO || [];
if (!calls.length) {
return (
No model IO captured for this trace
);
}
return (
{calls.map((call) => (
{(() => {
const systemInstruction = modelSystemInstruction(call.input);
const messages = modelInputMessages(call.input);
const tools = modelInputTools(call.input);
const toolCount = countModelTools(tools);
return (
Round {call.round_index} · {call.model}
{!call.completed_at_ms && running }
window.copyText?.(JSON.stringify(call, null, 2))}>copy
{call.completed_at_ms ? (
) : (
Waiting for model output…
)}
{call.output?.reasoning?.text && (
{call.output.reasoning.text}
)}
);
})()}
))}
);
}
function TracesAppWithModal() {
return ;
}
ReactDOM.createRoot(document.getElementById("root")).render( );