glaip-sdk 0.0.1b10__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/__init__.py +2 -2
- glaip_sdk/_version.py +51 -0
- glaip_sdk/cli/commands/agents.py +201 -109
- glaip_sdk/cli/commands/configure.py +29 -87
- glaip_sdk/cli/commands/init.py +16 -7
- glaip_sdk/cli/commands/mcps.py +73 -153
- glaip_sdk/cli/commands/tools.py +185 -49
- glaip_sdk/cli/main.py +30 -27
- glaip_sdk/cli/utils.py +126 -13
- glaip_sdk/client/__init__.py +54 -2
- glaip_sdk/client/agents.py +175 -237
- glaip_sdk/client/base.py +62 -2
- glaip_sdk/client/mcps.py +63 -20
- glaip_sdk/client/tools.py +95 -28
- glaip_sdk/config/constants.py +10 -3
- glaip_sdk/exceptions.py +13 -0
- glaip_sdk/models.py +20 -4
- glaip_sdk/utils/__init__.py +116 -18
- glaip_sdk/utils/client_utils.py +284 -0
- glaip_sdk/utils/rendering/__init__.py +1 -0
- glaip_sdk/utils/rendering/formatting.py +211 -0
- glaip_sdk/utils/rendering/models.py +53 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +38 -0
- glaip_sdk/utils/rendering/renderer/base.py +827 -0
- glaip_sdk/utils/rendering/renderer/config.py +33 -0
- glaip_sdk/utils/rendering/renderer/console.py +54 -0
- glaip_sdk/utils/rendering/renderer/debug.py +82 -0
- glaip_sdk/utils/rendering/renderer/panels.py +123 -0
- glaip_sdk/utils/rendering/renderer/progress.py +118 -0
- glaip_sdk/utils/rendering/renderer/stream.py +198 -0
- glaip_sdk/utils/rendering/steps.py +168 -0
- glaip_sdk/utils/run_renderer.py +22 -1086
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/METADATA +9 -37
- glaip_sdk-0.0.3.dist-info/RECORD +40 -0
- glaip_sdk/cli/config.py +0 -592
- glaip_sdk/utils.py +0 -167
- glaip_sdk-0.0.1b10.dist-info/RECORD +0 -28
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/entry_points.txt +0 -0
glaip_sdk/utils/run_renderer.py
CHANGED
|
@@ -4,1102 +4,38 @@
|
|
|
4
4
|
This module provides a modern CLI experience similar to Claude Code and Gemini CLI,
|
|
5
5
|
with compact headers, streaming markdown, collapsible tool steps, and clean output.
|
|
6
6
|
|
|
7
|
+
This is a compatibility shim that re-exports components from the new modular renderer package.
|
|
8
|
+
|
|
7
9
|
Authors:
|
|
8
10
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
from __future__ import annotations
|
|
12
14
|
|
|
13
|
-
import logging
|
|
14
|
-
import os
|
|
15
|
-
import re
|
|
16
|
-
from dataclasses import dataclass, field
|
|
17
|
-
from time import monotonic
|
|
18
|
-
from typing import Any
|
|
19
|
-
|
|
20
|
-
from rich.console import Console, Group
|
|
21
|
-
from rich.live import Live
|
|
22
|
-
from rich.markdown import Markdown
|
|
23
|
-
from rich.panel import Panel
|
|
24
|
-
from rich.text import Text
|
|
25
|
-
from rich.tree import Tree
|
|
26
|
-
|
|
27
15
|
# Configure logger
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _pretty_args(d: dict | None, max_len: int = 80) -> str:
|
|
32
|
-
if not d:
|
|
33
|
-
return ""
|
|
34
|
-
try:
|
|
35
|
-
import json
|
|
36
|
-
|
|
37
|
-
s = json.dumps(d, ensure_ascii=False)
|
|
38
|
-
except Exception:
|
|
39
|
-
s = str(d)
|
|
40
|
-
return s if len(s) <= max_len else s[: max_len - 1] + "…"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _pretty_out(s: str | None, max_len: int = 80) -> str:
|
|
44
|
-
if not s:
|
|
45
|
-
return ""
|
|
46
|
-
s = s.strip().replace("\n", " ")
|
|
47
|
-
# strip common LaTeX markers so collapsed lines are clean
|
|
48
|
-
s = re.sub(r"\\\((.*?)\\\)", r"\1", s)
|
|
49
|
-
s = re.sub(r"\\\[(.*?)\\\]", r"\1", s)
|
|
50
|
-
s = re.sub(r"\\begin\{.*?\}|\\end\{.*?\}", "", s)
|
|
51
|
-
return s if len(s) <= max_len else s[: max_len - 1] + "…"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@dataclass
|
|
55
|
-
class Step:
|
|
56
|
-
step_id: str
|
|
57
|
-
kind: str # "tool" | "delegate" | "agent"
|
|
58
|
-
name: str
|
|
59
|
-
status: str = "running"
|
|
60
|
-
args: dict = field(default_factory=dict)
|
|
61
|
-
output: str = ""
|
|
62
|
-
parent_id: str | None = None
|
|
63
|
-
task_id: str | None = None
|
|
64
|
-
context_id: str | None = None
|
|
65
|
-
started_at: float = field(default_factory=monotonic)
|
|
66
|
-
duration_ms: int | None = None
|
|
67
|
-
|
|
68
|
-
def finish(self, duration_raw: float | None):
|
|
69
|
-
if isinstance(duration_raw, int | float):
|
|
70
|
-
self.duration_ms = int(duration_raw * 1000)
|
|
71
|
-
else:
|
|
72
|
-
self.duration_ms = int((monotonic() - self.started_at) * 1000)
|
|
73
|
-
self.status = "finished"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class StepManager:
|
|
77
|
-
def __init__(self, max_steps: int = 200):
|
|
78
|
-
self.by_id: dict[str, Step] = {}
|
|
79
|
-
self.order: list[str] = [] # top-level order
|
|
80
|
-
self.children: dict[str, list[str]] = {}
|
|
81
|
-
self.key_index: dict[
|
|
82
|
-
tuple, str
|
|
83
|
-
] = {} # (task_id, context_id, kind, name, slot) -> step_id
|
|
84
|
-
self.slot_counter: dict[tuple, int] = {}
|
|
85
|
-
self.max_steps = max_steps
|
|
86
|
-
|
|
87
|
-
def _alloc_slot(self, task_id, context_id, kind, name) -> int:
|
|
88
|
-
k = (task_id, context_id, kind, name)
|
|
89
|
-
self.slot_counter[k] = self.slot_counter.get(k, 0) + 1
|
|
90
|
-
return self.slot_counter[k]
|
|
91
|
-
|
|
92
|
-
def _key(self, task_id, context_id, kind, name, slot) -> tuple:
|
|
93
|
-
return (task_id, context_id, kind, name, slot)
|
|
94
|
-
|
|
95
|
-
def _make_id(self, task_id, context_id, kind, name, slot) -> str:
|
|
96
|
-
return f"{task_id or 't'}::{context_id or 'c'}::{kind}::{name}::{slot}"
|
|
97
|
-
|
|
98
|
-
def start_or_get(
|
|
99
|
-
self, *, task_id, context_id, kind, name, parent_id=None, args=None
|
|
100
|
-
) -> Step:
|
|
101
|
-
slot = self._alloc_slot(task_id, context_id, kind, name)
|
|
102
|
-
key = self._key(task_id, context_id, kind, name, slot)
|
|
103
|
-
step_id = self._make_id(task_id, context_id, kind, name, slot)
|
|
104
|
-
st = Step(
|
|
105
|
-
step_id=step_id,
|
|
106
|
-
kind=kind,
|
|
107
|
-
name=name,
|
|
108
|
-
parent_id=parent_id,
|
|
109
|
-
task_id=task_id,
|
|
110
|
-
context_id=context_id,
|
|
111
|
-
args=args or {},
|
|
112
|
-
)
|
|
113
|
-
self.by_id[step_id] = st
|
|
114
|
-
if parent_id:
|
|
115
|
-
self.children.setdefault(parent_id, []).append(step_id)
|
|
116
|
-
else:
|
|
117
|
-
self.order.append(step_id)
|
|
118
|
-
self.key_index[key] = step_id
|
|
119
|
-
|
|
120
|
-
# Prune old steps if we exceed the limit
|
|
121
|
-
self._prune_steps()
|
|
122
|
-
|
|
123
|
-
return st
|
|
124
|
-
|
|
125
|
-
def _prune_steps(self):
|
|
126
|
-
"""Remove oldest finished steps (and their children) to stay within max_steps limit."""
|
|
127
|
-
# Count total (top-level + children)
|
|
128
|
-
total = len(self.order) + sum(len(v) for v in self.children.values())
|
|
129
|
-
if total <= self.max_steps:
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
while self.order and total > self.max_steps:
|
|
133
|
-
oldest = self.order[0]
|
|
134
|
-
st = self.by_id.get(oldest)
|
|
135
|
-
if not st or st.status != "finished":
|
|
136
|
-
# don't remove running/unknown steps
|
|
137
|
-
break
|
|
138
|
-
|
|
139
|
-
# remove oldest + its children
|
|
140
|
-
self.order.pop(0)
|
|
141
|
-
kids = self.children.pop(oldest, [])
|
|
142
|
-
total -= 1 + len(kids)
|
|
143
|
-
for cid in kids:
|
|
144
|
-
self.by_id.pop(cid, None)
|
|
145
|
-
self.by_id.pop(oldest, None)
|
|
146
|
-
|
|
147
|
-
def get_child_count(self, step_id: str) -> int:
|
|
148
|
-
"""Get the number of child steps for a given step."""
|
|
149
|
-
return len(self.children.get(step_id, []))
|
|
150
|
-
|
|
151
|
-
def get_step_summary(self, step_id: str, verbose: bool = False) -> str:
|
|
152
|
-
"""Get a formatted summary of a step with child count information."""
|
|
153
|
-
step = self.by_id.get(step_id)
|
|
154
|
-
if not step:
|
|
155
|
-
return "Unknown step"
|
|
156
|
-
|
|
157
|
-
# Basic step info
|
|
158
|
-
status = step.status
|
|
159
|
-
duration = f"{step.duration_ms}ms" if step.duration_ms else "running"
|
|
160
|
-
|
|
161
|
-
if verbose:
|
|
162
|
-
# Verbose view: show full details
|
|
163
|
-
summary = f"{step.name} → {status} [{duration}]"
|
|
164
|
-
if step.args:
|
|
165
|
-
summary += f" | Args: {_pretty_args(step.args)}"
|
|
166
|
-
if step.output:
|
|
167
|
-
summary += f" | Output: {_pretty_out(step.output)}"
|
|
168
|
-
else:
|
|
169
|
-
# Compact view: show child count if applicable
|
|
170
|
-
child_count = self.get_child_count(step_id)
|
|
171
|
-
if child_count > 0:
|
|
172
|
-
summary = (
|
|
173
|
-
f"{step.name} → {status} [{duration}] ✓ ({child_count} sub-steps)"
|
|
174
|
-
)
|
|
175
|
-
else:
|
|
176
|
-
summary = f"{step.name} → {status} [{duration}]"
|
|
177
|
-
|
|
178
|
-
return summary
|
|
179
|
-
|
|
180
|
-
def find_running(self, *, task_id, context_id, kind, name) -> Step | None:
|
|
181
|
-
# Find the last started with same (task, context, kind, name) still running
|
|
182
|
-
for sid in reversed(
|
|
183
|
-
self.order + sum([self.children.get(k, []) for k in self.order], [])
|
|
184
|
-
):
|
|
185
|
-
st = self.by_id[sid]
|
|
186
|
-
if (st.task_id, st.context_id, st.kind, st.name) == (
|
|
187
|
-
task_id,
|
|
188
|
-
context_id,
|
|
189
|
-
kind,
|
|
190
|
-
name,
|
|
191
|
-
) and st.status != "finished":
|
|
192
|
-
return st
|
|
193
|
-
return None
|
|
194
|
-
|
|
195
|
-
def finish(
|
|
196
|
-
self, *, task_id, context_id, kind, name, output=None, duration_raw=None
|
|
197
|
-
):
|
|
198
|
-
st = self.find_running(
|
|
199
|
-
task_id=task_id, context_id=context_id, kind=kind, name=name
|
|
200
|
-
)
|
|
201
|
-
if not st:
|
|
202
|
-
# if no running step, create and immediately finish
|
|
203
|
-
st = self.start_or_get(
|
|
204
|
-
task_id=task_id, context_id=context_id, kind=kind, name=name
|
|
205
|
-
)
|
|
206
|
-
if output:
|
|
207
|
-
st.output = output
|
|
208
|
-
st.finish(duration_raw)
|
|
209
|
-
return st
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@dataclass
|
|
213
|
-
class RunStats:
|
|
214
|
-
"""Statistics for agent run execution."""
|
|
215
|
-
|
|
216
|
-
started_at: float = field(default_factory=monotonic)
|
|
217
|
-
finished_at: float | None = None
|
|
218
|
-
usage: dict[str, Any] = field(default_factory=dict)
|
|
219
|
-
|
|
220
|
-
def stop(self) -> None:
|
|
221
|
-
"""Stop timing and record finish time."""
|
|
222
|
-
self.finished_at = monotonic()
|
|
223
|
-
|
|
224
|
-
@property
|
|
225
|
-
def duration_s(self) -> float | None:
|
|
226
|
-
"""Get duration in seconds."""
|
|
227
|
-
return (
|
|
228
|
-
None
|
|
229
|
-
if self.finished_at is None
|
|
230
|
-
else round(self.finished_at - self.started_at, 2)
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class RichStreamRenderer:
|
|
235
|
-
"""
|
|
236
|
-
Live, modern terminal view:
|
|
237
|
-
- Compact header
|
|
238
|
-
- Streaming Markdown for assistant content
|
|
239
|
-
- Collapsed tool steps unless verbose=True
|
|
240
|
-
"""
|
|
241
|
-
|
|
242
|
-
def __init__(
|
|
243
|
-
self,
|
|
244
|
-
console: Console,
|
|
245
|
-
verbose: bool = False,
|
|
246
|
-
theme: str | None = None,
|
|
247
|
-
use_emoji: bool | None = None,
|
|
248
|
-
):
|
|
249
|
-
# Allow environment variable overrides
|
|
250
|
-
|
|
251
|
-
# Choose defaults first
|
|
252
|
-
_theme = theme or os.getenv("AIP_THEME", "dark")
|
|
253
|
-
_emoji = (
|
|
254
|
-
use_emoji
|
|
255
|
-
if use_emoji is not None
|
|
256
|
-
else os.getenv("AIP_NO_EMOJI", "").lower() != "true"
|
|
257
|
-
)
|
|
258
|
-
_persist_live = (
|
|
259
|
-
os.getenv("AIP_PERSIST_LIVE", "1") != "0"
|
|
260
|
-
) # default: keep live as final
|
|
261
|
-
|
|
262
|
-
"""Initialize the rich stream renderer."""
|
|
263
|
-
self.console = console
|
|
264
|
-
self.verbose = verbose
|
|
265
|
-
self.theme = _theme
|
|
266
|
-
self.use_emoji = _emoji
|
|
267
|
-
self.persist_live = _persist_live
|
|
268
|
-
self.buffer: list[str] = [] # accumulated assistant text chunks
|
|
269
|
-
self.tools: list[dict[str, Any]] = []
|
|
270
|
-
self.header_text = ""
|
|
271
|
-
self.stats = RunStats()
|
|
272
|
-
self._live: Live | None = None
|
|
273
|
-
self.steps = StepManager()
|
|
274
|
-
self.context_parent: dict[str, str] = {} # child_context_id -> parent step_id
|
|
275
|
-
self.root_context_id: str | None = (
|
|
276
|
-
None # Track root context for sub-agent routing
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
# sub-agent (child context) panels
|
|
280
|
-
self.context_panels: dict[str, list[str]] = {} # context_id -> list[str] chunks
|
|
281
|
-
self.context_meta: dict[
|
|
282
|
-
str, dict
|
|
283
|
-
] = {} # context_id -> {"title","kind","status"}
|
|
284
|
-
self.context_order: list[str] = [] # preserve creation order
|
|
285
|
-
|
|
286
|
-
# tool panels keyed by StepManager step_id
|
|
287
|
-
self.tool_panels: dict[
|
|
288
|
-
str, dict
|
|
289
|
-
] = {} # step_id -> {"title","status","chunks":[str]}
|
|
290
|
-
self.tool_order: list[str] = []
|
|
291
|
-
|
|
292
|
-
# header / status de-dup
|
|
293
|
-
self._last_status: str | None = None
|
|
294
|
-
self._last_header_rule: str | None = None
|
|
295
|
-
self._header_rules_enabled = os.getenv("AIP_HEADER_STATUS_RULES", "0") == "1"
|
|
296
|
-
self.show_delegate_tool_panels = (
|
|
297
|
-
os.getenv("AIP_SHOW_DELEGATE_PANELS", "0") == "1"
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
def __del__(self):
|
|
301
|
-
"""Destructor to ensure Live is always stopped."""
|
|
302
|
-
try:
|
|
303
|
-
if hasattr(self, "_live") and self._live:
|
|
304
|
-
self._live.stop()
|
|
305
|
-
except Exception:
|
|
306
|
-
pass # Ignore cleanup errors during destruction
|
|
307
|
-
|
|
308
|
-
def _print_header_once(self, text: str, style: str | None = None):
|
|
309
|
-
"""Print header only when changed to avoid duplicates."""
|
|
310
|
-
if not self._header_rules_enabled:
|
|
311
|
-
# don't draw rule; store the text so _main_title can still use name
|
|
312
|
-
self._last_header_rule = text
|
|
313
|
-
self.header_text = text
|
|
314
|
-
return
|
|
315
|
-
if text and text != self._last_header_rule:
|
|
316
|
-
try:
|
|
317
|
-
self.console.rule(text, style=style)
|
|
318
|
-
except Exception:
|
|
319
|
-
self.console.print(text)
|
|
320
|
-
self._last_header_rule = text
|
|
321
|
-
|
|
322
|
-
def _spinner(self) -> str:
|
|
323
|
-
"""Return animated spinner character."""
|
|
324
|
-
frames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
325
|
-
import time
|
|
326
|
-
|
|
327
|
-
return frames[int(time.time() * 10) % len(frames)]
|
|
328
|
-
|
|
329
|
-
def _has_running_steps(self) -> bool:
|
|
330
|
-
"""Check if any non-finished step is present."""
|
|
331
|
-
for _sid, st in self.steps.by_id.items():
|
|
332
|
-
if st.status != "finished":
|
|
333
|
-
return True
|
|
334
|
-
return False
|
|
335
|
-
|
|
336
|
-
def _is_delegation_tool(self, tool_name: str) -> bool:
|
|
337
|
-
"""Check if a tool name indicates delegation functionality."""
|
|
338
|
-
if not tool_name:
|
|
339
|
-
return False
|
|
340
|
-
# common patterns: delegate_to_weather-sub-agent, delegate_to_math_specialist, delegate, spawn_agent, etc.
|
|
341
|
-
return bool(
|
|
342
|
-
re.search(
|
|
343
|
-
r"delegate_to_|(?:^|_)delegate(?:_|$)|spawn_|sub_agent", tool_name, re.I
|
|
344
|
-
)
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
def _main_title(self) -> str:
|
|
348
|
-
"""Generate main panel title with spinner and status chips."""
|
|
349
|
-
# base name
|
|
350
|
-
name = (self.header_text or "").strip() or "Assistant"
|
|
351
|
-
# strip leading rule emojis if present
|
|
352
|
-
name = name.replace("—", " ").strip()
|
|
353
|
-
# spinner if still working
|
|
354
|
-
mark = "✓" if not self._has_running_steps() else self._spinner()
|
|
355
|
-
# show a tiny hint if there's an active delegate
|
|
356
|
-
active_delegates = sum(
|
|
357
|
-
1
|
|
358
|
-
for sid, st in self.steps.by_id.items()
|
|
359
|
-
if st.kind == "delegate" and st.status != "finished"
|
|
360
|
-
)
|
|
361
|
-
chip = f" • delegating ({active_delegates})" if active_delegates else ""
|
|
362
|
-
# show tools count for parity
|
|
363
|
-
active_tools = sum(
|
|
364
|
-
1
|
|
365
|
-
for st in self.steps.by_id.values()
|
|
366
|
-
if st.kind == "tool" and st.status != "finished"
|
|
367
|
-
)
|
|
368
|
-
chip2 = f" • tools ({active_tools})" if active_tools else ""
|
|
369
|
-
return f"{name} {mark}{chip}{chip2}"
|
|
370
|
-
|
|
371
|
-
def _render_main_panel(self):
|
|
372
|
-
"""Render the main panel with content or placeholder."""
|
|
373
|
-
body = "".join(self.buffer).strip()
|
|
374
|
-
if body:
|
|
375
|
-
return Panel(
|
|
376
|
-
self._render_stream(),
|
|
377
|
-
title=self._main_title(),
|
|
378
|
-
border_style="green",
|
|
379
|
-
)
|
|
380
|
-
# fallback placeholder if no content yet
|
|
381
|
-
placeholder = Text("working…", style="dim")
|
|
382
|
-
if self._has_running_steps():
|
|
383
|
-
placeholder = Text("working… running steps", style="dim")
|
|
384
|
-
return Panel(
|
|
385
|
-
placeholder,
|
|
386
|
-
title=self._main_title(),
|
|
387
|
-
border_style="green",
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
def _norm_markdown(self, s: str) -> str:
|
|
391
|
-
"""Reuse LaTeX normalization for panel bodies."""
|
|
392
|
-
s = self._normalize_math(s)
|
|
393
|
-
return s
|
|
394
|
-
|
|
395
|
-
def _render_context_panels(self):
|
|
396
|
-
"""Render sub-agent panels."""
|
|
397
|
-
panels = []
|
|
398
|
-
for cid in self.context_order:
|
|
399
|
-
chunks = self.context_panels.get(cid) or []
|
|
400
|
-
meta = self.context_meta.get(cid) or {}
|
|
401
|
-
title = meta.get("title") or f"Sub-agent {cid[:6]}…"
|
|
402
|
-
status = meta.get("status") or "running"
|
|
403
|
-
mark = "✓" if status == "finished" else self._spinner()
|
|
404
|
-
body = self._norm_markdown("".join(chunks))
|
|
405
|
-
panels.append(
|
|
406
|
-
Panel(
|
|
407
|
-
Markdown(
|
|
408
|
-
body,
|
|
409
|
-
code_theme=("monokai" if self.theme == "dark" else "github"),
|
|
410
|
-
),
|
|
411
|
-
title=f"{title} {mark}",
|
|
412
|
-
border_style="magenta"
|
|
413
|
-
if meta.get("kind") == "delegate"
|
|
414
|
-
else "cyan",
|
|
415
|
-
)
|
|
416
|
-
)
|
|
417
|
-
return panels
|
|
418
|
-
|
|
419
|
-
def _render_tool_panels(self):
|
|
420
|
-
"""Render tool output panels."""
|
|
421
|
-
panels = []
|
|
422
|
-
for sid in self.tool_order:
|
|
423
|
-
meta = self.tool_panels.get(sid) or {}
|
|
424
|
-
title = meta.get("title") or "Tool"
|
|
425
|
-
status = meta.get("status") or "running"
|
|
426
|
-
mark = "✓" if status == "finished" else self._spinner()
|
|
427
|
-
body = self._norm_markdown("".join(meta.get("chunks") or []))
|
|
428
|
-
panels.append(
|
|
429
|
-
Panel(
|
|
430
|
-
Markdown(
|
|
431
|
-
body,
|
|
432
|
-
code_theme=("monokai" if self.theme == "dark" else "github"),
|
|
433
|
-
),
|
|
434
|
-
title=f"{title} {mark}",
|
|
435
|
-
border_style="blue",
|
|
436
|
-
)
|
|
437
|
-
)
|
|
438
|
-
return panels
|
|
439
|
-
|
|
440
|
-
def _process_tool_output_for_sub_agents(
|
|
441
|
-
self, tool_name: str, output: str, task_id: str, context_id: str
|
|
442
|
-
) -> bool:
|
|
443
|
-
"""Process tool output to extract and create sub-agent panels."""
|
|
444
|
-
if not output:
|
|
445
|
-
return False
|
|
446
|
-
|
|
447
|
-
# Check if this is a delegation tool (contains sub-agent responses)
|
|
448
|
-
if "delegate" in (tool_name or "").lower() or "math_specialist" in output:
|
|
449
|
-
# Extract sub-agent name from output (e.g., "[math_specialist] ...")
|
|
450
|
-
import re
|
|
451
|
-
|
|
452
|
-
agent_match = re.search(r"^\s*\[([^\]]+)\]\s*(.*)$", output, re.S)
|
|
453
|
-
if agent_match:
|
|
454
|
-
agent_name = agent_match.group(1).strip()
|
|
455
|
-
agent_content = agent_match.group(2).strip()
|
|
456
|
-
|
|
457
|
-
# Create a unique context ID for this sub-agent response
|
|
458
|
-
sub_context_id = f"{context_id}_sub_{agent_name}"
|
|
459
|
-
|
|
460
|
-
# Create sub-agent panel if it doesn't exist
|
|
461
|
-
if sub_context_id not in self.context_panels:
|
|
462
|
-
self.context_panels[sub_context_id] = []
|
|
463
|
-
self.context_meta[sub_context_id] = {
|
|
464
|
-
"title": f"Sub-Agent: {agent_name}",
|
|
465
|
-
"kind": "delegate",
|
|
466
|
-
"status": "finished", # Already completed
|
|
467
|
-
}
|
|
468
|
-
self.context_order.append(sub_context_id)
|
|
469
|
-
|
|
470
|
-
# Add the content to the sub-agent panel
|
|
471
|
-
self.context_panels[sub_context_id].append(agent_content)
|
|
472
|
-
|
|
473
|
-
# Mark as finished since it's already complete
|
|
474
|
-
self.context_meta[sub_context_id]["status"] = "finished"
|
|
475
|
-
return True
|
|
476
|
-
return False
|
|
477
|
-
|
|
478
|
-
def _render_tools(self):
|
|
479
|
-
if not (self.steps.order or self.steps.children):
|
|
480
|
-
return Text("")
|
|
481
|
-
|
|
482
|
-
if not self.verbose:
|
|
483
|
-
# collapsed: one line per top-level step (children are summarized)
|
|
484
|
-
lines = []
|
|
485
|
-
# Get terminal width for better spacing
|
|
486
|
-
width = max(40, self.console.size.width - 8)
|
|
487
|
-
args_budget = min(60, width // 3)
|
|
488
|
-
|
|
489
|
-
for sid in self.steps.order:
|
|
490
|
-
st = self.steps.by_id[sid]
|
|
491
|
-
icon = (
|
|
492
|
-
"⚙️ "
|
|
493
|
-
if (self.use_emoji and st.kind == "tool")
|
|
494
|
-
else ("🤝 " if (self.use_emoji and st.kind == "delegate") else "")
|
|
495
|
-
)
|
|
496
|
-
dur = f"[{st.duration_ms}ms]" if st.duration_ms is not None else ""
|
|
497
|
-
|
|
498
|
-
# Truncate args to fit budget
|
|
499
|
-
args = _pretty_args(st.args, max_len=args_budget)
|
|
500
|
-
rhs = f"({args})" if args else ""
|
|
501
|
-
|
|
502
|
-
# Use spinner for running steps, checkmark for finished
|
|
503
|
-
tail = " ✓" if st.status == "finished" else f" {self._spinner()}"
|
|
504
|
-
|
|
505
|
-
# Show actual tool name or improved step name
|
|
506
|
-
display_name = st.name if st.name != "step" else f"{st.kind} step"
|
|
507
|
-
lines.append(f"{icon}{display_name}{rhs} {dur}{tail}".rstrip())
|
|
508
|
-
return Text("\n".join(lines), style="dim")
|
|
509
|
-
|
|
510
|
-
# verbose: full tree
|
|
511
|
-
def add_children(node: Tree, sid: str):
|
|
512
|
-
st = self.steps.by_id[sid]
|
|
513
|
-
dur = f"[{st.duration_ms}ms]" if st.duration_ms is not None else ""
|
|
514
|
-
args_str = _pretty_args(st.args)
|
|
515
|
-
icon = (
|
|
516
|
-
"⚙️" if st.kind == "tool" else ("🤝" if st.kind == "delegate" else "🧠")
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
# Build detailed label for verbose mode
|
|
520
|
-
label = f"{icon} {st.name}"
|
|
521
|
-
if args_str:
|
|
522
|
-
label += f"({args_str})"
|
|
523
|
-
|
|
524
|
-
# Add tool output for finished tools (truncated for display)
|
|
525
|
-
if st.status == "finished" and st.output and st.kind == "tool":
|
|
526
|
-
# For verbose mode, show more output details
|
|
527
|
-
if len(st.output) <= 200:
|
|
528
|
-
# Short output: show inline
|
|
529
|
-
output_preview = _pretty_out(st.output, max_len=200)
|
|
530
|
-
if output_preview:
|
|
531
|
-
label += f" → {output_preview}"
|
|
532
|
-
else:
|
|
533
|
-
# Long output: show first part with indicator
|
|
534
|
-
first_line = st.output.split("\n")[0][:100]
|
|
535
|
-
if first_line:
|
|
536
|
-
label += f" → {first_line}... (truncated, see tool panel for full output)"
|
|
537
|
-
|
|
538
|
-
label += f" {dur} {'✓' if st.status=='finished' else '…'}"
|
|
539
|
-
node2 = node.add(label)
|
|
540
|
-
for child_id in self.steps.children.get(sid, []):
|
|
541
|
-
add_children(node2, child_id)
|
|
542
|
-
|
|
543
|
-
root = Tree("Steps")
|
|
544
|
-
for sid in self.steps.order:
|
|
545
|
-
add_children(root, sid)
|
|
546
|
-
return root
|
|
547
|
-
|
|
548
|
-
def _normalize_math(self, md: str) -> str:
|
|
549
|
-
"""Robust LaTeX normalization for better display."""
|
|
550
|
-
import re
|
|
551
|
-
|
|
552
|
-
# \text{...} → plain
|
|
553
|
-
md = re.sub(r"\\text\{([^}]*)\}", r"\1", md)
|
|
554
|
-
|
|
555
|
-
# simple symbols
|
|
556
|
-
md = md.replace(r"\times", "×").replace(r"\cdot", "·")
|
|
557
|
-
|
|
558
|
-
# \boxed{...} → **...**
|
|
559
|
-
md = re.sub(r"\\boxed\{([^}]*)\}", r"**\1**", md)
|
|
560
|
-
|
|
561
|
-
# \begin{array}{...} ... \end{array} → code block
|
|
562
|
-
def _array_to_block(m):
|
|
563
|
-
body = m.group("body")
|
|
564
|
-
# drop leading alignment spec {c@{}c@{}c} etc at start of body if present
|
|
565
|
-
body = re.sub(r"^\{[^}]*\}\s*", "", body.strip())
|
|
566
|
-
|
|
567
|
-
# split on LaTeX row separator \\ and replace alignment & with spacing
|
|
568
|
-
rows = []
|
|
569
|
-
for raw in re.split(r"\\\\", body):
|
|
570
|
-
line = raw.strip()
|
|
571
|
-
if not line:
|
|
572
|
-
continue
|
|
573
|
-
# remove explicit + / bullets spacing issues and align with double-spaces
|
|
574
|
-
line = line.replace("&", " ")
|
|
575
|
-
line = re.sub(r"\s{3,}", " ", line)
|
|
576
|
-
rows.append(line)
|
|
577
|
-
|
|
578
|
-
return "```\n" + "\n".join(rows) + "\n```"
|
|
579
|
-
|
|
580
|
-
md = re.sub(
|
|
581
|
-
r"\\begin\{array\}\{[^}]*\}(?P<body>.*?)\\end\{array\}",
|
|
582
|
-
_array_to_block,
|
|
583
|
-
md,
|
|
584
|
-
flags=re.S,
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
# Block math \[...\] → fenced code
|
|
588
|
-
md = re.sub(r"\\\[(.*?)\\\]", r"```\n\1\n```", md, flags=re.S)
|
|
589
|
-
|
|
590
|
-
# Inline math \(...\) → inline code
|
|
591
|
-
md = re.sub(r"\\\((.*?)\\\)", r"`\1`", md, flags=re.S)
|
|
592
|
-
|
|
593
|
-
# Strip any remaining begin/end environments harmlessly
|
|
594
|
-
md = re.sub(r"\\begin\{[^}]*\}|\\end\{[^}]*\}", "", md)
|
|
595
|
-
|
|
596
|
-
return md
|
|
597
|
-
|
|
598
|
-
def _render_stream(self) -> Markdown:
|
|
599
|
-
"""Render the streaming markdown content."""
|
|
600
|
-
content = "".join(self.buffer)
|
|
601
|
-
content = self._normalize_math(content)
|
|
602
|
-
code_theme = "monokai" if self.theme == "dark" else "github"
|
|
603
|
-
return Markdown(content, code_theme=code_theme)
|
|
604
|
-
|
|
605
|
-
def _ensure_live(self):
|
|
606
|
-
"""Ensure live area is started for any first event."""
|
|
607
|
-
if self._live:
|
|
608
|
-
return
|
|
609
|
-
# Start live view without outer panel to fix double boxing
|
|
610
|
-
self._live = Live(
|
|
611
|
-
refresh_per_second=24,
|
|
612
|
-
console=self.console,
|
|
613
|
-
transient=not self.persist_live, # Respect flag
|
|
614
|
-
auto_refresh=True,
|
|
615
|
-
)
|
|
616
|
-
self._live.start()
|
|
617
|
-
|
|
618
|
-
def _refresh(self) -> None:
|
|
619
|
-
"""Refresh the live display with stacked panels."""
|
|
620
|
-
if not self._live:
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
panels = [self._render_main_panel()] # Main assistant content
|
|
624
|
-
|
|
625
|
-
# Only include Steps panel when there are actual steps
|
|
626
|
-
if self.steps.order or self.steps.children:
|
|
627
|
-
panels.append(
|
|
628
|
-
Panel(
|
|
629
|
-
self._render_tools(),
|
|
630
|
-
title="Steps (collapsed)" if not self.verbose else "Steps",
|
|
631
|
-
border_style="blue",
|
|
632
|
-
)
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
context_panels = self._render_context_panels()
|
|
636
|
-
|
|
637
|
-
panels.extend(context_panels) # Sub-agent panels
|
|
638
|
-
panels.extend(self._render_tool_panels()) # tools
|
|
639
|
-
|
|
640
|
-
self._live.update(Group(*panels))
|
|
641
|
-
|
|
642
|
-
def _refresh_thread_safe(self) -> None:
|
|
643
|
-
"""Thread-safe refresh method for use from background threads."""
|
|
644
|
-
if not self._live:
|
|
645
|
-
return
|
|
646
|
-
|
|
647
|
-
try:
|
|
648
|
-
# Use console.call_from_thread for thread-safe updates
|
|
649
|
-
self._live.console.call_from_thread(self._refresh)
|
|
650
|
-
except Exception:
|
|
651
|
-
# Fallback to direct call if call_from_thread fails
|
|
652
|
-
try:
|
|
653
|
-
self._refresh()
|
|
654
|
-
except Exception:
|
|
655
|
-
pass # Ignore refresh errors
|
|
656
|
-
|
|
657
|
-
def on_start(self, meta: dict[str, Any]) -> None:
|
|
658
|
-
"""Handle agent run start."""
|
|
659
|
-
parts = []
|
|
660
|
-
|
|
661
|
-
# Add emoji if enabled
|
|
662
|
-
if self.use_emoji:
|
|
663
|
-
parts.append("🤖")
|
|
664
|
-
|
|
665
|
-
# Add agent name
|
|
666
|
-
agent_name = meta.get("agent_name", "agent")
|
|
667
|
-
if agent_name:
|
|
668
|
-
parts.append(agent_name)
|
|
669
|
-
|
|
670
|
-
# Add model if available
|
|
671
|
-
model = meta.get("model", "")
|
|
672
|
-
if model:
|
|
673
|
-
parts.append("•")
|
|
674
|
-
parts.append(model)
|
|
675
|
-
|
|
676
|
-
# Add run ID if available
|
|
677
|
-
run_id = meta.get("run_id", "")
|
|
678
|
-
if run_id:
|
|
679
|
-
parts.append("•")
|
|
680
|
-
parts.append(run_id)
|
|
681
|
-
|
|
682
|
-
self.header_text = " ".join(parts)
|
|
683
|
-
|
|
684
|
-
# Show a compact header once (de-duplicated)
|
|
685
|
-
self._print_header_once(self.header_text)
|
|
686
|
-
|
|
687
|
-
# Show the original query for context
|
|
688
|
-
query = meta.get("input_message") or meta.get("query") or meta.get("message")
|
|
689
|
-
if query:
|
|
690
|
-
from rich.markdown import Markdown
|
|
691
|
-
|
|
692
|
-
self.console.print(
|
|
693
|
-
Panel(
|
|
694
|
-
Markdown(f"**Query:** {query}"),
|
|
695
|
-
title="User Request",
|
|
696
|
-
border_style="yellow",
|
|
697
|
-
padding=(0, 1),
|
|
698
|
-
)
|
|
699
|
-
)
|
|
700
|
-
|
|
701
|
-
# Don't start live display immediately - wait for actual content
|
|
702
|
-
self._live = None
|
|
703
|
-
|
|
704
|
-
def on_event(self, ev: dict[str, Any]) -> None:
|
|
705
|
-
"""Handle streaming events from the backend."""
|
|
706
|
-
try:
|
|
707
|
-
# Handle different event types based on backend's SSE format
|
|
708
|
-
metadata = ev.get("metadata", {})
|
|
709
|
-
kind = metadata.get("kind", "")
|
|
710
|
-
|
|
711
|
-
if kind == "artifact":
|
|
712
|
-
return # Hidden by default
|
|
713
|
-
|
|
714
|
-
# --- tool steps (collapsed) ---
|
|
715
|
-
if kind == "agent_step":
|
|
716
|
-
# Extract task and context IDs from metadata or direct fields
|
|
717
|
-
task_id = ev.get("task_id") or metadata.get("task_id")
|
|
718
|
-
context_id = ev.get("context_id") or metadata.get("context_id")
|
|
719
|
-
|
|
720
|
-
status = metadata.get("status", "running")
|
|
721
|
-
|
|
722
|
-
# Tool name + args + (optional) output
|
|
723
|
-
tool_name = None
|
|
724
|
-
tool_args = {}
|
|
725
|
-
tool_out = None
|
|
726
|
-
|
|
727
|
-
# First try the tool_calls field (legacy)
|
|
728
|
-
tc = ev.get("tool_calls")
|
|
729
|
-
if isinstance(tc, list) and tc:
|
|
730
|
-
tool_name = (tc[0] or {}).get("name")
|
|
731
|
-
tool_args = (tc[0] or {}).get("args") or {}
|
|
732
|
-
elif isinstance(tc, dict):
|
|
733
|
-
tool_name = tc.get("name")
|
|
734
|
-
tool_args = tc.get("args") or {}
|
|
735
|
-
tool_out = tc.get("output")
|
|
736
|
-
|
|
737
|
-
# Then try the tool_info field in metadata (new format)
|
|
738
|
-
tool_info = metadata.get("tool_info", {})
|
|
739
|
-
if tool_info and not tool_name:
|
|
740
|
-
# Handle running tool calls
|
|
741
|
-
tool_calls = tool_info.get("tool_calls", [])
|
|
742
|
-
if tool_calls and isinstance(tool_calls, list):
|
|
743
|
-
first_call = tool_calls[0] if tool_calls else {}
|
|
744
|
-
tool_name = first_call.get("name")
|
|
745
|
-
tool_args = first_call.get("args", {})
|
|
746
|
-
|
|
747
|
-
# Handle finished tool with output
|
|
748
|
-
if tool_info.get("name"):
|
|
749
|
-
tool_name = tool_info.get("name")
|
|
750
|
-
tool_args = tool_info.get("args", {})
|
|
751
|
-
tool_out = tool_info.get("output")
|
|
752
|
-
|
|
753
|
-
# Heuristic: delegation events (sub-agent) signalled by message, or parent/child context ids
|
|
754
|
-
message_en = ev.get("metadata", {}).get("message", {}).get("en", "")
|
|
755
|
-
maybe_delegate = bool(
|
|
756
|
-
re.search(
|
|
757
|
-
r"\bdelegat(e|ion|ed)\b|\bspawn(ed)?\b|\bsub[- ]?agent\b",
|
|
758
|
-
message_en,
|
|
759
|
-
re.I,
|
|
760
|
-
)
|
|
761
|
-
)
|
|
762
|
-
child_ctx = ev.get("child_context_id") or ev.get("sub_context_id")
|
|
763
|
-
|
|
764
|
-
# Check if this is a delegation tool (like delegate_to_weather-sub-agent)
|
|
765
|
-
is_delegation_tool = tool_name and self._is_delegation_tool(tool_name)
|
|
766
|
-
|
|
767
|
-
# Parent mapping: if this step spawns a child context, remember who spawned it
|
|
768
|
-
parent_id = None
|
|
769
|
-
if maybe_delegate and child_ctx:
|
|
770
|
-
# start a delegate step at current context; child steps will hang under it
|
|
771
|
-
parent_step = self.steps.start_or_get(
|
|
772
|
-
task_id=task_id,
|
|
773
|
-
context_id=context_id,
|
|
774
|
-
kind="delegate",
|
|
775
|
-
name=ev.get("delegate_name")
|
|
776
|
-
or ev.get("agent_name")
|
|
777
|
-
or "delegate",
|
|
778
|
-
args={},
|
|
779
|
-
)
|
|
780
|
-
self.context_parent[child_ctx] = parent_step.step_id
|
|
781
|
-
|
|
782
|
-
# NEW: reserve a sub-agent panel immediately (spinner until content arrives)
|
|
783
|
-
title = (
|
|
784
|
-
ev.get("delegate_name")
|
|
785
|
-
or ev.get("agent_name")
|
|
786
|
-
or f"Sub-agent {child_ctx[:6]}…"
|
|
787
|
-
)
|
|
788
|
-
if child_ctx not in self.context_panels:
|
|
789
|
-
self.context_panels[child_ctx] = []
|
|
790
|
-
self.context_meta[child_ctx] = {
|
|
791
|
-
"title": f"Sub-Agent: {title}",
|
|
792
|
-
"kind": "delegate",
|
|
793
|
-
"status": "running",
|
|
794
|
-
}
|
|
795
|
-
self.context_order.append(child_ctx)
|
|
796
|
-
|
|
797
|
-
self._ensure_live() # Ensure live for step-first runs
|
|
798
|
-
self._refresh()
|
|
799
|
-
return
|
|
800
|
-
|
|
801
|
-
# If this is a delegation tool, create a sub-agent panel immediately
|
|
802
|
-
if is_delegation_tool and not self.show_delegate_tool_panels:
|
|
803
|
-
# Extract sub-agent name from tool name (e.g., "delegate_to_weather-sub-agent" -> "weather-sub-agent")
|
|
804
|
-
sub_agent_name = tool_name.replace("delegate_to_", "").replace(
|
|
805
|
-
"delegate_", ""
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
# Create a unique context ID for this delegation
|
|
809
|
-
delegation_context_id = f"{context_id}_delegation_{tool_name}"
|
|
810
|
-
|
|
811
|
-
# Create sub-agent panel immediately
|
|
812
|
-
if delegation_context_id not in self.context_panels:
|
|
813
|
-
self.context_panels[delegation_context_id] = []
|
|
814
|
-
self.context_meta[delegation_context_id] = {
|
|
815
|
-
"title": f"Sub-Agent: {sub_agent_name}",
|
|
816
|
-
"kind": "delegate",
|
|
817
|
-
"status": "running",
|
|
818
|
-
}
|
|
819
|
-
self.context_order.append(delegation_context_id)
|
|
820
|
-
|
|
821
|
-
# Mark this as a delegation tool to avoid creating tool panels
|
|
822
|
-
is_delegation_tool = True
|
|
823
|
-
|
|
824
|
-
# Pick kind for this step
|
|
825
|
-
kind_name = (
|
|
826
|
-
"tool" if tool_name else ("delegate" if maybe_delegate else "agent")
|
|
827
|
-
)
|
|
828
|
-
name = tool_name or (
|
|
829
|
-
ev.get("delegate_name") or ev.get("agent_name") or "step"
|
|
830
|
-
)
|
|
831
|
-
|
|
832
|
-
# Parent: if this event belongs to a child context, attach under its spawner
|
|
833
|
-
parent_id = self.context_parent.get(context_id)
|
|
834
|
-
|
|
835
|
-
# Determine if this is a running or finished step
|
|
836
|
-
# If tool_calls is a list, it's starting; if it's a dict with output, it's finishing
|
|
837
|
-
if isinstance(tc, list):
|
|
838
|
-
status = "running"
|
|
839
|
-
elif isinstance(tc, dict) and tc.get("output"):
|
|
840
|
-
status = "finished"
|
|
841
|
-
else:
|
|
842
|
-
status = status or ev.get("status") or "running"
|
|
843
|
-
dur_raw = ev.get("metadata", {}).get("time")
|
|
844
|
-
|
|
845
|
-
if status == "running":
|
|
846
|
-
st = self.steps.find_running(
|
|
847
|
-
task_id=task_id,
|
|
848
|
-
context_id=context_id,
|
|
849
|
-
kind=kind_name,
|
|
850
|
-
name=name,
|
|
851
|
-
)
|
|
852
|
-
if not st:
|
|
853
|
-
st = self.steps.start_or_get(
|
|
854
|
-
task_id=task_id,
|
|
855
|
-
context_id=context_id,
|
|
856
|
-
kind=kind_name,
|
|
857
|
-
name=name,
|
|
858
|
-
parent_id=parent_id,
|
|
859
|
-
args=tool_args,
|
|
860
|
-
)
|
|
861
|
-
|
|
862
|
-
# If it's a tool, ensure a tool panel exists and is running
|
|
863
|
-
if kind_name == "tool":
|
|
864
|
-
# Suppress tool panel for delegation tools unless explicitly enabled
|
|
865
|
-
should_show_panel = (
|
|
866
|
-
not self._is_delegation_tool(name)
|
|
867
|
-
or self.show_delegate_tool_panels
|
|
868
|
-
)
|
|
869
|
-
if should_show_panel:
|
|
870
|
-
sid = st.step_id
|
|
871
|
-
if sid not in self.tool_panels:
|
|
872
|
-
self.tool_panels[sid] = {
|
|
873
|
-
"title": f"Tool: {name}",
|
|
874
|
-
"status": "running",
|
|
875
|
-
"chunks": [],
|
|
876
|
-
}
|
|
877
|
-
self.tool_order.append(sid)
|
|
878
|
-
else:
|
|
879
|
-
st = self.steps.finish(
|
|
880
|
-
task_id=task_id,
|
|
881
|
-
context_id=context_id,
|
|
882
|
-
kind=kind_name,
|
|
883
|
-
name=name,
|
|
884
|
-
output=tool_out,
|
|
885
|
-
duration_raw=dur_raw,
|
|
886
|
-
)
|
|
887
|
-
|
|
888
|
-
if kind_name == "tool":
|
|
889
|
-
sid = st.step_id
|
|
890
|
-
|
|
891
|
-
out = tool_out or ""
|
|
892
|
-
|
|
893
|
-
# Handle delegation tools by updating sub-agent panels
|
|
894
|
-
if (
|
|
895
|
-
self._is_delegation_tool(name)
|
|
896
|
-
and not self.show_delegate_tool_panels
|
|
897
|
-
):
|
|
898
|
-
# Find the corresponding sub-agent panel and update it
|
|
899
|
-
delegation_context_id = f"{context_id}_delegation_{name}"
|
|
900
|
-
if delegation_context_id in self.context_panels:
|
|
901
|
-
# Update the sub-agent panel with the delegation tool output
|
|
902
|
-
self.context_panels[delegation_context_id].append(out)
|
|
903
|
-
self.context_meta[delegation_context_id]["status"] = (
|
|
904
|
-
"finished"
|
|
905
|
-
)
|
|
906
|
-
|
|
907
|
-
# Remove any accidentally created tool panel
|
|
908
|
-
if sid in self.tool_panels:
|
|
909
|
-
self.tool_panels.pop(sid, None)
|
|
910
|
-
try:
|
|
911
|
-
self.tool_order.remove(sid)
|
|
912
|
-
except ValueError:
|
|
913
|
-
pass
|
|
914
|
-
|
|
915
|
-
self._ensure_live()
|
|
916
|
-
self._refresh()
|
|
917
|
-
return
|
|
918
|
-
|
|
919
|
-
# First, see if this created a sub-agent panel
|
|
920
|
-
self._process_tool_output_for_sub_agents(
|
|
921
|
-
name, out, task_id, context_id
|
|
922
|
-
)
|
|
923
|
-
|
|
924
|
-
# If it's a delegation tool and we don't want its panel, suppress it
|
|
925
|
-
if (
|
|
926
|
-
self._is_delegation_tool(name)
|
|
927
|
-
and not self.show_delegate_tool_panels
|
|
928
|
-
):
|
|
929
|
-
# If a running tool panel was accidentally created, remove it
|
|
930
|
-
if sid in self.tool_panels:
|
|
931
|
-
self.tool_panels.pop(sid, None)
|
|
932
|
-
try:
|
|
933
|
-
self.tool_order.remove(sid)
|
|
934
|
-
except ValueError:
|
|
935
|
-
pass
|
|
936
|
-
self._ensure_live()
|
|
937
|
-
self._refresh()
|
|
938
|
-
return
|
|
939
|
-
|
|
940
|
-
# Normal (non-delegation) tool panel behavior
|
|
941
|
-
panel = self.tool_panels.get(sid)
|
|
942
|
-
if not panel:
|
|
943
|
-
panel = {
|
|
944
|
-
"title": f"Tool: {name}",
|
|
945
|
-
"status": "running",
|
|
946
|
-
"chunks": [],
|
|
947
|
-
}
|
|
948
|
-
self.tool_panels[sid] = panel
|
|
949
|
-
self.tool_order.append(sid)
|
|
950
|
-
|
|
951
|
-
if bool(out) and (
|
|
952
|
-
(out.strip().startswith("{") and out.strip().endswith("}"))
|
|
953
|
-
or (
|
|
954
|
-
out.strip().startswith("[")
|
|
955
|
-
and out.strip().endswith("]")
|
|
956
|
-
)
|
|
957
|
-
):
|
|
958
|
-
panel["chunks"].append("```json\n" + out + "\n```")
|
|
959
|
-
else:
|
|
960
|
-
panel["chunks"].append(out)
|
|
961
|
-
|
|
962
|
-
# trim for memory
|
|
963
|
-
if sum(len(x) for x in panel["chunks"]) > 20000:
|
|
964
|
-
joined = "".join(panel["chunks"])[-10000:]
|
|
965
|
-
panel["chunks"] = [joined]
|
|
966
|
-
|
|
967
|
-
panel["status"] = "finished"
|
|
968
|
-
|
|
969
|
-
self._ensure_live() # Ensure live for step-first runs
|
|
970
|
-
self._refresh()
|
|
971
|
-
return
|
|
972
|
-
|
|
973
|
-
# --- status updates (backend sends status: "streaming_started", "execution_started", etc.) ---
|
|
974
|
-
if "status" in ev and ev.get("metadata", {}).get("kind") != "agent_step":
|
|
975
|
-
status_msg = ev.get("status", "")
|
|
976
|
-
if status_msg in ("streaming_started", "execution_started"):
|
|
977
|
-
# These are informational status updates, no need to display
|
|
978
|
-
return
|
|
979
|
-
self._last_status = status_msg # keep it if you want chips later
|
|
980
|
-
# no rule printing here; _main_title() already animates
|
|
981
|
-
return
|
|
982
|
-
|
|
983
|
-
# --- content streaming with boundary spacing ---
|
|
984
|
-
if "content" in ev and ev["content"]:
|
|
985
|
-
content = ev["content"]
|
|
986
|
-
|
|
987
|
-
if "Artifact received:" in content:
|
|
988
|
-
return
|
|
989
|
-
|
|
990
|
-
cid = ev.get("context_id") or metadata.get("context_id")
|
|
991
|
-
|
|
992
|
-
# establish root context on first content
|
|
993
|
-
if self.root_context_id is None and cid:
|
|
994
|
-
self.root_context_id = cid
|
|
995
|
-
|
|
996
|
-
# sub-agent / child context streaming → stream into its own panel
|
|
997
|
-
if cid and self.root_context_id and cid != self.root_context_id:
|
|
998
|
-
self._ensure_live()
|
|
999
|
-
if cid not in self.context_panels:
|
|
1000
|
-
# Create the panel the first time we see content
|
|
1001
|
-
title = (
|
|
1002
|
-
ev.get("agent_name")
|
|
1003
|
-
or ev.get("delegate_name")
|
|
1004
|
-
or f"Sub-agent {cid[:6]}…"
|
|
1005
|
-
)
|
|
1006
|
-
self.context_panels[cid] = []
|
|
1007
|
-
self.context_meta[cid] = {
|
|
1008
|
-
"title": f"Sub-Agent: {title}",
|
|
1009
|
-
"kind": "delegate",
|
|
1010
|
-
"status": "running",
|
|
1011
|
-
}
|
|
1012
|
-
self.context_order.append(cid)
|
|
1013
|
-
|
|
1014
|
-
# append & trim (memory guard)
|
|
1015
|
-
buf = self.context_panels[cid]
|
|
1016
|
-
buf.append(content)
|
|
1017
|
-
if sum(len(x) for x in buf) > 20000:
|
|
1018
|
-
# keep last ~10k chars
|
|
1019
|
-
joined = "".join(buf)[-10000:]
|
|
1020
|
-
self.context_panels[cid] = [joined]
|
|
1021
|
-
|
|
1022
|
-
self._refresh()
|
|
1023
|
-
return
|
|
1024
|
-
|
|
1025
|
-
# root / unknown context → assistant
|
|
1026
|
-
self._ensure_live() # Ensure live for content-first runs
|
|
1027
|
-
|
|
1028
|
-
# insert a space at boundary when needed
|
|
1029
|
-
if (
|
|
1030
|
-
self.buffer
|
|
1031
|
-
and self.buffer[-1]
|
|
1032
|
-
and self.buffer[-1][-1].isalnum()
|
|
1033
|
-
and content
|
|
1034
|
-
and content[0].isalnum()
|
|
1035
|
-
):
|
|
1036
|
-
self.buffer.append(" ")
|
|
1037
|
-
self.buffer.append(content)
|
|
1038
|
-
|
|
1039
|
-
# Memory guard: trim main buffer if it gets too large
|
|
1040
|
-
joined = "".join(self.buffer)
|
|
1041
|
-
if len(joined) > 200_000: # ~200KB
|
|
1042
|
-
self.buffer = [joined[-100_000:]] # keep last ~100KB
|
|
1043
|
-
|
|
1044
|
-
self._refresh()
|
|
1045
|
-
return
|
|
16
|
+
import logging
|
|
1046
17
|
|
|
1047
|
-
|
|
1048
|
-
except Exception as e:
|
|
1049
|
-
# Log the error and ensure Live is stopped to prevent terminal corruption
|
|
1050
|
-
logger.error(f"Error in event handler: {e}")
|
|
1051
|
-
try:
|
|
1052
|
-
if self._live:
|
|
1053
|
-
self._live.stop()
|
|
1054
|
-
except Exception:
|
|
1055
|
-
pass # Ignore cleanup errors
|
|
1056
|
-
raise # Re-raise the original exception
|
|
18
|
+
from glaip_sdk.utils.rendering.models import RunStats
|
|
1057
19
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
if final:
|
|
1063
|
-
whole = "".join(self.buffer)
|
|
1064
|
-
if not whole or final not in whole:
|
|
1065
|
-
if (
|
|
1066
|
-
self.buffer
|
|
1067
|
-
and self.buffer[-1]
|
|
1068
|
-
and self.buffer[-1][-1].isalnum()
|
|
1069
|
-
and final
|
|
1070
|
-
and final[0].isalnum()
|
|
1071
|
-
):
|
|
1072
|
-
self.buffer.append(" ")
|
|
1073
|
-
self.buffer.append(final)
|
|
20
|
+
# Re-export main components from the new modular renderer package
|
|
21
|
+
from glaip_sdk.utils.rendering.renderer.base import RichStreamRenderer
|
|
22
|
+
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
23
|
+
from glaip_sdk.utils.rendering.renderer.console import CapturingConsole
|
|
1074
24
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
25
|
+
# Legacy imports for backward compatibility
|
|
26
|
+
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
27
|
+
from glaip_sdk.utils.rendering.steps import StepManager
|
|
1078
28
|
|
|
1079
|
-
|
|
1080
|
-
if self._live is None:
|
|
1081
|
-
self._ensure_live()
|
|
29
|
+
logger = logging.getLogger("glaip_sdk.run_renderer")
|
|
1082
30
|
|
|
1083
|
-
# update both panels one last time
|
|
1084
|
-
self._refresh()
|
|
1085
31
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
footer.append(f"${u['cost']}")
|
|
1097
|
-
if footer:
|
|
1098
|
-
self.console.print(Text(" • ".join(footer), style="bold green"))
|
|
1099
|
-
finally:
|
|
1100
|
-
# Always ensure Live is stopped, even if an exception occurs
|
|
1101
|
-
try:
|
|
1102
|
-
if self._live:
|
|
1103
|
-
self._live.stop()
|
|
1104
|
-
except Exception:
|
|
1105
|
-
pass # Ignore errors during cleanup
|
|
32
|
+
# The full implementation has been moved to glaip_sdk.utils.rendering.renderer.base
|
|
33
|
+
# This file now serves as a compatibility shim for existing imports.
|
|
34
|
+
__all__ = [
|
|
35
|
+
"CapturingConsole",
|
|
36
|
+
"RendererConfig",
|
|
37
|
+
"RichStreamRenderer",
|
|
38
|
+
"render_debug_event",
|
|
39
|
+
"RunStats",
|
|
40
|
+
"StepManager",
|
|
41
|
+
]
|