glaip-sdk 0.0.5b1__py3-none-any.whl → 0.0.7__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 +1 -1
- glaip_sdk/_version.py +42 -19
- glaip_sdk/branding.py +3 -2
- glaip_sdk/cli/commands/__init__.py +1 -1
- glaip_sdk/cli/commands/agents.py +452 -285
- glaip_sdk/cli/commands/configure.py +14 -13
- glaip_sdk/cli/commands/mcps.py +30 -20
- glaip_sdk/cli/commands/models.py +5 -3
- glaip_sdk/cli/commands/tools.py +111 -106
- glaip_sdk/cli/display.py +48 -27
- glaip_sdk/cli/io.py +1 -1
- glaip_sdk/cli/main.py +26 -5
- glaip_sdk/cli/resolution.py +5 -4
- glaip_sdk/cli/utils.py +437 -188
- glaip_sdk/cli/validators.py +7 -2
- glaip_sdk/client/agents.py +276 -153
- glaip_sdk/client/base.py +69 -27
- glaip_sdk/client/tools.py +44 -26
- glaip_sdk/client/validators.py +154 -94
- glaip_sdk/config/constants.py +0 -2
- glaip_sdk/models.py +5 -4
- glaip_sdk/utils/__init__.py +7 -7
- glaip_sdk/utils/client_utils.py +191 -101
- glaip_sdk/utils/display.py +4 -2
- glaip_sdk/utils/general.py +8 -6
- glaip_sdk/utils/import_export.py +58 -25
- glaip_sdk/utils/rendering/formatting.py +12 -6
- glaip_sdk/utils/rendering/models.py +1 -1
- glaip_sdk/utils/rendering/renderer/base.py +523 -332
- glaip_sdk/utils/rendering/renderer/console.py +6 -5
- glaip_sdk/utils/rendering/renderer/debug.py +94 -52
- glaip_sdk/utils/rendering/renderer/stream.py +93 -48
- glaip_sdk/utils/rendering/steps.py +103 -39
- glaip_sdk/utils/rich_utils.py +1 -1
- glaip_sdk/utils/run_renderer.py +1 -1
- glaip_sdk/utils/serialization.py +9 -3
- glaip_sdk/utils/validation.py +2 -2
- glaip_sdk-0.0.7.dist-info/METADATA +183 -0
- glaip_sdk-0.0.7.dist-info/RECORD +55 -0
- glaip_sdk-0.0.5b1.dist-info/METADATA +0 -645
- glaip_sdk-0.0.5b1.dist-info/RECORD +0 -55
- {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.7.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.7.dist-info}/entry_points.txt +0 -0
|
@@ -24,7 +24,7 @@ from glaip_sdk.utils.rendering.formatting import (
|
|
|
24
24
|
get_spinner_char,
|
|
25
25
|
is_step_finished,
|
|
26
26
|
)
|
|
27
|
-
from glaip_sdk.utils.rendering.models import RunStats
|
|
27
|
+
from glaip_sdk.utils.rendering.models import RunStats, Step
|
|
28
28
|
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
29
29
|
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
30
30
|
from glaip_sdk.utils.rendering.renderer.panels import (
|
|
@@ -56,7 +56,7 @@ class RendererState:
|
|
|
56
56
|
printed_final_panel: bool = False
|
|
57
57
|
finalizing_ui: bool = False
|
|
58
58
|
|
|
59
|
-
def __post_init__(self):
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
60
|
if self.buffer is None:
|
|
61
61
|
self.buffer = []
|
|
62
62
|
|
|
@@ -66,11 +66,11 @@ class RichStreamRenderer:
|
|
|
66
66
|
|
|
67
67
|
def __init__(
|
|
68
68
|
self,
|
|
69
|
-
console=None,
|
|
69
|
+
console: RichConsole | None = None,
|
|
70
70
|
*,
|
|
71
71
|
cfg: RendererConfig | None = None,
|
|
72
72
|
verbose: bool = False,
|
|
73
|
-
):
|
|
73
|
+
) -> None:
|
|
74
74
|
"""Initialize the renderer.
|
|
75
75
|
|
|
76
76
|
Args:
|
|
@@ -107,7 +107,10 @@ class RichStreamRenderer:
|
|
|
107
107
|
# Track per-step server start times for accurate elapsed labels
|
|
108
108
|
self._step_server_start_times: dict[str, float] = {}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
# Output formatting constants
|
|
111
|
+
self.OUTPUT_PREFIX: str = "**Output:**\n"
|
|
112
|
+
|
|
113
|
+
def on_start(self, meta: dict[str, Any]) -> None:
|
|
111
114
|
"""Handle renderer start event."""
|
|
112
115
|
if self.cfg.live:
|
|
113
116
|
# Defer creating Live to _ensure_live so tests and prod both work
|
|
@@ -118,40 +121,68 @@ class RichStreamRenderer:
|
|
|
118
121
|
self.stream_processor.streaming_started_at = self._started_at
|
|
119
122
|
|
|
120
123
|
# Print compact header and user request (parity with old renderer)
|
|
124
|
+
self._render_header(meta)
|
|
125
|
+
self._render_user_query(meta)
|
|
126
|
+
|
|
127
|
+
def _render_header(self, meta: dict[str, Any]) -> None:
|
|
128
|
+
"""Render the agent header with metadata."""
|
|
129
|
+
parts = self._build_header_parts(meta)
|
|
130
|
+
self.header_text = " ".join(parts)
|
|
131
|
+
|
|
132
|
+
if not self.header_text:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Use a rule-like header for readability with fallback
|
|
136
|
+
if not self._render_header_rule():
|
|
137
|
+
self._render_header_fallback()
|
|
138
|
+
|
|
139
|
+
def _build_header_parts(self, meta: dict[str, Any]) -> list[str]:
|
|
140
|
+
"""Build header text parts from metadata."""
|
|
141
|
+
parts: list[str] = ["🤖"]
|
|
142
|
+
agent_name = meta.get("agent_name", "agent")
|
|
143
|
+
if agent_name:
|
|
144
|
+
parts.append(agent_name)
|
|
145
|
+
|
|
146
|
+
model = meta.get("model", "")
|
|
147
|
+
if model:
|
|
148
|
+
parts.extend(["•", model])
|
|
149
|
+
|
|
150
|
+
run_id = meta.get("run_id", "")
|
|
151
|
+
if run_id:
|
|
152
|
+
parts.extend(["•", run_id])
|
|
153
|
+
|
|
154
|
+
return parts
|
|
155
|
+
|
|
156
|
+
def _render_header_rule(self) -> bool:
|
|
157
|
+
"""Render header as a rule. Returns True if successful."""
|
|
121
158
|
try:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
159
|
+
self.console.rule(self.header_text)
|
|
160
|
+
return True
|
|
161
|
+
except Exception: # pragma: no cover - defensive fallback
|
|
162
|
+
logger.exception("Failed to render header rule")
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
def _render_header_fallback(self) -> None:
|
|
166
|
+
"""Fallback header rendering."""
|
|
167
|
+
try:
|
|
168
|
+
self.console.print(self.header_text)
|
|
169
|
+
except Exception:
|
|
170
|
+
logger.exception("Failed to print header fallback")
|
|
171
|
+
|
|
172
|
+
def _render_user_query(self, meta: dict[str, Any]) -> None:
|
|
173
|
+
"""Render the user query panel."""
|
|
174
|
+
query = meta.get("input_message") or meta.get("query") or meta.get("message")
|
|
175
|
+
if not query:
|
|
176
|
+
return
|
|
139
177
|
|
|
140
|
-
|
|
141
|
-
|
|
178
|
+
self.console.print(
|
|
179
|
+
AIPPanel(
|
|
180
|
+
Markdown(f"**Query:** {query}"),
|
|
181
|
+
title="User Request",
|
|
182
|
+
border_style="yellow",
|
|
183
|
+
padding=(0, 1),
|
|
142
184
|
)
|
|
143
|
-
|
|
144
|
-
self.console.print(
|
|
145
|
-
AIPPanel(
|
|
146
|
-
Markdown(f"**Query:** {query}"),
|
|
147
|
-
title="User Request",
|
|
148
|
-
border_style="yellow",
|
|
149
|
-
padding=(0, 1),
|
|
150
|
-
)
|
|
151
|
-
)
|
|
152
|
-
except Exception:
|
|
153
|
-
# Non-fatal: header is nice-to-have
|
|
154
|
-
pass
|
|
185
|
+
)
|
|
155
186
|
|
|
156
187
|
def on_event(self, ev: dict[str, Any]) -> None:
|
|
157
188
|
"""Handle streaming events from the backend."""
|
|
@@ -209,9 +240,12 @@ class RichStreamRenderer:
|
|
|
209
240
|
# Note: Thinking gaps are primarily a visual aid. Keep minimal here.
|
|
210
241
|
|
|
211
242
|
# Extract tool information
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
243
|
+
(
|
|
244
|
+
tool_name,
|
|
245
|
+
tool_args,
|
|
246
|
+
tool_out,
|
|
247
|
+
tool_calls_info,
|
|
248
|
+
) = self.stream_processor.parse_tool_calls(ev)
|
|
215
249
|
|
|
216
250
|
# Track tools and sub-agents
|
|
217
251
|
self.stream_processor.track_tools_and_agents(
|
|
@@ -224,48 +258,57 @@ class RichStreamRenderer:
|
|
|
224
258
|
# Update live display
|
|
225
259
|
self._ensure_live()
|
|
226
260
|
|
|
227
|
-
def
|
|
261
|
+
def _finish_running_steps(self) -> None:
|
|
262
|
+
"""Mark any running steps as finished to avoid lingering spinners."""
|
|
263
|
+
for st in list(self.steps.by_id.values()):
|
|
264
|
+
if not is_step_finished(st):
|
|
265
|
+
st.finish(None)
|
|
266
|
+
|
|
267
|
+
def _finish_tool_panels(self) -> None:
|
|
268
|
+
"""Mark unfinished tool panels as finished."""
|
|
269
|
+
try:
|
|
270
|
+
items = list(self.tool_panels.items())
|
|
271
|
+
except Exception: # pragma: no cover - defensive guard
|
|
272
|
+
logger.exception("Failed to iterate tool panels during cleanup")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
for _sid, meta in items:
|
|
276
|
+
if meta.get("status") != "finished":
|
|
277
|
+
meta["status"] = "finished"
|
|
278
|
+
|
|
279
|
+
def _stop_live_display(self) -> None:
|
|
280
|
+
"""Stop live display and clean up."""
|
|
281
|
+
self._shutdown_live()
|
|
282
|
+
|
|
283
|
+
def _print_final_panel_if_needed(self) -> None:
|
|
284
|
+
"""Print final result panel if verbose mode and content available."""
|
|
285
|
+
if self.verbose and not self.state.printed_final_panel:
|
|
286
|
+
body = ("".join(self.state.buffer) or "").strip()
|
|
287
|
+
if body:
|
|
288
|
+
final_panel = create_final_panel(body, theme=self.cfg.theme)
|
|
289
|
+
self.console.print(final_panel)
|
|
290
|
+
self.state.printed_final_panel = True
|
|
291
|
+
|
|
292
|
+
def on_complete(self, _stats: RunStats) -> None:
|
|
228
293
|
"""Handle completion event."""
|
|
229
294
|
self.state.finalizing_ui = True
|
|
230
295
|
|
|
231
296
|
# Mark any running steps as finished to avoid lingering spinners
|
|
232
|
-
|
|
233
|
-
for st in list(self.steps.by_id.values()):
|
|
234
|
-
if not is_step_finished(st):
|
|
235
|
-
st.finish(None)
|
|
236
|
-
except Exception:
|
|
237
|
-
pass
|
|
297
|
+
self._finish_running_steps()
|
|
238
298
|
|
|
239
299
|
# Mark unfinished tool panels as finished
|
|
240
|
-
|
|
241
|
-
for _sid, meta in list(self.tool_panels.items()):
|
|
242
|
-
if meta.get("status") != "finished":
|
|
243
|
-
meta["status"] = "finished"
|
|
244
|
-
except Exception:
|
|
245
|
-
pass
|
|
300
|
+
self._finish_tool_panels()
|
|
246
301
|
|
|
247
302
|
# Final refresh
|
|
248
303
|
self._ensure_live()
|
|
249
304
|
|
|
250
305
|
# Stop live display
|
|
251
|
-
|
|
252
|
-
self.live.stop()
|
|
253
|
-
self.live = None
|
|
306
|
+
self._stop_live_display()
|
|
254
307
|
|
|
255
|
-
#
|
|
256
|
-
|
|
257
|
-
try:
|
|
258
|
-
if self.verbose and not self.state.printed_final_panel:
|
|
259
|
-
body = ("".join(self.state.buffer) or "").strip()
|
|
260
|
-
if body:
|
|
261
|
-
final_panel = create_final_panel(body, theme=self.cfg.theme)
|
|
262
|
-
self.console.print(final_panel)
|
|
263
|
-
self.state.printed_final_panel = True
|
|
264
|
-
except Exception:
|
|
265
|
-
# Non-fatal; renderer best-effort
|
|
266
|
-
pass
|
|
308
|
+
# Print final panel if needed
|
|
309
|
+
self._print_final_panel_if_needed()
|
|
267
310
|
|
|
268
|
-
def _ensure_live(self):
|
|
311
|
+
def _ensure_live(self) -> None:
|
|
269
312
|
"""Ensure live display is updated."""
|
|
270
313
|
# Lazily create Live if needed
|
|
271
314
|
if self.live is None and self.cfg.live:
|
|
@@ -292,14 +335,16 @@ class RichStreamRenderer:
|
|
|
292
335
|
panels.extend(self._render_tool_panels())
|
|
293
336
|
self.live.update(Group(*panels))
|
|
294
337
|
|
|
295
|
-
def _render_main_panel(self):
|
|
338
|
+
def _render_main_panel(self) -> Any:
|
|
296
339
|
"""Render the main content panel."""
|
|
297
340
|
body = "".join(self.state.buffer).strip()
|
|
298
341
|
# Dynamic title with spinner + elapsed/hints
|
|
299
342
|
title = self._format_enhanced_main_title()
|
|
300
343
|
return create_main_panel(body, title, self.cfg.theme)
|
|
301
344
|
|
|
302
|
-
def _maybe_insert_thinking_gap(
|
|
345
|
+
def _maybe_insert_thinking_gap(
|
|
346
|
+
self, task_id: str | None, context_id: str | None
|
|
347
|
+
) -> None:
|
|
303
348
|
"""Insert thinking gap if needed."""
|
|
304
349
|
# Implementation would track thinking states
|
|
305
350
|
pass
|
|
@@ -345,7 +390,7 @@ class RichStreamRenderer:
|
|
|
345
390
|
tool_name: str,
|
|
346
391
|
tool_args: Any,
|
|
347
392
|
_tool_sid: str,
|
|
348
|
-
):
|
|
393
|
+
) -> Step | None:
|
|
349
394
|
"""Start or get a step for a tool."""
|
|
350
395
|
if is_delegation_tool(tool_name):
|
|
351
396
|
st = self.steps.start_or_get(
|
|
@@ -373,8 +418,12 @@ class RichStreamRenderer:
|
|
|
373
418
|
return st
|
|
374
419
|
|
|
375
420
|
def _process_additional_tool_calls(
|
|
376
|
-
self,
|
|
377
|
-
|
|
421
|
+
self,
|
|
422
|
+
tool_calls_info: list[tuple[str, Any, Any]],
|
|
423
|
+
tool_name: str,
|
|
424
|
+
task_id: str,
|
|
425
|
+
context_id: str,
|
|
426
|
+
) -> None:
|
|
378
427
|
"""Process additional tool calls to avoid duplicates."""
|
|
379
428
|
for call_name, call_args, _ in tool_calls_info or []:
|
|
380
429
|
if call_name and call_name != tool_name:
|
|
@@ -421,55 +470,122 @@ class RichStreamRenderer:
|
|
|
421
470
|
|
|
422
471
|
return False, None, None
|
|
423
472
|
|
|
473
|
+
def _get_tool_session_id(
|
|
474
|
+
self, finished_tool_name: str, task_id: str, context_id: str
|
|
475
|
+
) -> str:
|
|
476
|
+
"""Generate tool session ID."""
|
|
477
|
+
return f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
478
|
+
|
|
479
|
+
def _calculate_tool_duration(self, meta: dict[str, Any]) -> float | None:
|
|
480
|
+
"""Calculate tool duration from metadata."""
|
|
481
|
+
server_now = self.stream_processor.server_elapsed_time
|
|
482
|
+
server_start = meta.get("server_started_at")
|
|
483
|
+
dur = None
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
if isinstance(server_now, (int, float)) and server_start is not None:
|
|
487
|
+
dur = max(0.0, float(server_now) - float(server_start))
|
|
488
|
+
else:
|
|
489
|
+
started_at = meta.get("started_at")
|
|
490
|
+
if started_at is not None:
|
|
491
|
+
started_at_float = float(started_at)
|
|
492
|
+
dur = max(0.0, float(monotonic()) - started_at_float)
|
|
493
|
+
except (TypeError, ValueError):
|
|
494
|
+
logger.exception("Failed to calculate tool duration")
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
return dur
|
|
498
|
+
|
|
499
|
+
def _update_tool_metadata(self, meta: dict[str, Any], dur: float | None) -> None:
|
|
500
|
+
"""Update tool metadata with duration information."""
|
|
501
|
+
if dur is not None:
|
|
502
|
+
meta["duration_seconds"] = dur
|
|
503
|
+
meta["server_finished_at"] = (
|
|
504
|
+
self.stream_processor.server_elapsed_time
|
|
505
|
+
if isinstance(self.stream_processor.server_elapsed_time, int | float)
|
|
506
|
+
else None
|
|
507
|
+
)
|
|
508
|
+
meta["finished_at"] = monotonic()
|
|
509
|
+
|
|
510
|
+
def _add_tool_output_to_panel(
|
|
511
|
+
self, meta: dict[str, Any], finished_tool_output: Any, finished_tool_name: str
|
|
512
|
+
) -> None:
|
|
513
|
+
"""Add tool output to panel metadata."""
|
|
514
|
+
if finished_tool_output is not None:
|
|
515
|
+
meta["chunks"].append(
|
|
516
|
+
self._format_output_block(finished_tool_output, finished_tool_name)
|
|
517
|
+
)
|
|
518
|
+
meta["output"] = finished_tool_output
|
|
519
|
+
|
|
520
|
+
def _mark_panel_as_finished(self, meta: dict[str, Any], tool_sid: str) -> None:
|
|
521
|
+
"""Mark panel as finished and ensure visibility."""
|
|
522
|
+
if meta.get("status") != "finished":
|
|
523
|
+
meta["status"] = "finished"
|
|
524
|
+
|
|
525
|
+
dur = self._calculate_tool_duration(meta)
|
|
526
|
+
self._update_tool_metadata(meta, dur)
|
|
527
|
+
|
|
528
|
+
# Ensure this finished panel is visible in this frame
|
|
529
|
+
self.stream_processor.current_event_finished_panels.add(tool_sid)
|
|
530
|
+
|
|
424
531
|
def _finish_tool_panel(
|
|
425
532
|
self,
|
|
426
533
|
finished_tool_name: str,
|
|
427
534
|
finished_tool_output: Any,
|
|
428
535
|
task_id: str,
|
|
429
536
|
context_id: str,
|
|
430
|
-
):
|
|
537
|
+
) -> None:
|
|
431
538
|
"""Finish a tool panel and update its status."""
|
|
432
|
-
tool_sid =
|
|
433
|
-
if tool_sid in self.tool_panels:
|
|
434
|
-
|
|
435
|
-
prev_status = meta.get("status")
|
|
539
|
+
tool_sid = self._get_tool_session_id(finished_tool_name, task_id, context_id)
|
|
540
|
+
if tool_sid not in self.tool_panels:
|
|
541
|
+
return
|
|
436
542
|
|
|
437
|
-
|
|
438
|
-
|
|
543
|
+
meta = self.tool_panels[tool_sid]
|
|
544
|
+
self._mark_panel_as_finished(meta, tool_sid)
|
|
545
|
+
self._add_tool_output_to_panel(meta, finished_tool_output, finished_tool_name)
|
|
439
546
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if isinstance(server_now, int | float) and isinstance(
|
|
447
|
-
server_start, int | float
|
|
448
|
-
):
|
|
449
|
-
dur = max(0.0, float(server_now) - float(server_start))
|
|
450
|
-
elif meta.get("started_at") is not None:
|
|
451
|
-
dur = max(0.0, float(monotonic() - meta.get("started_at")))
|
|
452
|
-
|
|
453
|
-
if dur is not None:
|
|
454
|
-
meta["duration_seconds"] = dur
|
|
455
|
-
meta["server_finished_at"] = (
|
|
456
|
-
server_now if isinstance(server_now, int | float) else None
|
|
457
|
-
)
|
|
458
|
-
meta["finished_at"] = monotonic()
|
|
459
|
-
except Exception:
|
|
460
|
-
pass
|
|
461
|
-
|
|
462
|
-
# Add output to panel
|
|
463
|
-
if finished_tool_output is not None:
|
|
464
|
-
meta["chunks"].append(
|
|
465
|
-
self._format_output_block(
|
|
466
|
-
finished_tool_output, finished_tool_name
|
|
467
|
-
)
|
|
468
|
-
)
|
|
469
|
-
meta["output"] = finished_tool_output
|
|
547
|
+
def _get_step_duration(
|
|
548
|
+
self, finished_tool_name: str, task_id: str, context_id: str
|
|
549
|
+
) -> float | None:
|
|
550
|
+
"""Get step duration from tool panels."""
|
|
551
|
+
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
552
|
+
return self.tool_panels.get(tool_sid, {}).get("duration_seconds")
|
|
470
553
|
|
|
471
|
-
|
|
472
|
-
|
|
554
|
+
def _finish_delegation_step(
|
|
555
|
+
self,
|
|
556
|
+
finished_tool_name: str,
|
|
557
|
+
finished_tool_output: Any,
|
|
558
|
+
task_id: str,
|
|
559
|
+
context_id: str,
|
|
560
|
+
step_duration: float | None,
|
|
561
|
+
) -> None:
|
|
562
|
+
"""Finish a delegation step."""
|
|
563
|
+
self.steps.finish(
|
|
564
|
+
task_id=task_id,
|
|
565
|
+
context_id=context_id,
|
|
566
|
+
kind="delegate",
|
|
567
|
+
name=finished_tool_name,
|
|
568
|
+
output=finished_tool_output,
|
|
569
|
+
duration_raw=step_duration,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
def _finish_tool_step_type(
|
|
573
|
+
self,
|
|
574
|
+
finished_tool_name: str,
|
|
575
|
+
finished_tool_output: Any,
|
|
576
|
+
task_id: str,
|
|
577
|
+
context_id: str,
|
|
578
|
+
step_duration: float | None,
|
|
579
|
+
) -> None:
|
|
580
|
+
"""Finish a regular tool step."""
|
|
581
|
+
self.steps.finish(
|
|
582
|
+
task_id=task_id,
|
|
583
|
+
context_id=context_id,
|
|
584
|
+
kind="tool",
|
|
585
|
+
name=finished_tool_name,
|
|
586
|
+
output=finished_tool_output,
|
|
587
|
+
duration_raw=step_duration,
|
|
588
|
+
)
|
|
473
589
|
|
|
474
590
|
def _finish_tool_step(
|
|
475
591
|
self,
|
|
@@ -477,88 +593,113 @@ class RichStreamRenderer:
|
|
|
477
593
|
finished_tool_output: Any,
|
|
478
594
|
task_id: str,
|
|
479
595
|
context_id: str,
|
|
480
|
-
):
|
|
596
|
+
) -> None:
|
|
481
597
|
"""Finish the corresponding step for a completed tool."""
|
|
482
|
-
|
|
483
|
-
step_duration = None
|
|
484
|
-
|
|
485
|
-
try:
|
|
486
|
-
step_duration = self.tool_panels.get(tool_sid, {}).get("duration_seconds")
|
|
487
|
-
except Exception:
|
|
488
|
-
step_duration = None
|
|
598
|
+
step_duration = self._get_step_duration(finished_tool_name, task_id, context_id)
|
|
489
599
|
|
|
490
600
|
if is_delegation_tool(finished_tool_name):
|
|
491
|
-
self.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
duration_raw=step_duration,
|
|
601
|
+
self._finish_delegation_step(
|
|
602
|
+
finished_tool_name,
|
|
603
|
+
finished_tool_output,
|
|
604
|
+
task_id,
|
|
605
|
+
context_id,
|
|
606
|
+
step_duration,
|
|
498
607
|
)
|
|
499
608
|
else:
|
|
500
|
-
self.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
duration_raw=step_duration,
|
|
609
|
+
self._finish_tool_step_type(
|
|
610
|
+
finished_tool_name,
|
|
611
|
+
finished_tool_output,
|
|
612
|
+
task_id,
|
|
613
|
+
context_id,
|
|
614
|
+
step_duration,
|
|
507
615
|
)
|
|
508
616
|
|
|
617
|
+
def _should_create_snapshot(self, tool_sid: str) -> bool:
|
|
618
|
+
"""Check if a snapshot should be created."""
|
|
619
|
+
return self.cfg.append_finished_snapshots and not self.tool_panels.get(
|
|
620
|
+
tool_sid, {}
|
|
621
|
+
).get("snapshot_printed")
|
|
622
|
+
|
|
623
|
+
def _get_snapshot_title(self, meta: dict[str, Any], finished_tool_name: str) -> str:
|
|
624
|
+
"""Get the title for the snapshot."""
|
|
625
|
+
adjusted_title = meta.get("title") or finished_tool_name
|
|
626
|
+
|
|
627
|
+
# Add elapsed time to title
|
|
628
|
+
dur = meta.get("duration_seconds")
|
|
629
|
+
if isinstance(dur, int | float):
|
|
630
|
+
elapsed_str = self._format_snapshot_duration(dur)
|
|
631
|
+
adjusted_title = f"{adjusted_title} · {elapsed_str}"
|
|
632
|
+
|
|
633
|
+
return adjusted_title
|
|
634
|
+
|
|
635
|
+
def _format_snapshot_duration(self, dur: int | float) -> str:
|
|
636
|
+
"""Format duration for snapshot title."""
|
|
637
|
+
try:
|
|
638
|
+
# Handle invalid types
|
|
639
|
+
if not isinstance(dur, (int, float)):
|
|
640
|
+
return "<1ms"
|
|
641
|
+
|
|
642
|
+
if dur >= 1:
|
|
643
|
+
return f"{dur:.2f}s"
|
|
644
|
+
elif int(dur * 1000) > 0:
|
|
645
|
+
return f"{int(dur * 1000)}ms"
|
|
646
|
+
else:
|
|
647
|
+
return "<1ms"
|
|
648
|
+
except (TypeError, ValueError, OverflowError):
|
|
649
|
+
return "<1ms"
|
|
650
|
+
|
|
651
|
+
def _clamp_snapshot_body(self, body_text: str) -> str:
|
|
652
|
+
"""Clamp snapshot body to configured limits."""
|
|
653
|
+
max_lines = int(self.cfg.snapshot_max_lines or 0) or 60
|
|
654
|
+
lines = body_text.splitlines()
|
|
655
|
+
if len(lines) > max_lines:
|
|
656
|
+
lines = lines[:max_lines] + ["… (truncated)"]
|
|
657
|
+
body_text = "\n".join(lines)
|
|
658
|
+
|
|
659
|
+
max_chars = int(self.cfg.snapshot_max_chars or 0) or 4000
|
|
660
|
+
if len(body_text) > max_chars:
|
|
661
|
+
body_text = body_text[: max_chars - 12] + "\n… (truncated)"
|
|
662
|
+
|
|
663
|
+
return body_text
|
|
664
|
+
|
|
665
|
+
def _create_snapshot_panel(
|
|
666
|
+
self, adjusted_title: str, body_text: str, finished_tool_name: str
|
|
667
|
+
) -> Any:
|
|
668
|
+
"""Create the snapshot panel."""
|
|
669
|
+
return create_tool_panel(
|
|
670
|
+
title=adjusted_title,
|
|
671
|
+
content=body_text or "(no output)",
|
|
672
|
+
status="finished",
|
|
673
|
+
theme=self.cfg.theme,
|
|
674
|
+
is_delegation=is_delegation_tool(finished_tool_name),
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
def _print_and_mark_snapshot(self, tool_sid: str, snapshot_panel: Any) -> None:
|
|
678
|
+
"""Print snapshot and mark as printed."""
|
|
679
|
+
self.console.print(snapshot_panel)
|
|
680
|
+
self.tool_panels[tool_sid]["snapshot_printed"] = True
|
|
681
|
+
|
|
509
682
|
def _create_tool_snapshot(
|
|
510
683
|
self, finished_tool_name: str, task_id: str, context_id: str
|
|
511
|
-
):
|
|
684
|
+
) -> None:
|
|
512
685
|
"""Create and print a snapshot for a finished tool."""
|
|
513
686
|
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
514
687
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
self.cfg.append_finished_snapshots
|
|
518
|
-
and not self.tool_panels.get(tool_sid, {}).get("snapshot_printed")
|
|
519
|
-
):
|
|
520
|
-
return
|
|
521
|
-
|
|
522
|
-
meta = self.tool_panels[tool_sid]
|
|
523
|
-
adjusted_title = meta.get("title") or finished_tool_name
|
|
524
|
-
|
|
525
|
-
# Add elapsed time to title
|
|
526
|
-
dur = meta.get("duration_seconds")
|
|
527
|
-
if isinstance(dur, int | float):
|
|
528
|
-
elapsed_str = (
|
|
529
|
-
f"{dur:.2f}s"
|
|
530
|
-
if dur >= 1
|
|
531
|
-
else (f"{int(dur * 1000)}ms" if int(dur * 1000) > 0 else "<1ms")
|
|
532
|
-
)
|
|
533
|
-
adjusted_title = f"{adjusted_title} · {elapsed_str}"
|
|
534
|
-
|
|
535
|
-
# Compose body from chunks and clamp
|
|
536
|
-
body_text = "".join(meta.get("chunks") or [])
|
|
537
|
-
max_lines = int(self.cfg.snapshot_max_lines or 0) or 60
|
|
538
|
-
lines = body_text.splitlines()
|
|
539
|
-
if len(lines) > max_lines:
|
|
540
|
-
lines = lines[:max_lines] + ["… (truncated)"]
|
|
541
|
-
body_text = "\n".join(lines)
|
|
542
|
-
|
|
543
|
-
max_chars = int(self.cfg.snapshot_max_chars or 0) or 4000
|
|
544
|
-
if len(body_text) > max_chars:
|
|
545
|
-
body_text = body_text[: max_chars - 12] + "\n… (truncated)"
|
|
546
|
-
|
|
547
|
-
snapshot_panel = create_tool_panel(
|
|
548
|
-
title=adjusted_title,
|
|
549
|
-
content=body_text or "(no output)",
|
|
550
|
-
status="finished",
|
|
551
|
-
theme=self.cfg.theme,
|
|
552
|
-
is_delegation=is_delegation_tool(finished_tool_name),
|
|
553
|
-
)
|
|
688
|
+
if not self._should_create_snapshot(tool_sid):
|
|
689
|
+
return
|
|
554
690
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
# Guard so we don't print snapshot twice
|
|
558
|
-
self.tool_panels[tool_sid]["snapshot_printed"] = True
|
|
691
|
+
meta = self.tool_panels[tool_sid]
|
|
692
|
+
adjusted_title = self._get_snapshot_title(meta, finished_tool_name)
|
|
559
693
|
|
|
560
|
-
|
|
561
|
-
|
|
694
|
+
# Compose body from chunks and clamp
|
|
695
|
+
body_text = "".join(meta.get("chunks") or [])
|
|
696
|
+
body_text = self._clamp_snapshot_body(body_text)
|
|
697
|
+
|
|
698
|
+
snapshot_panel = self._create_snapshot_panel(
|
|
699
|
+
adjusted_title, body_text, finished_tool_name
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
self._print_and_mark_snapshot(tool_sid, snapshot_panel)
|
|
562
703
|
|
|
563
704
|
def _handle_agent_step(
|
|
564
705
|
self,
|
|
@@ -566,8 +707,8 @@ class RichStreamRenderer:
|
|
|
566
707
|
tool_name: str | None,
|
|
567
708
|
tool_args: Any,
|
|
568
709
|
_tool_out: Any,
|
|
569
|
-
tool_calls_info: list,
|
|
570
|
-
):
|
|
710
|
+
tool_calls_info: list[tuple[str, Any, Any]],
|
|
711
|
+
) -> None:
|
|
571
712
|
"""Handle agent step event."""
|
|
572
713
|
metadata = event.get("metadata", {})
|
|
573
714
|
task_id = event.get("task_id")
|
|
@@ -587,9 +728,11 @@ class RichStreamRenderer:
|
|
|
587
728
|
)
|
|
588
729
|
|
|
589
730
|
# Check for tool completion
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
731
|
+
(
|
|
732
|
+
is_tool_finished,
|
|
733
|
+
finished_tool_name,
|
|
734
|
+
finished_tool_output,
|
|
735
|
+
) = self._detect_tool_completion(metadata, content)
|
|
593
736
|
|
|
594
737
|
if is_tool_finished and finished_tool_name:
|
|
595
738
|
self._finish_tool_panel(
|
|
@@ -614,21 +757,30 @@ class RichStreamRenderer:
|
|
|
614
757
|
|
|
615
758
|
def close(self) -> None:
|
|
616
759
|
"""Gracefully stop any live rendering and release resources."""
|
|
760
|
+
self._shutdown_live()
|
|
761
|
+
|
|
762
|
+
def __del__(self) -> None:
|
|
763
|
+
# Destructors must never raise
|
|
617
764
|
try:
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
self.live.stop()
|
|
621
|
-
finally:
|
|
622
|
-
self.live = None
|
|
623
|
-
except Exception:
|
|
765
|
+
self._shutdown_live(reset_attr=False)
|
|
766
|
+
except Exception: # pragma: no cover - destructor safety net
|
|
624
767
|
pass
|
|
625
768
|
|
|
626
|
-
def
|
|
769
|
+
def _shutdown_live(self, reset_attr: bool = True) -> None:
|
|
770
|
+
"""Stop the live renderer without letting exceptions escape."""
|
|
771
|
+
live = getattr(self, "live", None)
|
|
772
|
+
if not live:
|
|
773
|
+
if reset_attr and not hasattr(self, "live"):
|
|
774
|
+
self.live = None
|
|
775
|
+
return
|
|
776
|
+
|
|
627
777
|
try:
|
|
628
|
-
|
|
629
|
-
self.live.stop()
|
|
778
|
+
live.stop()
|
|
630
779
|
except Exception:
|
|
631
|
-
|
|
780
|
+
logger.exception("Failed to stop live display")
|
|
781
|
+
finally:
|
|
782
|
+
if reset_attr:
|
|
783
|
+
self.live = None
|
|
632
784
|
|
|
633
785
|
def _get_analysis_progress_info(self) -> dict[str, Any]:
|
|
634
786
|
total_steps = len(self.steps.order)
|
|
@@ -643,15 +795,9 @@ class RichStreamRenderer:
|
|
|
643
795
|
# Prefer server elapsed time when available
|
|
644
796
|
elapsed = 0.0
|
|
645
797
|
if isinstance(self.stream_processor.server_elapsed_time, int | float):
|
|
646
|
-
|
|
647
|
-
elapsed = float(self.stream_processor.server_elapsed_time)
|
|
648
|
-
except Exception:
|
|
649
|
-
elapsed = 0.0
|
|
798
|
+
elapsed = float(self.stream_processor.server_elapsed_time)
|
|
650
799
|
elif self._started_at is not None:
|
|
651
|
-
|
|
652
|
-
elapsed = monotonic() - self._started_at
|
|
653
|
-
except Exception:
|
|
654
|
-
elapsed = 0.0
|
|
800
|
+
elapsed = monotonic() - self._started_at
|
|
655
801
|
progress_percent = (
|
|
656
802
|
int((completed_steps / total_steps) * 100) if total_steps else 0
|
|
657
803
|
)
|
|
@@ -705,13 +851,13 @@ class RichStreamRenderer:
|
|
|
705
851
|
return "🧠"
|
|
706
852
|
return ""
|
|
707
853
|
|
|
708
|
-
def _format_step_status(self, step) -> str:
|
|
854
|
+
def _format_step_status(self, step: Step) -> str:
|
|
709
855
|
"""Format step status with elapsed time or duration."""
|
|
710
856
|
if is_step_finished(step):
|
|
711
857
|
if step.duration_ms is None:
|
|
712
858
|
return "[<1ms]"
|
|
713
859
|
elif step.duration_ms >= 1000:
|
|
714
|
-
return f"[{step.duration_ms/1000:.2f}s]"
|
|
860
|
+
return f"[{step.duration_ms / 1000:.2f}s]"
|
|
715
861
|
elif step.duration_ms > 0:
|
|
716
862
|
return f"[{step.duration_ms}ms]"
|
|
717
863
|
return "[<1ms]"
|
|
@@ -723,7 +869,7 @@ class RichStreamRenderer:
|
|
|
723
869
|
ms = int(elapsed * 1000)
|
|
724
870
|
return f"[{ms}ms]" if ms > 0 else "[<1ms]"
|
|
725
871
|
|
|
726
|
-
def _calculate_step_elapsed_time(self, step) -> float:
|
|
872
|
+
def _calculate_step_elapsed_time(self, step: Step) -> float:
|
|
727
873
|
"""Calculate elapsed time for a running step."""
|
|
728
874
|
server_elapsed = self.stream_processor.server_elapsed_time
|
|
729
875
|
server_start = self._step_server_start_times.get(step.step_id)
|
|
@@ -738,7 +884,7 @@ class RichStreamRenderer:
|
|
|
738
884
|
except Exception:
|
|
739
885
|
return 0.0
|
|
740
886
|
|
|
741
|
-
def _get_step_display_name(self, step) -> str:
|
|
887
|
+
def _get_step_display_name(self, step: Step) -> str:
|
|
742
888
|
"""Get display name for a step."""
|
|
743
889
|
if step.name and step.name != "step":
|
|
744
890
|
return step.name
|
|
@@ -779,142 +925,187 @@ class RichStreamRenderer:
|
|
|
779
925
|
|
|
780
926
|
return Text("\n".join(lines), style="dim")
|
|
781
927
|
|
|
928
|
+
def _should_skip_finished_panel(self, sid: str, status: str) -> bool:
|
|
929
|
+
"""Check if a finished panel should be skipped."""
|
|
930
|
+
if status != "finished":
|
|
931
|
+
return False
|
|
932
|
+
|
|
933
|
+
if getattr(self.cfg, "append_finished_snapshots", False):
|
|
934
|
+
return True
|
|
935
|
+
|
|
936
|
+
return (
|
|
937
|
+
not self.state.finalizing_ui
|
|
938
|
+
and sid not in self.stream_processor.current_event_finished_panels
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
def _calculate_elapsed_time(self, meta: dict[str, Any]) -> str:
|
|
942
|
+
"""Calculate elapsed time string for running tools."""
|
|
943
|
+
server_elapsed = self.stream_processor.server_elapsed_time
|
|
944
|
+
server_start = meta.get("server_started_at")
|
|
945
|
+
|
|
946
|
+
if isinstance(server_elapsed, int | float) and isinstance(
|
|
947
|
+
server_start, int | float
|
|
948
|
+
):
|
|
949
|
+
elapsed = max(0.0, float(server_elapsed) - float(server_start))
|
|
950
|
+
else:
|
|
951
|
+
elapsed = max(0.0, monotonic() - (meta.get("started_at") or 0.0))
|
|
952
|
+
|
|
953
|
+
return self._format_elapsed_time(elapsed)
|
|
954
|
+
|
|
955
|
+
def _format_elapsed_time(self, elapsed: float) -> str:
|
|
956
|
+
"""Format elapsed time as a readable string."""
|
|
957
|
+
if elapsed >= 1:
|
|
958
|
+
return f"{elapsed:.2f}s"
|
|
959
|
+
elif int(elapsed * 1000) > 0:
|
|
960
|
+
return f"{int(elapsed * 1000)}ms"
|
|
961
|
+
else:
|
|
962
|
+
return "<1ms"
|
|
963
|
+
|
|
964
|
+
def _calculate_finished_duration(self, meta: dict[str, Any]) -> str | None:
|
|
965
|
+
"""Calculate duration string for finished tools."""
|
|
966
|
+
dur = meta.get("duration_seconds")
|
|
967
|
+
if isinstance(dur, int | float):
|
|
968
|
+
return self._format_elapsed_time(dur)
|
|
969
|
+
|
|
970
|
+
try:
|
|
971
|
+
server_now = self.stream_processor.server_elapsed_time
|
|
972
|
+
server_start = meta.get("server_started_at")
|
|
973
|
+
if isinstance(server_now, int | float) and isinstance(
|
|
974
|
+
server_start, int | float
|
|
975
|
+
):
|
|
976
|
+
dur = max(0.0, float(server_now) - float(server_start))
|
|
977
|
+
elif meta.get("started_at") is not None:
|
|
978
|
+
dur = max(0.0, float(monotonic() - meta.get("started_at")))
|
|
979
|
+
except Exception:
|
|
980
|
+
dur = None
|
|
981
|
+
|
|
982
|
+
return self._format_elapsed_time(dur) if isinstance(dur, int | float) else None
|
|
983
|
+
|
|
984
|
+
def _process_running_tool_panel(
|
|
985
|
+
self, title: str, meta: dict[str, Any], body: str
|
|
986
|
+
) -> tuple[str, str]:
|
|
987
|
+
"""Process a running tool panel."""
|
|
988
|
+
elapsed_str = self._calculate_elapsed_time(meta)
|
|
989
|
+
adjusted_title = f"{title} · {elapsed_str}"
|
|
990
|
+
chip = f"⏱ {elapsed_str}"
|
|
991
|
+
|
|
992
|
+
if not body:
|
|
993
|
+
body = chip
|
|
994
|
+
else:
|
|
995
|
+
body = f"{body}\n\n{chip}"
|
|
996
|
+
|
|
997
|
+
return adjusted_title, body
|
|
998
|
+
|
|
999
|
+
def _process_finished_tool_panel(self, title: str, meta: dict[str, Any]) -> str:
|
|
1000
|
+
"""Process a finished tool panel."""
|
|
1001
|
+
duration_str = self._calculate_finished_duration(meta)
|
|
1002
|
+
return f"{title} · {duration_str}" if duration_str else title
|
|
1003
|
+
|
|
1004
|
+
def _create_tool_panel_for_session(
|
|
1005
|
+
self, sid: str, meta: dict[str, Any]
|
|
1006
|
+
) -> AIPPanel | None:
|
|
1007
|
+
"""Create a single tool panel for the session."""
|
|
1008
|
+
title = meta.get("title") or "Tool"
|
|
1009
|
+
status = meta.get("status") or "running"
|
|
1010
|
+
chunks = meta.get("chunks") or []
|
|
1011
|
+
is_delegation = bool(meta.get("is_delegation"))
|
|
1012
|
+
|
|
1013
|
+
if self._should_skip_finished_panel(sid, status):
|
|
1014
|
+
return None
|
|
1015
|
+
|
|
1016
|
+
body = "".join(chunks)
|
|
1017
|
+
adjusted_title = title
|
|
1018
|
+
|
|
1019
|
+
if status == "running":
|
|
1020
|
+
adjusted_title, body = self._process_running_tool_panel(title, meta, body)
|
|
1021
|
+
elif status == "finished":
|
|
1022
|
+
adjusted_title = self._process_finished_tool_panel(title, meta)
|
|
1023
|
+
|
|
1024
|
+
return create_tool_panel(
|
|
1025
|
+
title=adjusted_title,
|
|
1026
|
+
content=body or "Processing...",
|
|
1027
|
+
status=status,
|
|
1028
|
+
theme=self.cfg.theme,
|
|
1029
|
+
is_delegation=is_delegation,
|
|
1030
|
+
)
|
|
1031
|
+
|
|
782
1032
|
def _render_tool_panels(self) -> list[AIPPanel]:
|
|
783
1033
|
"""Render tool execution output panels."""
|
|
784
1034
|
panels: list[AIPPanel] = []
|
|
785
1035
|
for sid in self.tool_order:
|
|
786
1036
|
meta = self.tool_panels.get(sid) or {}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
is_delegation = bool(meta.get("is_delegation"))
|
|
791
|
-
|
|
792
|
-
# Finished panels visibility rules
|
|
793
|
-
if status == "finished":
|
|
794
|
-
if getattr(self.cfg, "append_finished_snapshots", False):
|
|
795
|
-
# When snapshots are enabled, don't also render finished panels in the live area
|
|
796
|
-
# (prevents duplicates both mid-run and at the end)
|
|
797
|
-
continue
|
|
798
|
-
if (
|
|
799
|
-
not self.state.finalizing_ui
|
|
800
|
-
and sid not in self.stream_processor.current_event_finished_panels
|
|
801
|
-
):
|
|
802
|
-
continue
|
|
803
|
-
|
|
804
|
-
body = "".join(chunks)
|
|
805
|
-
adjusted_title = title
|
|
806
|
-
if status == "running":
|
|
807
|
-
# Prefer server-based elapsed from when this tool panel started
|
|
808
|
-
server_elapsed = self.stream_processor.server_elapsed_time
|
|
809
|
-
server_start = meta.get("server_started_at")
|
|
810
|
-
if isinstance(server_elapsed, int | float) and isinstance(
|
|
811
|
-
server_start, int | float
|
|
812
|
-
):
|
|
813
|
-
elapsed = max(0.0, float(server_elapsed) - float(server_start))
|
|
814
|
-
else:
|
|
815
|
-
try:
|
|
816
|
-
elapsed = max(
|
|
817
|
-
0.0, monotonic() - (meta.get("started_at") or 0.0)
|
|
818
|
-
)
|
|
819
|
-
except Exception:
|
|
820
|
-
elapsed = 0.0
|
|
821
|
-
elapsed_str = (
|
|
822
|
-
f"{elapsed:.2f}s"
|
|
823
|
-
if elapsed >= 1
|
|
824
|
-
else (
|
|
825
|
-
f"{int(elapsed * 1000)}ms"
|
|
826
|
-
if int(elapsed * 1000) > 0
|
|
827
|
-
else "<1ms"
|
|
828
|
-
)
|
|
829
|
-
)
|
|
830
|
-
# Add a small elapsed hint to the title and panel body (standardized)
|
|
831
|
-
adjusted_title = f"{title} · {elapsed_str}"
|
|
832
|
-
chip = f"⏱ {elapsed_str}"
|
|
833
|
-
if not body:
|
|
834
|
-
body = chip
|
|
835
|
-
else:
|
|
836
|
-
body = f"{body}\n\n{chip}"
|
|
837
|
-
elif status == "finished":
|
|
838
|
-
# Use stored duration if present; otherwise try to compute once more
|
|
839
|
-
dur = meta.get("duration_seconds")
|
|
840
|
-
if not isinstance(dur, int | float):
|
|
841
|
-
try:
|
|
842
|
-
server_now = self.stream_processor.server_elapsed_time
|
|
843
|
-
server_start = meta.get("server_started_at")
|
|
844
|
-
if isinstance(server_now, int | float) and isinstance(
|
|
845
|
-
server_start, int | float
|
|
846
|
-
):
|
|
847
|
-
dur = max(0.0, float(server_now) - float(server_start))
|
|
848
|
-
elif meta.get("started_at") is not None:
|
|
849
|
-
dur = max(0.0, float(monotonic() - meta.get("started_at")))
|
|
850
|
-
except Exception:
|
|
851
|
-
dur = None
|
|
852
|
-
if isinstance(dur, int | float):
|
|
853
|
-
elapsed_str = (
|
|
854
|
-
f"{dur:.2f}s"
|
|
855
|
-
if dur >= 1
|
|
856
|
-
else (f"{int(dur * 1000)}ms" if int(dur * 1000) > 0 else "<1ms")
|
|
857
|
-
)
|
|
858
|
-
adjusted_title = f"{title} · {elapsed_str}"
|
|
1037
|
+
panel = self._create_tool_panel_for_session(sid, meta)
|
|
1038
|
+
if panel:
|
|
1039
|
+
panels.append(panel)
|
|
859
1040
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1041
|
+
return panels
|
|
1042
|
+
|
|
1043
|
+
def _format_dict_or_list_output(self, output_value: dict | list) -> str:
|
|
1044
|
+
"""Format dict/list output as pretty JSON."""
|
|
1045
|
+
try:
|
|
1046
|
+
return (
|
|
1047
|
+
self.OUTPUT_PREFIX
|
|
1048
|
+
+ "```json\n"
|
|
1049
|
+
+ json.dumps(output_value, indent=2)
|
|
1050
|
+
+ "\n```\n"
|
|
868
1051
|
)
|
|
1052
|
+
except Exception:
|
|
1053
|
+
return self.OUTPUT_PREFIX + str(output_value) + "\n"
|
|
1054
|
+
|
|
1055
|
+
def _clean_sub_agent_prefix(self, output: str, tool_name: str | None) -> str:
|
|
1056
|
+
"""Clean sub-agent name prefix from output."""
|
|
1057
|
+
if not (tool_name and is_delegation_tool(tool_name)):
|
|
1058
|
+
return output
|
|
1059
|
+
|
|
1060
|
+
sub = tool_name
|
|
1061
|
+
if tool_name.startswith("delegate_to_"):
|
|
1062
|
+
sub = tool_name.replace("delegate_to_", "")
|
|
1063
|
+
elif tool_name.startswith("delegate_"):
|
|
1064
|
+
sub = tool_name.replace("delegate_", "")
|
|
1065
|
+
prefix = f"[{sub}]"
|
|
1066
|
+
if output.startswith(prefix):
|
|
1067
|
+
return output[len(prefix) :].lstrip()
|
|
1068
|
+
|
|
1069
|
+
return output
|
|
1070
|
+
|
|
1071
|
+
def _format_json_string_output(self, output: str) -> str:
|
|
1072
|
+
"""Format string that looks like JSON."""
|
|
1073
|
+
try:
|
|
1074
|
+
parsed = json.loads(output)
|
|
1075
|
+
return (
|
|
1076
|
+
self.OUTPUT_PREFIX
|
|
1077
|
+
+ "```json\n"
|
|
1078
|
+
+ json.dumps(parsed, indent=2)
|
|
1079
|
+
+ "\n```\n"
|
|
1080
|
+
)
|
|
1081
|
+
except Exception:
|
|
1082
|
+
return self.OUTPUT_PREFIX + output + "\n"
|
|
869
1083
|
|
|
870
|
-
|
|
1084
|
+
def _format_string_output(self, output: str, tool_name: str | None) -> str:
|
|
1085
|
+
"""Format string output with optional prefix cleaning."""
|
|
1086
|
+
s = output.strip()
|
|
1087
|
+
s = self._clean_sub_agent_prefix(s, tool_name)
|
|
871
1088
|
|
|
872
|
-
|
|
873
|
-
""
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
return (
|
|
878
|
-
"**Output:**\n```json\n"
|
|
879
|
-
+ json.dumps(output_value, indent=2)
|
|
880
|
-
+ "\n```\n"
|
|
881
|
-
)
|
|
882
|
-
except Exception:
|
|
883
|
-
pass
|
|
1089
|
+
# If looks like JSON, pretty print it
|
|
1090
|
+
if (s.startswith("{") and s.endswith("}")) or (
|
|
1091
|
+
s.startswith("[") and s.endswith("]")
|
|
1092
|
+
):
|
|
1093
|
+
return self._format_json_string_output(s)
|
|
884
1094
|
|
|
885
|
-
|
|
886
|
-
s = output_value.strip()
|
|
887
|
-
# Clean sub-agent name prefix like "[research_compiler_agent_testing] "
|
|
888
|
-
try:
|
|
889
|
-
if tool_name and is_delegation_tool(tool_name):
|
|
890
|
-
sub = tool_name
|
|
891
|
-
if tool_name.startswith("delegate_to_"):
|
|
892
|
-
sub = tool_name.replace("delegate_to_", "")
|
|
893
|
-
elif tool_name.startswith("delegate_"):
|
|
894
|
-
sub = tool_name.replace("delegate_", "")
|
|
895
|
-
prefix = f"[{sub}]"
|
|
896
|
-
if s.startswith(prefix):
|
|
897
|
-
s = s[len(prefix) :].lstrip()
|
|
898
|
-
except Exception:
|
|
899
|
-
pass
|
|
900
|
-
# If looks like JSON, pretty print it
|
|
901
|
-
if (s.startswith("{") and s.endswith("}")) or (
|
|
902
|
-
s.startswith("[") and s.endswith("]")
|
|
903
|
-
):
|
|
904
|
-
try:
|
|
905
|
-
parsed = json.loads(s)
|
|
906
|
-
return (
|
|
907
|
-
"**Output:**\n```json\n"
|
|
908
|
-
+ json.dumps(parsed, indent=2)
|
|
909
|
-
+ "\n```\n"
|
|
910
|
-
)
|
|
911
|
-
except Exception:
|
|
912
|
-
pass
|
|
913
|
-
return "**Output:**\n" + s + "\n"
|
|
1095
|
+
return self.OUTPUT_PREFIX + s + "\n"
|
|
914
1096
|
|
|
1097
|
+
def _format_other_output(self, output_value: Any) -> str:
|
|
1098
|
+
"""Format other types of output."""
|
|
915
1099
|
try:
|
|
916
|
-
return
|
|
1100
|
+
return self.OUTPUT_PREFIX + json.dumps(output_value, indent=2) + "\n"
|
|
917
1101
|
except Exception:
|
|
918
|
-
return
|
|
1102
|
+
return self.OUTPUT_PREFIX + str(output_value) + "\n"
|
|
919
1103
|
|
|
920
|
-
|
|
1104
|
+
def _format_output_block(self, output_value: Any, tool_name: str | None) -> str:
|
|
1105
|
+
"""Format an output value for panel display."""
|
|
1106
|
+
if isinstance(output_value, dict | list):
|
|
1107
|
+
return self._format_dict_or_list_output(output_value)
|
|
1108
|
+
elif isinstance(output_value, str):
|
|
1109
|
+
return self._format_string_output(output_value, tool_name)
|
|
1110
|
+
else:
|
|
1111
|
+
return self._format_other_output(output_value)
|