// Generate ~50 realistic mock traces. // Spans have start_offset_ms from the run start, and parent linkage for nesting. // // The Traces page is still mock-driven (PR 14 will wire real `/v1/traces`). // Until then we synthesize a fixed agent pool here instead of depending on // `window.INITIAL_AGENTS` — that global is now populated from /v1/agents at // runtime and may be empty (no agents created yet), which used to silently // produce 0 traces and render the page blank (Codex BUG-3). const MOCK_TRACE_AGENTS = [ { id: "agt_general", name: "General Assistant", color: "#10a37f", initials: "GA", model: "gpt-4o" }, { id: "agt_coder", name: "Code Reviewer", color: "#2563eb", initials: "CR", model: "claude-3.7-sonnet" }, { id: "agt_analyst", name: "Data Analyst", color: "#d97706", initials: "DA", model: "gpt-4o" }, { id: "agt_research", name: "Research Agent", color: "#c026d3", initials: "RA", model: "claude-haiku-4.5" }, ]; window.MOCK_TRACE_AGENTS = MOCK_TRACE_AGENTS; const TRACE_SAMPLE_INPUTS = [ "Summarize the latest Anthropic release notes", "Find the top 5 PRs opened this week in acme/web", "查一下 2025 年 Q1 北美 SaaS 公司 ARR 增长中位数", "Refactor this Python function to be O(n log n)", "Compute 17% compounded monthly for 3 years on $4,200", "Generate a cohort retention chart from the orders table", "What did the team discuss in #eng-platform yesterday?", "把这份 PRD 翻译成英文并提炼 5 个 bullet point", "Read https://news.ycombinator.com and pick 3 stories about agents", "Run: SELECT count(*) FROM users WHERE created_at > now() - interval '7 days'", "Why is the checkout p95 latency spiking since Friday?", "为什么我的 token 用量这周翻倍了", "Find issues labeled 'p0' across all repos", "把昨天 #design 频道的讨论整理成一份会议纪要", "Diff the schema between staging and prod", "Plot daily active users for the last 30 days", "Search Notion for the on-call runbook", "Calculate the break-even point for our new pricing tier", "Suggest 3 prompt tweaks to reduce hallucinations in the analyst agent", "Open a PR that bumps lodash to the latest patch version", "Tell me what's broken in the last hour", "Why did this user fail to sign up? user_id=u_8421", "执行迁移脚本并报告耗时", "Pull the last 100 errors from the events table", "找出和这个 issue 重复的相关 issue", ]; const TRACE_FINAL_OUTPUTS = [ "I found 5 results. The top one looks most relevant: Anthropic shipped a new memory feature on May 19...", "There are 7 open PRs this week. The 3 oldest are PR #4421, #4430, #4438 — all waiting on review.", "Q1 2025 median ARR growth for North American SaaS was ~28% YoY based on the search results below.", "Here is the refactor — replaced the nested loop with a heap-based merge to get O(n log n).", "Final amount: $4,200 × (1 + 0.17/12)^36 ≈ $6,983.42. Effective annual yield ≈ 18.39%.", "Generated cohort_retention.png — week 4 retention dropped from 38% → 31% in the latest cohort.", "Yesterday in #eng-platform: incident postmortem for the Friday 17:42 deploy + Q2 roadmap review.", "Translated. Key points: (1) tighter onboarding flow, (2) audit log v2, (3) per-skill billing...", "Picked 3 HN stories about agents — open-source MCP launcher, ReAct vs Plan-Execute benchmarks, and...", "Result: 1,842 users created in the last 7 days (+12% vs prior period).", "p95 jumped from 480ms → 1.2s on Friday 16:00. Correlates with deploy of commit a3f2d11 to checkout-svc.", "Token usage this week is 2.1× last week. The spike is concentrated in the analyst agent — three runs alone burned 142k tokens.", ]; const ERROR_MESSAGES = [ "tool_call_failed: web_search returned 429 Too Many Requests", "kernel_timeout: code_interpreter exceeded 30s wall clock", "schema_violation: tool returned an unexpected field 'foo'", "context_length_exceeded: prompt was 198,432 tokens (max 128,000)", "mcp_disconnected: slack-mcp lost connection mid-call", "rate_limit: org-level rpm cap hit (10,000 / min)", ]; const SAMPLE_FULL_OUTPUT = `Here are the top 5 results, ranked by relevance: 1. **Anthropic ships persistent memory for Claude** — May 19. Claude can now opt into a memory store that persists across conversations within a workspace. 2. **MCP 1.0 spec finalized** — May 14. The official 1.0 spec adds streaming + auth flows. 3. **Computer Use 2.0 preview** — May 8. New screen-grounded action API. 4. **Claude 3.7 Sonnet release notes** — May 5. Tool-use improvements + cheaper price tier. 5. **Anthropic-Cohere benchmark study** — Apr 30. Want me to expand any of these?`; function makeWaterfallSpans(plan, opts = {}) { // plan: list of { kind, name, durMs, error?, payload? } // returns spans + total duration const spans = []; let t = 0; // RUN_STARTED + final RUN_FINISHED bracket spans.push({ id: rid("sp"), kind: "run", name: "agent.run", parent: null, startMs: 0, durMs: 0 }); // initial system prompt -> LLM for (const step of plan) { spans.push({ id: rid("sp"), kind: step.kind, name: step.name, parent: step.parent || null, startMs: t, durMs: step.durMs, error: !!step.error, payload: step.payload || null, }); t += step.durMs + (step.gap ?? 18); } // close root span spans[0].durMs = t; return { spans, totalMs: t }; } function makeTrace({ minutesAgo, agent, model, status, scenario }) { const userInput = scenario.input; // build a step plan const plan = []; // pre-LLM (always) plan.push({ kind: "llm", name: `LLM · plan (${model})`, durMs: 180 + Math.random() * 280, payload: { prompt_tokens: 642 + Math.floor(Math.random() * 200), role: "planner", messages_in: 2, }}); // tools scenario.tools.forEach((tk) => { plan.push({ kind: tk.kind || "tool", name: tk.name, durMs: tk.durMs ?? (200 + Math.random() * 600), payload: { args: tk.args || {}, result: tk.error ? { error: tk.errorMsg } : tk.result || { ok: true }, }, error: !!tk.error, }); // post-tool LLM if (tk.followLLM !== false) { plan.push({ kind: "llm", name: `LLM · reflect (${model})`, durMs: 120 + Math.random() * 200, payload: { reflecting_on: tk.name, prompt_tokens: 420 + Math.floor(Math.random() * 600) } }); } }); // final synthesis LLM plan.push({ kind: "llm", name: `LLM · respond (${model})`, durMs: 400 + Math.random() * 800, payload: { completion_tokens: 280 + Math.floor(Math.random() * 600), streaming: true } }); // append a terminal error if status === error if (status === "error") { plan.push({ kind: "err", name: "fatal_error", durMs: 20, payload: { message: ERROR_MESSAGES[Math.floor(Math.random() * ERROR_MESSAGES.length)] }, error: true }); } const { spans, totalMs } = makeWaterfallSpans(plan); const toolCalls = plan.filter((p) => p.kind === "tool" || p.kind === "mcp").length; const llmRounds = plan.filter((p) => p.kind === "llm").length; const tokensIn = 800 + Math.floor(Math.random() * 4000) + llmRounds * 300; const tokensOut = 200 + Math.floor(Math.random() * 1200) + llmRounds * 80; const costUSD = +((tokensIn * 0.000005 + tokensOut * 0.000015)).toFixed(4); const startedAt = Date.now() - minutesAgo * 60_000; return { id: rid("tr"), runId: rid("run"), threadId: rid("thread"), agentId: agent.id, agentName: agent.name, agentColor: agent.color, agentInitials: agent.initials, model, startedAt, durationMs: Math.round(totalMs), status, statusReason: status === "error" ? plan[plan.length - 1].payload?.message : null, tokensIn, tokensOut, costUSD, toolCalls, llmRounds, userInput, finalOutput: status === "ok" ? (TRACE_FINAL_OUTPUTS[Math.floor(Math.random() * TRACE_FINAL_OUTPUTS.length)]) : null, spans, }; } function generateTraces(n = 50) { const scenarios = [ { input: TRACE_SAMPLE_INPUTS[0], tools: [ { name: "web_search", args: { q: "anthropic release notes" }, result: { hits: 5 }, durMs: 620 }, ]}, { input: TRACE_SAMPLE_INPUTS[1], tools: [ { name: "github.list_prs", kind: "mcp", args: { repo: "acme/web", state: "open" }, result: { count: 7 }, durMs: 340 }, { name: "github.get_pr_files", kind: "mcp", args: { pr: 4421 }, result: { files: 14 }, durMs: 240 }, ]}, { input: TRACE_SAMPLE_INPUTS[2], tools: [ { name: "web_search", args: { q: "saas median ARR Q1 2025" }, result: { hits: 8 }, durMs: 720 }, { name: "url_reader", args: { url: "https://saastr.com/…" }, result: { kb: 24 }, durMs: 460 }, { name: "calculator", args: { expr: "median([22,28,31,40,25])" }, result: { value: 28 }, durMs: 60 }, ]}, { input: TRACE_SAMPLE_INPUTS[3], tools: [ { name: "code_interpreter", args: { language: "python", lines: 18 }, result: { ok: true }, durMs: 540 }, ]}, { input: TRACE_SAMPLE_INPUTS[4], tools: [ { name: "calculator", args: { expr: "4200*(1+.17/12)^36" }, result: { value: 6983.42 }, durMs: 30 }, ]}, { input: TRACE_SAMPLE_INPUTS[5], tools: [ { name: "sql_runner", args: { sql: "SELECT … FROM orders" }, result: { rows: 12400 }, durMs: 820 }, { name: "chart_builder", args: { type: "line", x: "week" }, result: { png: "..." }, durMs: 380 }, ]}, { input: TRACE_SAMPLE_INPUTS[6], tools: [ { name: "slack.fetch_channel", kind: "mcp", args: { channel: "eng-platform", since: "yesterday" }, result: { messages: 142 }, durMs: 440 }, ]}, { input: TRACE_SAMPLE_INPUTS[7], tools: []}, { input: TRACE_SAMPLE_INPUTS[8], tools: [ { name: "url_reader", args: { url: "https://news.ycombinator.com" }, result: { stories: 30 }, durMs: 380 }, { name: "url_reader", args: { url: "https://item/..." }, result: { kb: 12 }, durMs: 290 }, ]}, { input: TRACE_SAMPLE_INPUTS[9], tools: [ { name: "sql_runner", args: { sql: "SELECT count(*)…" }, result: { rows: 1, value: 1842 }, durMs: 220 }, ]}, { input: TRACE_SAMPLE_INPUTS[10], tools: [ { name: "sql_runner", args: { sql: "SELECT percentile_cont…" }, result: { rows: 30 }, durMs: 980, followLLM: true }, { name: "github.list_commits", kind: "mcp", args: { repo: "acme/checkout-svc", since: "friday" }, result: { count: 14 }, durMs: 420 }, ]}, // failing ones { input: TRACE_SAMPLE_INPUTS[11], tools: [ { name: "sql_runner", args: { sql: "…" }, durMs: 1820, error: true, errorMsg: "connection_timeout" }, ]}, { input: TRACE_SAMPLE_INPUTS[12], tools: [ { name: "github.list_issues", kind: "mcp", args: { label: "p0" }, result: { count: 9 }, durMs: 360 }, ]}, { input: TRACE_SAMPLE_INPUTS[13], tools: [ { name: "slack.fetch_channel", kind: "mcp", args: { channel: "design", since: "yesterday" }, durMs: 1240, error: true, errorMsg: "channel_not_found" }, ]}, ]; // Prefer real agents when available so the trace samples match what the // user actually has installed; fall back to the fixed mock pool so the // page is never blank. const agentsPool = (window.INITIAL_AGENTS && window.INITIAL_AGENTS.length) ? window.INITIAL_AGENTS : MOCK_TRACE_AGENTS; const modelsPool = ["gpt-4o", "gpt-4o-mini", "claude-3.7-sonnet", "claude-haiku-4.5"]; const traces = []; for (let i = 0; i < n; i++) { const minutesAgo = Math.floor(Math.pow(i / n, 1.6) * 60 * 36); // last ~36h, weighted recent const agent = agentsPool[i % agentsPool.length]; const model = agent.model || modelsPool[Math.floor(Math.random() * modelsPool.length)]; const scenario = scenarios[i % scenarios.length]; // sprinkle errors const willError = scenario.tools.some((t) => t.error) || (Math.random() < 0.07); const status = willError ? "error" : (Math.random() < 0.03 ? "warn" : "ok"); traces.push(makeTrace({ minutesAgo, agent, model, status, scenario })); } // newest first return traces.sort((a, b) => b.startedAt - a.startedAt); } // time bucketing for sparkline function bucketByHour(traces, hours = 24) { const now = Date.now(); const buckets = Array(hours).fill(0).map((_, i) => ({ t: now - (hours - 1 - i) * 3_600_000, count: 0, errors: 0, totalLatency: 0, })); traces.forEach((tr) => { const idx = Math.floor((now - tr.startedAt) / 3_600_000); const b = buckets[hours - 1 - idx]; if (!b) return; b.count += 1; b.totalLatency += tr.durationMs; if (tr.status === "error") b.errors += 1; }); return buckets; } // ---------- API → UI shape adapters (PR 14b) ---------- // // The backend /v1/traces returns a flat summary; the UI was originally built // against the rich mock shape in makeTrace(). traceFromApiSummary maps the // summary fields to that shape so existing components (TraceRow, SummaryStrip, // Inspector …) don't need to change. Missing fields (spans, full tools) are // filled in lazily when the user opens a trace via api.getTraceDetail(). const STATUS_MAP = { completed: "ok", failed: "error", timeout: "error", cancelled: "error", queued: "warn", running: "warn", unknown: "warn", }; function agentLookup(agents, agentId) { return (agents || []).find((a) => a.id === agentId) || null; } function traceFromApiSummary(s, agents) { const agent = agentLookup(agents, s.agent_id) || {}; const usage = s.usage || {}; const tokensIn = usage.input_tokens ?? usage.prompt_tokens ?? 0; const tokensOut = usage.output_tokens ?? usage.completion_tokens ?? 0; const toolCalls = usage.tool_calls ?? 0; const llmRounds = usage.llm_turns ?? usage.llm_rounds ?? 0; // The backend calculates a model-specific catalog reference estimate (or // preserves a provider-reported cost). Never reprice live runs in the UI. const costUSD = Number(usage.cost_usd ?? 0); // Runtime timestamps are milliseconds; accept legacy second values too. const rawStartedAt = s.started_at ?? s.queued_at; const startedAt = rawStartedAt ? (Number(rawStartedAt) < 100_000_000_000 ? Number(rawStartedAt) * 1000 : Number(rawStartedAt)) : Date.now(); return { id: s.run_id, runId: s.run_id, threadId: s.thread_id || "", agentId: s.agent_id, agentName: agent.name || (s.agent_id || "agent"), agentColor: agent.color || hashColorFallback(s.agent_id || ""), agentInitials: agent.initials || initialsFromName(agent.name || s.agent_id || "?"), model: agent.model || "—", startedAt, durationMs: s.duration_ms || 0, status: STATUS_MAP[s.status] || "warn", statusReason: s.error ? String(s.error) : null, tokensIn, tokensOut, costUSD, userId: s.user_id || null, toolCalls, llmRounds, userInput: s.preview || "(no preview)", finalOutput: null, // filled in by detail load spans: [], // filled in by detail load _live: true, // marker: backed by real API, not mock _detailLoaded: false, _selectedSkills: s.selected_skills || [], _rawStatus: s.status, }; } // Pair tool.started + tool.completed events into tool spans; collapse // successive text.delta into a single llm span. The result is a thin // waterfall — exact timings come from event timestamps, parent linkage // keeps spans flat under the root. // // PR α (pr-trace-a): payload field reads now prefer OpenInference-style // dotted names (`tool.name`, `tool.call_id`, `tool.args`) but fall back // to the bare names (`tool_name`, `tool_call_id`, `arguments`) so a // browser pointing at a pre-PR-α server (e.g. the still-stale PID 29322) // still renders. When event.parent_event_id is present (future PR β/γ) // we use it to build a real span tree instead of guessing from time // windows. function _pref(payload, ...keys) { for (const k of keys) { const v = payload?.[k]; if (v !== undefined && v !== null) return v; } return undefined; } function eventsToSpans(events, runStartMs) { const spans = []; const rootId = rid("sp"); spans.push({ id: rootId, kind: "run", name: "agent.run", parent: null, startMs: 0, durMs: 0 }); const toolByCallId = {}; let lastTextStart = null; let lastTextEnd = null; let firstTs = null; // Track event_id → span id so when a child event carries // parent_event_id we can wire span.parent to the right span (not just // null = root). const spanByEventId = {}; for (const ev of events || []) { const ts = ev.timestamp ? Math.floor(ev.timestamp * 1000) : null; if (ts && firstTs === null) firstTs = ts; const baseStart = ts && firstTs ? (ts - firstTs) : 0; const parentSpanId = ev.parent_event_id ? (spanByEventId[ev.parent_event_id] || rootId) : null; if (ev.type === "tool.started") { const callId = _pref(ev.payload, "tool.call_id", "call_id", "tool_call_id") || rid("call"); const toolName = _pref(ev.payload, "tool.name", "tool_name", "name") || "tool"; const toolArgs = _pref(ev.payload, "tool.args", "args", "arguments") || ev.payload; toolByCallId[callId] = { startMs: baseStart, name: toolName, args: toolArgs }; } else if (ev.type === "tool.completed") { const callId = _pref(ev.payload, "tool.call_id", "call_id", "tool_call_id"); const prev = callId ? toolByCallId[callId] : null; const startMs = prev?.startMs ?? baseStart; const durMs = Math.max(1, baseStart - startMs); const spanId = rid("sp"); spans.push({ id: spanId, kind: "tool", name: prev?.name || _pref(ev.payload, "tool.name", "tool_name") || "tool", parent: parentSpanId, startMs, durMs, payload: { args: prev?.args, result: ev.payload?.result || ev.payload || {} }, error: ev.payload?.error || ev.payload?.is_error ? true : false, }); if (ev.event_id) spanByEventId[ev.event_id] = spanId; } else if (ev.type === "text.delta") { if (lastTextStart === null) lastTextStart = baseStart; lastTextEnd = baseStart; } else if (ev.type === "agent.skill_activated" || ev.type === "skill.activated") { spans.push({ id: rid("sp"), kind: "llm", name: "skill · " + (_pref(ev.payload, "skill.name", "skill_name", "name") || "?"), parent: parentSpanId, startMs: baseStart, durMs: 50, payload: ev.payload || {}, error: false, }); } else if (ev.type === "error" || ev.type === "run.failed") { spans.push({ id: rid("sp"), kind: "err", name: "fatal_error", parent: null, startMs: baseStart, durMs: 20, payload: { message: ev.payload?.message || ev.payload?.error || "run failed" }, error: true, }); } } if (lastTextStart !== null) { spans.push({ id: rid("sp"), kind: "llm", name: "LLM · respond", parent: null, startMs: lastTextStart, durMs: Math.max(50, (lastTextEnd ?? lastTextStart) - lastTextStart), payload: { streaming: true }, error: false, }); } // Root span dur = last endpoint const lastEnd = spans.slice(1).reduce((m, s) => Math.max(m, s.startMs + s.durMs), 0); spans[0].durMs = lastEnd; return spans; } function hydrateTraceFromDetail(uiTrace, detail) { const events = detail.events || []; const summary = detail.summary || {}; const startedAt = summary.started_at ? Math.floor(summary.started_at * 1000) : uiTrace.startedAt; const spans = eventsToSpans(events, startedAt); const modelIO = [...(detail.model_io || [])].sort((a, b) => { const roundDelta = (a.round_index || 0) - (b.round_index || 0); if (roundDelta) return roundDelta; return (a.started_at_ms || 0) - (b.started_at_ms || 0); }); // assistant final text = concatenation of text.delta deltas const finalText = events.filter((e) => e.type === "text.delta") .map((e) => (e.payload || {}).delta || "") .join(""); const persistedOutput = detail.run?.output_text || detail.summary?.output_text || ""; return { ...uiTrace, spans, finalOutput: finalText || persistedOutput || uiTrace.finalOutput, durationMs: summary.duration_ms || uiTrace.durationMs, modelIO, _detailLoaded: true, _events: events, _detail: detail, }; } // Tiny fallbacks for color/initials when api.hashColor / initialsFor aren't // wired in (defensive — TracesApp normally passes the live api object's // helpers via INITIAL_AGENTS, which already have agent.color / .initials). function hashColorFallback(str) { let h = 0; for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) | 0; const hue = Math.abs(h) % 360; return `hsl(${hue}, 65%, 55%)`; } function initialsFromName(name) { const parts = String(name || "").split(/\s+/).filter(Boolean); if (!parts.length) return "?"; return (parts[0][0] + (parts[1]?.[0] || "")).toUpperCase(); } Object.assign(window, { generateTraces, bucketByHour, SAMPLE_FULL_OUTPUT, traceFromApiSummary, hydrateTraceFromDetail, eventsToSpans, });