"""Trace rendering for the Gradio web UI.""" from __future__ import annotations from .agent import Step from .trace_collector import TraceEvent _TOOL_ICON = { "write_file": "✏️", "read_file": "📖", "list_files": "📂", "run_python": "▶️", "run_tests": "🧪", "check_app": "🌐", } def merge_step_metadata(events: list[TraceEvent], raw_history: list) -> list[TraceEvent]: """Attach LiteForge timing/token stats to tool_call events.""" if not raw_history: return events calls = [e for e in events if e.kind == "tool_call"] merged: list[TraceEvent] = [] call_idx = 0 for ev in events: if ev.kind != "tool_call" or call_idx >= len(raw_history): merged.append(ev) continue step = raw_history[call_idx] call_idx += 1 merged.append(TraceEvent( kind=ev.kind, name=ev.name, detail=ev.detail, step=ev.step, duration_ms=getattr(step, "duration_ms", None), tokens=getattr(step, "total_tokens", None), )) return merged def format_trace_md( events: list[TraceEvent], *, steps: list[Step] | None = None, max_detail: int = 500, idle: str = "_waiting for the model…_", ) -> str: """Render trace events as markdown with expandable tool I/O.""" if not events and not steps: return idle if not events and steps: return _steps_only_md(steps) lines: list[str] = [] step_no = 0 i = 0 while i < len(events): ev = events[i] if ev.kind == "tool_call": icon = _TOOL_ICON.get(ev.name, "🔧") meta = _meta_badge(ev) summary = f"`{step_no}`   {icon} **{ev.name}**{meta}" detail = _truncate(ev.detail, max_detail) block = f"
{summary}\n\n```json\n{detail}\n```\n
" if i + 1 < len(events) and events[i + 1].kind == "tool_result": result = _truncate(events[i + 1].detail, max_detail) block += f"\n\n↳ result:\n\n```json\n{result}\n```" i += 1 lines.append(block) step_no += 1 elif ev.kind == "tier_escalation": lines.append(f"⬆️ **escalated** → `{ev.name}`: {ev.detail}") elif ev.kind == "final": lines.append("✅ **final answer**") elif ev.kind == "error": lines.append(f"⚠️ **error**: {_truncate(ev.detail, max_detail)}") i += 1 return "\n\n".join(lines) if lines else idle def format_fanout_trace_md(results) -> str: """Per-subagent expandable traces for fan-out mode.""" if not results: return "_no subagents_" blocks = [] for r in results: events = getattr(r, "trace_events", None) or [] inner = format_trace_md(events, steps=r.steps, idle="_no steps yet_") verdict = "✓ verified" if r.verified else ("⚠️ error" if r.error else "· unverified") blocks.append( f"
`{r.index + 1}` **subagent** ({r.model}): " f"{len(r.steps)} steps · {verdict}\n\n{inner}\n
" ) return "\n\n".join(blocks) def _steps_only_md(steps: list[Step]) -> str: lines = [] for s in steps: kind = s.kind if kind.startswith("tool_call:"): tool = kind.split(":", 1)[1] icon = _TOOL_ICON.get(tool, "🔧") meta = "" if s.total_tokens: meta = f" · {s.total_tokens} tok" lines.append(f"`{s.number}`   {icon} **{tool}**{meta}") elif kind == "response": lines.append("✅ **final answer**") else: lines.append(f"• {kind}") return "\n\n".join(lines) if lines else "_waiting for the model…_" def _meta_badge(ev: TraceEvent) -> str: parts = [] if ev.duration_ms is not None: parts.append(f"{ev.duration_ms}ms") if ev.tokens is not None: parts.append(f"{ev.tokens} tok") return f" ({', '.join(parts)})" if parts else "" def _truncate(text: str, limit: int) -> str: text = text.strip() if len(text) <= limit: return text return text[:limit] + f"\n… ({len(text)} chars total)"