glaip-sdk 0.0.5__py3-none-any.whl → 0.0.6a0__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/branding.py +3 -2
- glaip_sdk/cli/commands/__init__.py +1 -1
- glaip_sdk/cli/commands/agents.py +444 -268
- glaip_sdk/cli/commands/configure.py +12 -11
- glaip_sdk/cli/commands/mcps.py +28 -16
- glaip_sdk/cli/commands/models.py +5 -3
- glaip_sdk/cli/commands/tools.py +109 -102
- glaip_sdk/cli/display.py +38 -16
- 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 +376 -157
- glaip_sdk/cli/validators.py +7 -2
- glaip_sdk/client/agents.py +184 -89
- glaip_sdk/client/base.py +24 -13
- glaip_sdk/client/validators.py +154 -94
- glaip_sdk/config/constants.py +0 -2
- glaip_sdk/models.py +4 -4
- glaip_sdk/utils/__init__.py +7 -7
- glaip_sdk/utils/client_utils.py +144 -78
- glaip_sdk/utils/display.py +4 -2
- glaip_sdk/utils/general.py +8 -6
- glaip_sdk/utils/import_export.py +55 -24
- glaip_sdk/utils/rendering/formatting.py +12 -6
- glaip_sdk/utils/rendering/models.py +1 -1
- glaip_sdk/utils/rendering/renderer/base.py +412 -248
- 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 +3 -1
- glaip_sdk/utils/validation.py +2 -2
- glaip_sdk-0.0.6a0.dist-info/METADATA +183 -0
- glaip_sdk-0.0.6a0.dist-info/RECORD +55 -0
- {glaip_sdk-0.0.5.dist-info → glaip_sdk-0.0.6a0.dist-info}/WHEEL +1 -1
- glaip_sdk-0.0.6a0.dist-info/entry_points.txt +3 -0
- glaip_sdk-0.0.5.dist-info/METADATA +0 -645
- glaip_sdk-0.0.5.dist-info/RECORD +0 -55
- glaip_sdk-0.0.5.dist-info/entry_points.txt +0 -2
|
@@ -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
|
|
@@ -209,9 +212,12 @@ class RichStreamRenderer:
|
|
|
209
212
|
# Note: Thinking gaps are primarily a visual aid. Keep minimal here.
|
|
210
213
|
|
|
211
214
|
# Extract tool information
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
(
|
|
216
|
+
tool_name,
|
|
217
|
+
tool_args,
|
|
218
|
+
tool_out,
|
|
219
|
+
tool_calls_info,
|
|
220
|
+
) = self.stream_processor.parse_tool_calls(ev)
|
|
215
221
|
|
|
216
222
|
# Track tools and sub-agents
|
|
217
223
|
self.stream_processor.track_tools_and_agents(
|
|
@@ -224,7 +230,7 @@ class RichStreamRenderer:
|
|
|
224
230
|
# Update live display
|
|
225
231
|
self._ensure_live()
|
|
226
232
|
|
|
227
|
-
def on_complete(self, _stats: RunStats):
|
|
233
|
+
def on_complete(self, _stats: RunStats) -> None:
|
|
228
234
|
"""Handle completion event."""
|
|
229
235
|
self.state.finalizing_ui = True
|
|
230
236
|
|
|
@@ -265,7 +271,7 @@ class RichStreamRenderer:
|
|
|
265
271
|
# Non-fatal; renderer best-effort
|
|
266
272
|
pass
|
|
267
273
|
|
|
268
|
-
def _ensure_live(self):
|
|
274
|
+
def _ensure_live(self) -> None:
|
|
269
275
|
"""Ensure live display is updated."""
|
|
270
276
|
# Lazily create Live if needed
|
|
271
277
|
if self.live is None and self.cfg.live:
|
|
@@ -292,14 +298,16 @@ class RichStreamRenderer:
|
|
|
292
298
|
panels.extend(self._render_tool_panels())
|
|
293
299
|
self.live.update(Group(*panels))
|
|
294
300
|
|
|
295
|
-
def _render_main_panel(self):
|
|
301
|
+
def _render_main_panel(self) -> Any:
|
|
296
302
|
"""Render the main content panel."""
|
|
297
303
|
body = "".join(self.state.buffer).strip()
|
|
298
304
|
# Dynamic title with spinner + elapsed/hints
|
|
299
305
|
title = self._format_enhanced_main_title()
|
|
300
306
|
return create_main_panel(body, title, self.cfg.theme)
|
|
301
307
|
|
|
302
|
-
def _maybe_insert_thinking_gap(
|
|
308
|
+
def _maybe_insert_thinking_gap(
|
|
309
|
+
self, task_id: str | None, context_id: str | None
|
|
310
|
+
) -> None:
|
|
303
311
|
"""Insert thinking gap if needed."""
|
|
304
312
|
# Implementation would track thinking states
|
|
305
313
|
pass
|
|
@@ -345,7 +353,7 @@ class RichStreamRenderer:
|
|
|
345
353
|
tool_name: str,
|
|
346
354
|
tool_args: Any,
|
|
347
355
|
_tool_sid: str,
|
|
348
|
-
):
|
|
356
|
+
) -> Step | None:
|
|
349
357
|
"""Start or get a step for a tool."""
|
|
350
358
|
if is_delegation_tool(tool_name):
|
|
351
359
|
st = self.steps.start_or_get(
|
|
@@ -373,8 +381,12 @@ class RichStreamRenderer:
|
|
|
373
381
|
return st
|
|
374
382
|
|
|
375
383
|
def _process_additional_tool_calls(
|
|
376
|
-
self,
|
|
377
|
-
|
|
384
|
+
self,
|
|
385
|
+
tool_calls_info: list[tuple[str, Any, Any]],
|
|
386
|
+
tool_name: str,
|
|
387
|
+
task_id: str,
|
|
388
|
+
context_id: str,
|
|
389
|
+
) -> None:
|
|
378
390
|
"""Process additional tool calls to avoid duplicates."""
|
|
379
391
|
for call_name, call_args, _ in tool_calls_info or []:
|
|
380
392
|
if call_name and call_name != tool_name:
|
|
@@ -421,55 +433,123 @@ class RichStreamRenderer:
|
|
|
421
433
|
|
|
422
434
|
return False, None, None
|
|
423
435
|
|
|
436
|
+
def _get_tool_session_id(
|
|
437
|
+
self, finished_tool_name: str, task_id: str, context_id: str
|
|
438
|
+
) -> str:
|
|
439
|
+
"""Generate tool session ID."""
|
|
440
|
+
return f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
441
|
+
|
|
442
|
+
def _calculate_tool_duration(self, meta: dict[str, Any]) -> float | None:
|
|
443
|
+
"""Calculate tool duration from metadata."""
|
|
444
|
+
try:
|
|
445
|
+
server_now = self.stream_processor.server_elapsed_time
|
|
446
|
+
server_start = meta.get("server_started_at")
|
|
447
|
+
dur = None
|
|
448
|
+
|
|
449
|
+
if isinstance(server_now, int | float) and isinstance(
|
|
450
|
+
server_start, int | float
|
|
451
|
+
):
|
|
452
|
+
dur = max(0.0, float(server_now) - float(server_start))
|
|
453
|
+
elif meta.get("started_at") is not None:
|
|
454
|
+
dur = max(0.0, float(monotonic() - meta.get("started_at")))
|
|
455
|
+
|
|
456
|
+
return dur
|
|
457
|
+
except Exception:
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
def _update_tool_metadata(self, meta: dict[str, Any], dur: float | None) -> None:
|
|
461
|
+
"""Update tool metadata with duration information."""
|
|
462
|
+
if dur is not None:
|
|
463
|
+
meta["duration_seconds"] = dur
|
|
464
|
+
meta["server_finished_at"] = (
|
|
465
|
+
self.stream_processor.server_elapsed_time
|
|
466
|
+
if isinstance(self.stream_processor.server_elapsed_time, int | float)
|
|
467
|
+
else None
|
|
468
|
+
)
|
|
469
|
+
meta["finished_at"] = monotonic()
|
|
470
|
+
|
|
471
|
+
def _add_tool_output_to_panel(
|
|
472
|
+
self, meta: dict[str, Any], finished_tool_output: Any, finished_tool_name: str
|
|
473
|
+
) -> None:
|
|
474
|
+
"""Add tool output to panel metadata."""
|
|
475
|
+
if finished_tool_output is not None:
|
|
476
|
+
meta["chunks"].append(
|
|
477
|
+
self._format_output_block(finished_tool_output, finished_tool_name)
|
|
478
|
+
)
|
|
479
|
+
meta["output"] = finished_tool_output
|
|
480
|
+
|
|
481
|
+
def _mark_panel_as_finished(self, meta: dict[str, Any], tool_sid: str) -> None:
|
|
482
|
+
"""Mark panel as finished and ensure visibility."""
|
|
483
|
+
if meta.get("status") != "finished":
|
|
484
|
+
meta["status"] = "finished"
|
|
485
|
+
|
|
486
|
+
dur = self._calculate_tool_duration(meta)
|
|
487
|
+
self._update_tool_metadata(meta, dur)
|
|
488
|
+
|
|
489
|
+
# Ensure this finished panel is visible in this frame
|
|
490
|
+
self.stream_processor.current_event_finished_panels.add(tool_sid)
|
|
491
|
+
|
|
424
492
|
def _finish_tool_panel(
|
|
425
493
|
self,
|
|
426
494
|
finished_tool_name: str,
|
|
427
495
|
finished_tool_output: Any,
|
|
428
496
|
task_id: str,
|
|
429
497
|
context_id: str,
|
|
430
|
-
):
|
|
498
|
+
) -> None:
|
|
431
499
|
"""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")
|
|
500
|
+
tool_sid = self._get_tool_session_id(finished_tool_name, task_id, context_id)
|
|
501
|
+
if tool_sid not in self.tool_panels:
|
|
502
|
+
return
|
|
436
503
|
|
|
437
|
-
|
|
438
|
-
|
|
504
|
+
meta = self.tool_panels[tool_sid]
|
|
505
|
+
self._mark_panel_as_finished(meta, tool_sid)
|
|
506
|
+
self._add_tool_output_to_panel(meta, finished_tool_output, finished_tool_name)
|
|
439
507
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
508
|
+
def _get_step_duration(
|
|
509
|
+
self, finished_tool_name: str, task_id: str, context_id: str
|
|
510
|
+
) -> float | None:
|
|
511
|
+
"""Get step duration from tool panels."""
|
|
512
|
+
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
513
|
+
try:
|
|
514
|
+
return self.tool_panels.get(tool_sid, {}).get("duration_seconds")
|
|
515
|
+
except Exception:
|
|
516
|
+
return None
|
|
470
517
|
|
|
471
|
-
|
|
472
|
-
|
|
518
|
+
def _finish_delegation_step(
|
|
519
|
+
self,
|
|
520
|
+
finished_tool_name: str,
|
|
521
|
+
finished_tool_output: Any,
|
|
522
|
+
task_id: str,
|
|
523
|
+
context_id: str,
|
|
524
|
+
step_duration: float | None,
|
|
525
|
+
) -> None:
|
|
526
|
+
"""Finish a delegation step."""
|
|
527
|
+
self.steps.finish(
|
|
528
|
+
task_id=task_id,
|
|
529
|
+
context_id=context_id,
|
|
530
|
+
kind="delegate",
|
|
531
|
+
name=finished_tool_name,
|
|
532
|
+
output=finished_tool_output,
|
|
533
|
+
duration_raw=step_duration,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
def _finish_tool_step_type(
|
|
537
|
+
self,
|
|
538
|
+
finished_tool_name: str,
|
|
539
|
+
finished_tool_output: Any,
|
|
540
|
+
task_id: str,
|
|
541
|
+
context_id: str,
|
|
542
|
+
step_duration: float | None,
|
|
543
|
+
) -> None:
|
|
544
|
+
"""Finish a regular tool step."""
|
|
545
|
+
self.steps.finish(
|
|
546
|
+
task_id=task_id,
|
|
547
|
+
context_id=context_id,
|
|
548
|
+
kind="tool",
|
|
549
|
+
name=finished_tool_name,
|
|
550
|
+
output=finished_tool_output,
|
|
551
|
+
duration_raw=step_duration,
|
|
552
|
+
)
|
|
473
553
|
|
|
474
554
|
def _finish_tool_step(
|
|
475
555
|
self,
|
|
@@ -477,85 +557,114 @@ class RichStreamRenderer:
|
|
|
477
557
|
finished_tool_output: Any,
|
|
478
558
|
task_id: str,
|
|
479
559
|
context_id: str,
|
|
480
|
-
):
|
|
560
|
+
) -> None:
|
|
481
561
|
"""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
|
|
562
|
+
step_duration = self._get_step_duration(finished_tool_name, task_id, context_id)
|
|
489
563
|
|
|
490
564
|
if is_delegation_tool(finished_tool_name):
|
|
491
|
-
self.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
duration_raw=step_duration,
|
|
565
|
+
self._finish_delegation_step(
|
|
566
|
+
finished_tool_name,
|
|
567
|
+
finished_tool_output,
|
|
568
|
+
task_id,
|
|
569
|
+
context_id,
|
|
570
|
+
step_duration,
|
|
498
571
|
)
|
|
499
572
|
else:
|
|
500
|
-
self.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
duration_raw=step_duration,
|
|
573
|
+
self._finish_tool_step_type(
|
|
574
|
+
finished_tool_name,
|
|
575
|
+
finished_tool_output,
|
|
576
|
+
task_id,
|
|
577
|
+
context_id,
|
|
578
|
+
step_duration,
|
|
507
579
|
)
|
|
508
580
|
|
|
581
|
+
def _should_create_snapshot(self, tool_sid: str) -> bool:
|
|
582
|
+
"""Check if a snapshot should be created."""
|
|
583
|
+
return self.cfg.append_finished_snapshots and not self.tool_panels.get(
|
|
584
|
+
tool_sid, {}
|
|
585
|
+
).get("snapshot_printed")
|
|
586
|
+
|
|
587
|
+
def _get_snapshot_title(self, meta: dict[str, Any], finished_tool_name: str) -> str:
|
|
588
|
+
"""Get the title for the snapshot."""
|
|
589
|
+
adjusted_title = meta.get("title") or finished_tool_name
|
|
590
|
+
|
|
591
|
+
# Add elapsed time to title
|
|
592
|
+
dur = meta.get("duration_seconds")
|
|
593
|
+
if isinstance(dur, int | float):
|
|
594
|
+
elapsed_str = self._format_snapshot_duration(dur)
|
|
595
|
+
adjusted_title = f"{adjusted_title} · {elapsed_str}"
|
|
596
|
+
|
|
597
|
+
return adjusted_title
|
|
598
|
+
|
|
599
|
+
def _format_snapshot_duration(self, dur: int | float) -> str:
|
|
600
|
+
"""Format duration for snapshot title."""
|
|
601
|
+
try:
|
|
602
|
+
# Handle invalid types
|
|
603
|
+
if not isinstance(dur, (int, float)):
|
|
604
|
+
return "<1ms"
|
|
605
|
+
|
|
606
|
+
if dur >= 1:
|
|
607
|
+
return f"{dur:.2f}s"
|
|
608
|
+
elif int(dur * 1000) > 0:
|
|
609
|
+
return f"{int(dur * 1000)}ms"
|
|
610
|
+
else:
|
|
611
|
+
return "<1ms"
|
|
612
|
+
except (TypeError, ValueError, OverflowError):
|
|
613
|
+
return "<1ms"
|
|
614
|
+
|
|
615
|
+
def _clamp_snapshot_body(self, body_text: str) -> str:
|
|
616
|
+
"""Clamp snapshot body to configured limits."""
|
|
617
|
+
max_lines = int(self.cfg.snapshot_max_lines or 0) or 60
|
|
618
|
+
lines = body_text.splitlines()
|
|
619
|
+
if len(lines) > max_lines:
|
|
620
|
+
lines = lines[:max_lines] + ["… (truncated)"]
|
|
621
|
+
body_text = "\n".join(lines)
|
|
622
|
+
|
|
623
|
+
max_chars = int(self.cfg.snapshot_max_chars or 0) or 4000
|
|
624
|
+
if len(body_text) > max_chars:
|
|
625
|
+
body_text = body_text[: max_chars - 12] + "\n… (truncated)"
|
|
626
|
+
|
|
627
|
+
return body_text
|
|
628
|
+
|
|
629
|
+
def _create_snapshot_panel(
|
|
630
|
+
self, adjusted_title: str, body_text: str, finished_tool_name: str
|
|
631
|
+
) -> Any:
|
|
632
|
+
"""Create the snapshot panel."""
|
|
633
|
+
return create_tool_panel(
|
|
634
|
+
title=adjusted_title,
|
|
635
|
+
content=body_text or "(no output)",
|
|
636
|
+
status="finished",
|
|
637
|
+
theme=self.cfg.theme,
|
|
638
|
+
is_delegation=is_delegation_tool(finished_tool_name),
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
def _print_and_mark_snapshot(self, tool_sid: str, snapshot_panel: Any) -> None:
|
|
642
|
+
"""Print snapshot and mark as printed."""
|
|
643
|
+
self.console.print(snapshot_panel)
|
|
644
|
+
self.tool_panels[tool_sid]["snapshot_printed"] = True
|
|
645
|
+
|
|
509
646
|
def _create_tool_snapshot(
|
|
510
647
|
self, finished_tool_name: str, task_id: str, context_id: str
|
|
511
|
-
):
|
|
648
|
+
) -> None:
|
|
512
649
|
"""Create and print a snapshot for a finished tool."""
|
|
513
|
-
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
514
|
-
|
|
515
650
|
try:
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
):
|
|
651
|
+
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
652
|
+
|
|
653
|
+
if not self._should_create_snapshot(tool_sid):
|
|
520
654
|
return
|
|
521
655
|
|
|
522
656
|
meta = self.tool_panels[tool_sid]
|
|
523
|
-
adjusted_title =
|
|
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}"
|
|
657
|
+
adjusted_title = self._get_snapshot_title(meta, finished_tool_name)
|
|
534
658
|
|
|
535
659
|
# Compose body from chunks and clamp
|
|
536
660
|
body_text = "".join(meta.get("chunks") or [])
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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),
|
|
661
|
+
body_text = self._clamp_snapshot_body(body_text)
|
|
662
|
+
|
|
663
|
+
snapshot_panel = self._create_snapshot_panel(
|
|
664
|
+
adjusted_title, body_text, finished_tool_name
|
|
553
665
|
)
|
|
554
666
|
|
|
555
|
-
|
|
556
|
-
self.console.print(snapshot_panel)
|
|
557
|
-
# Guard so we don't print snapshot twice
|
|
558
|
-
self.tool_panels[tool_sid]["snapshot_printed"] = True
|
|
667
|
+
self._print_and_mark_snapshot(tool_sid, snapshot_panel)
|
|
559
668
|
|
|
560
669
|
except Exception:
|
|
561
670
|
pass
|
|
@@ -566,8 +675,8 @@ class RichStreamRenderer:
|
|
|
566
675
|
tool_name: str | None,
|
|
567
676
|
tool_args: Any,
|
|
568
677
|
_tool_out: Any,
|
|
569
|
-
tool_calls_info: list,
|
|
570
|
-
):
|
|
678
|
+
tool_calls_info: list[tuple[str, Any, Any]],
|
|
679
|
+
) -> None:
|
|
571
680
|
"""Handle agent step event."""
|
|
572
681
|
metadata = event.get("metadata", {})
|
|
573
682
|
task_id = event.get("task_id")
|
|
@@ -587,9 +696,11 @@ class RichStreamRenderer:
|
|
|
587
696
|
)
|
|
588
697
|
|
|
589
698
|
# Check for tool completion
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
699
|
+
(
|
|
700
|
+
is_tool_finished,
|
|
701
|
+
finished_tool_name,
|
|
702
|
+
finished_tool_output,
|
|
703
|
+
) = self._detect_tool_completion(metadata, content)
|
|
593
704
|
|
|
594
705
|
if is_tool_finished and finished_tool_name:
|
|
595
706
|
self._finish_tool_panel(
|
|
@@ -623,7 +734,7 @@ class RichStreamRenderer:
|
|
|
623
734
|
except Exception:
|
|
624
735
|
pass
|
|
625
736
|
|
|
626
|
-
def __del__(self):
|
|
737
|
+
def __del__(self) -> None:
|
|
627
738
|
try:
|
|
628
739
|
if self.live:
|
|
629
740
|
self.live.stop()
|
|
@@ -705,13 +816,13 @@ class RichStreamRenderer:
|
|
|
705
816
|
return "🧠"
|
|
706
817
|
return ""
|
|
707
818
|
|
|
708
|
-
def _format_step_status(self, step) -> str:
|
|
819
|
+
def _format_step_status(self, step: Step) -> str:
|
|
709
820
|
"""Format step status with elapsed time or duration."""
|
|
710
821
|
if is_step_finished(step):
|
|
711
822
|
if step.duration_ms is None:
|
|
712
823
|
return "[<1ms]"
|
|
713
824
|
elif step.duration_ms >= 1000:
|
|
714
|
-
return f"[{step.duration_ms/1000:.2f}s]"
|
|
825
|
+
return f"[{step.duration_ms / 1000:.2f}s]"
|
|
715
826
|
elif step.duration_ms > 0:
|
|
716
827
|
return f"[{step.duration_ms}ms]"
|
|
717
828
|
return "[<1ms]"
|
|
@@ -723,7 +834,7 @@ class RichStreamRenderer:
|
|
|
723
834
|
ms = int(elapsed * 1000)
|
|
724
835
|
return f"[{ms}ms]" if ms > 0 else "[<1ms]"
|
|
725
836
|
|
|
726
|
-
def _calculate_step_elapsed_time(self, step) -> float:
|
|
837
|
+
def _calculate_step_elapsed_time(self, step: Step) -> float:
|
|
727
838
|
"""Calculate elapsed time for a running step."""
|
|
728
839
|
server_elapsed = self.stream_processor.server_elapsed_time
|
|
729
840
|
server_start = self._step_server_start_times.get(step.step_id)
|
|
@@ -738,7 +849,7 @@ class RichStreamRenderer:
|
|
|
738
849
|
except Exception:
|
|
739
850
|
return 0.0
|
|
740
851
|
|
|
741
|
-
def _get_step_display_name(self, step) -> str:
|
|
852
|
+
def _get_step_display_name(self, step: Step) -> str:
|
|
742
853
|
"""Get display name for a step."""
|
|
743
854
|
if step.name and step.name != "step":
|
|
744
855
|
return step.name
|
|
@@ -779,142 +890,195 @@ class RichStreamRenderer:
|
|
|
779
890
|
|
|
780
891
|
return Text("\n".join(lines), style="dim")
|
|
781
892
|
|
|
893
|
+
def _should_skip_finished_panel(self, sid: str, status: str) -> bool:
|
|
894
|
+
"""Check if a finished panel should be skipped."""
|
|
895
|
+
if status != "finished":
|
|
896
|
+
return False
|
|
897
|
+
|
|
898
|
+
if getattr(self.cfg, "append_finished_snapshots", False):
|
|
899
|
+
return True
|
|
900
|
+
|
|
901
|
+
return (
|
|
902
|
+
not self.state.finalizing_ui
|
|
903
|
+
and sid not in self.stream_processor.current_event_finished_panels
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
def _calculate_elapsed_time(self, meta: dict[str, Any]) -> str:
|
|
907
|
+
"""Calculate elapsed time string for running tools."""
|
|
908
|
+
server_elapsed = self.stream_processor.server_elapsed_time
|
|
909
|
+
server_start = meta.get("server_started_at")
|
|
910
|
+
|
|
911
|
+
if isinstance(server_elapsed, int | float) and isinstance(
|
|
912
|
+
server_start, int | float
|
|
913
|
+
):
|
|
914
|
+
elapsed = max(0.0, float(server_elapsed) - float(server_start))
|
|
915
|
+
else:
|
|
916
|
+
try:
|
|
917
|
+
elapsed = max(0.0, monotonic() - (meta.get("started_at") or 0.0))
|
|
918
|
+
except Exception:
|
|
919
|
+
elapsed = 0.0
|
|
920
|
+
|
|
921
|
+
return self._format_elapsed_time(elapsed)
|
|
922
|
+
|
|
923
|
+
def _format_elapsed_time(self, elapsed: float) -> str:
|
|
924
|
+
"""Format elapsed time as a readable string."""
|
|
925
|
+
if elapsed >= 1:
|
|
926
|
+
return f"{elapsed:.2f}s"
|
|
927
|
+
elif int(elapsed * 1000) > 0:
|
|
928
|
+
return f"{int(elapsed * 1000)}ms"
|
|
929
|
+
else:
|
|
930
|
+
return "<1ms"
|
|
931
|
+
|
|
932
|
+
def _calculate_finished_duration(self, meta: dict[str, Any]) -> str | None:
|
|
933
|
+
"""Calculate duration string for finished tools."""
|
|
934
|
+
dur = meta.get("duration_seconds")
|
|
935
|
+
if isinstance(dur, int | float):
|
|
936
|
+
return self._format_elapsed_time(dur)
|
|
937
|
+
|
|
938
|
+
try:
|
|
939
|
+
server_now = self.stream_processor.server_elapsed_time
|
|
940
|
+
server_start = meta.get("server_started_at")
|
|
941
|
+
if isinstance(server_now, int | float) and isinstance(
|
|
942
|
+
server_start, int | float
|
|
943
|
+
):
|
|
944
|
+
dur = max(0.0, float(server_now) - float(server_start))
|
|
945
|
+
elif meta.get("started_at") is not None:
|
|
946
|
+
dur = max(0.0, float(monotonic() - meta.get("started_at")))
|
|
947
|
+
except Exception:
|
|
948
|
+
dur = None
|
|
949
|
+
|
|
950
|
+
return self._format_elapsed_time(dur) if isinstance(dur, int | float) else None
|
|
951
|
+
|
|
952
|
+
def _process_running_tool_panel(
|
|
953
|
+
self, title: str, meta: dict[str, Any], body: str
|
|
954
|
+
) -> tuple[str, str]:
|
|
955
|
+
"""Process a running tool panel."""
|
|
956
|
+
elapsed_str = self._calculate_elapsed_time(meta)
|
|
957
|
+
adjusted_title = f"{title} · {elapsed_str}"
|
|
958
|
+
chip = f"⏱ {elapsed_str}"
|
|
959
|
+
|
|
960
|
+
if not body:
|
|
961
|
+
body = chip
|
|
962
|
+
else:
|
|
963
|
+
body = f"{body}\n\n{chip}"
|
|
964
|
+
|
|
965
|
+
return adjusted_title, body
|
|
966
|
+
|
|
967
|
+
def _process_finished_tool_panel(self, title: str, meta: dict[str, Any]) -> str:
|
|
968
|
+
"""Process a finished tool panel."""
|
|
969
|
+
duration_str = self._calculate_finished_duration(meta)
|
|
970
|
+
return f"{title} · {duration_str}" if duration_str else title
|
|
971
|
+
|
|
972
|
+
def _create_tool_panel_for_session(
|
|
973
|
+
self, sid: str, meta: dict[str, Any]
|
|
974
|
+
) -> AIPPanel | None:
|
|
975
|
+
"""Create a single tool panel for the session."""
|
|
976
|
+
title = meta.get("title") or "Tool"
|
|
977
|
+
status = meta.get("status") or "running"
|
|
978
|
+
chunks = meta.get("chunks") or []
|
|
979
|
+
is_delegation = bool(meta.get("is_delegation"))
|
|
980
|
+
|
|
981
|
+
if self._should_skip_finished_panel(sid, status):
|
|
982
|
+
return None
|
|
983
|
+
|
|
984
|
+
body = "".join(chunks)
|
|
985
|
+
adjusted_title = title
|
|
986
|
+
|
|
987
|
+
if status == "running":
|
|
988
|
+
adjusted_title, body = self._process_running_tool_panel(title, meta, body)
|
|
989
|
+
elif status == "finished":
|
|
990
|
+
adjusted_title = self._process_finished_tool_panel(title, meta)
|
|
991
|
+
|
|
992
|
+
return create_tool_panel(
|
|
993
|
+
title=adjusted_title,
|
|
994
|
+
content=body or "Processing...",
|
|
995
|
+
status=status,
|
|
996
|
+
theme=self.cfg.theme,
|
|
997
|
+
is_delegation=is_delegation,
|
|
998
|
+
)
|
|
999
|
+
|
|
782
1000
|
def _render_tool_panels(self) -> list[AIPPanel]:
|
|
783
1001
|
"""Render tool execution output panels."""
|
|
784
1002
|
panels: list[AIPPanel] = []
|
|
785
1003
|
for sid in self.tool_order:
|
|
786
1004
|
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}"
|
|
1005
|
+
panel = self._create_tool_panel_for_session(sid, meta)
|
|
1006
|
+
if panel:
|
|
1007
|
+
panels.append(panel)
|
|
859
1008
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1009
|
+
return panels
|
|
1010
|
+
|
|
1011
|
+
def _format_dict_or_list_output(self, output_value: dict | list) -> str:
|
|
1012
|
+
"""Format dict/list output as pretty JSON."""
|
|
1013
|
+
try:
|
|
1014
|
+
return (
|
|
1015
|
+
self.OUTPUT_PREFIX
|
|
1016
|
+
+ "```json\n"
|
|
1017
|
+
+ json.dumps(output_value, indent=2)
|
|
1018
|
+
+ "\n```\n"
|
|
868
1019
|
)
|
|
1020
|
+
except Exception:
|
|
1021
|
+
return self.OUTPUT_PREFIX + str(output_value) + "\n"
|
|
869
1022
|
|
|
870
|
-
|
|
1023
|
+
def _clean_sub_agent_prefix(self, output: str, tool_name: str | None) -> str:
|
|
1024
|
+
"""Clean sub-agent name prefix from output."""
|
|
1025
|
+
if not (tool_name and is_delegation_tool(tool_name)):
|
|
1026
|
+
return output
|
|
871
1027
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
pass
|
|
1028
|
+
try:
|
|
1029
|
+
sub = tool_name
|
|
1030
|
+
if tool_name.startswith("delegate_to_"):
|
|
1031
|
+
sub = tool_name.replace("delegate_to_", "")
|
|
1032
|
+
elif tool_name.startswith("delegate_"):
|
|
1033
|
+
sub = tool_name.replace("delegate_", "")
|
|
1034
|
+
prefix = f"[{sub}]"
|
|
1035
|
+
if output.startswith(prefix):
|
|
1036
|
+
return output[len(prefix) :].lstrip()
|
|
1037
|
+
except Exception:
|
|
1038
|
+
pass
|
|
884
1039
|
|
|
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"
|
|
1040
|
+
return output
|
|
914
1041
|
|
|
1042
|
+
def _format_json_string_output(self, output: str) -> str:
|
|
1043
|
+
"""Format string that looks like JSON."""
|
|
915
1044
|
try:
|
|
916
|
-
|
|
1045
|
+
parsed = json.loads(output)
|
|
1046
|
+
return (
|
|
1047
|
+
self.OUTPUT_PREFIX
|
|
1048
|
+
+ "```json\n"
|
|
1049
|
+
+ json.dumps(parsed, indent=2)
|
|
1050
|
+
+ "\n```\n"
|
|
1051
|
+
)
|
|
917
1052
|
except Exception:
|
|
918
|
-
return
|
|
1053
|
+
return self.OUTPUT_PREFIX + output + "\n"
|
|
1054
|
+
|
|
1055
|
+
def _format_string_output(self, output: str, tool_name: str | None) -> str:
|
|
1056
|
+
"""Format string output with optional prefix cleaning."""
|
|
1057
|
+
s = output.strip()
|
|
1058
|
+
s = self._clean_sub_agent_prefix(s, tool_name)
|
|
1059
|
+
|
|
1060
|
+
# If looks like JSON, pretty print it
|
|
1061
|
+
if (s.startswith("{") and s.endswith("}")) or (
|
|
1062
|
+
s.startswith("[") and s.endswith("]")
|
|
1063
|
+
):
|
|
1064
|
+
return self._format_json_string_output(s)
|
|
1065
|
+
|
|
1066
|
+
return self.OUTPUT_PREFIX + s + "\n"
|
|
1067
|
+
|
|
1068
|
+
def _format_other_output(self, output_value: Any) -> str:
|
|
1069
|
+
"""Format other types of output."""
|
|
1070
|
+
try:
|
|
1071
|
+
return self.OUTPUT_PREFIX + json.dumps(output_value, indent=2) + "\n"
|
|
1072
|
+
except Exception:
|
|
1073
|
+
return self.OUTPUT_PREFIX + str(output_value) + "\n"
|
|
1074
|
+
|
|
1075
|
+
def _format_output_block(self, output_value: Any, tool_name: str | None) -> str:
|
|
1076
|
+
"""Format an output value for panel display."""
|
|
1077
|
+
if isinstance(output_value, dict | list):
|
|
1078
|
+
return self._format_dict_or_list_output(output_value)
|
|
1079
|
+
elif isinstance(output_value, str):
|
|
1080
|
+
return self._format_string_output(output_value, tool_name)
|
|
1081
|
+
else:
|
|
1082
|
+
return self._format_other_output(output_value)
|
|
919
1083
|
|
|
920
1084
|
# No legacy surface helpers are exposed; use modern interfaces only
|