// ========== 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 (
Runs
{total}total
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 ( {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 ( ); } // ----- 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) => )}
{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.userInput}
{trace.finalOutput &&
{trace.finalOutput}
} {trace.statusReason && (
{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 && (
{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}
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" />

Top tools by calls

Top tools by avg latency

Runs by agent

Cost by agent

); } 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 (
baizhi / Traces
Playground Manage Evals Marketplace Apps {isAdmin && Users} API Docs
{(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."}
)}
setSearch(e.target.value)} />
{fTime === "date" && setFDate(event.target.value)} aria-label="Trace date" />} {isAdmin && }
{view === "dashboard" ? (
) : (
Trace
Latency
Cost
{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 ? (

Select a trace

) : ( <> doReplay(selected)} onDownloadModelIO={() => doDownloadModelIO(selected)} downloadBusy={downloadBusy} onAddToDataset={() => setAddToDsTrace(selected)} />
{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"} {parts.length ? parts.map((part, partIndex) => (
{modelPartText(part)}
)) : ( )}
); })}
); } function ModelSystemInstruction({ value }) { if (!value) { return
No system instruction captured
; } return (
{fullText(value)}
); } 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"} {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();