"""Pure rendering of agent events to HTML, plus the session run-cap check.""" import html as _html import json from agent import config TIMELINE_CSS = """ :root { --bg:#0b0f14; --card:#121821; --line:#1e2733; --fg:#cfd8e3; --muted:#7d8a99; --cyan:#39d3ee; --green:#37e0a6; --amber:#f0b35e; } .gradio-container { background: var(--bg) !important; } #oa-timeline { font-family: ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace; display:flex; flex-direction:column; gap:10px; } .oa-card { background:var(--card); border:1px solid var(--line); border-left:3px solid var(--line); border-radius:8px; padding:10px 14px; color:var(--fg); } .oa-card .oa-head { font-size:12px; letter-spacing:.04em; color:var(--muted); margin-bottom:4px; } .oa-thought { border-left-color: var(--cyan); } .oa-revision { border-left-color: var(--amber); background:#1a1710; } .oa-tool_call { border-left-color: var(--green); } .oa-observation { border-left-color:#2b3a4a; } .oa-observation.oa-weak { border-left-color: var(--amber); opacity:.92; } .oa-final { border-left-color: var(--green); background:#0f1a16; font-size:15px; } .oa-status { color:var(--muted); font-style:italic; } .oa-args, .oa-obs { white-space:pre-wrap; word-break:break-word; margin-top:4px; } #oa-stats { color:var(--muted); font-family: ui-monospace, monospace; margin-top:6px; } """ _ICON = {"thought": "๐Ÿง ", "tool_call": "๐Ÿ”ง", "observation": "๐Ÿ“„", "final": "โœ…", "limit": "โ›”"} def _esc(text) -> str: return _html.escape(str(text)) def render_event(event) -> str: kind = event["kind"] step = event.get("step", "") if kind == "thought": revision = event.get("revision", False) icon = "โ†ป" if revision else _ICON["thought"] label = "revision" if revision else "thought" cls = "oa-thought oa-revision" if revision else "oa-thought" return (f'
{icon} {label} ยท step {step}
' f'
{_esc(event["text"])}
') if kind == "tool_call": args = json.dumps(event.get("args", {}), ensure_ascii=False) return (f'
๐Ÿ”ง tool call ยท step {step}
' f'
{_esc(event["tool"])}
{_esc(args)}
') if kind == "observation": weak = " oa-weak" if event.get("weak") else "" tag = " (weak โ€” reconsidering)" if event.get("weak") else "" return (f'
๐Ÿ“„ observation ยท ' f'{_esc(event["tool"])}{tag}
{_esc(event["text"])}
') if kind == "final": return (f'
โœ… final answer
' f'
{_esc(event["text"])}
') if kind == "limit": return (f'
โ›” stopped
' f'
{_esc(event["text"])}
') return "" def render_timeline(events, status=None) -> str: cards = [render_event(e) for e in events if e.get("kind") != "status"] if status: cards.append(f'
{_esc(status)}
') return f'
{"".join(cards)}
' def render_stats(*, steps: int, tool_calls: int, seconds: float) -> str: return f"{steps} steps ยท {tool_calls} tool calls ยท {seconds:.1f}s" def check_run_allowed(runs_used: int, cap: int = config.RUNS_PER_SESSION): if runs_used >= cap: return False, f"You've reached this session's run limit ({cap}). Refresh to start a new session." return True, ""