glaip-sdk 0.2.2__py3-none-any.whl → 0.4.0__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/cli/auth.py +2 -1
- glaip_sdk/cli/commands/agents.py +51 -36
- glaip_sdk/cli/commands/configure.py +2 -1
- glaip_sdk/cli/commands/mcps.py +219 -62
- glaip_sdk/cli/commands/models.py +3 -5
- glaip_sdk/cli/commands/tools.py +27 -16
- glaip_sdk/cli/commands/transcripts.py +1 -1
- glaip_sdk/cli/constants.py +3 -0
- glaip_sdk/cli/display.py +1 -1
- glaip_sdk/cli/hints.py +58 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +3 -4
- glaip_sdk/cli/slash/agent_session.py +4 -13
- glaip_sdk/cli/slash/prompt.py +3 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +139 -48
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
- glaip_sdk/cli/transcript/capture.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +19 -678
- glaip_sdk/cli/update_notifier.py +2 -1
- glaip_sdk/cli/utils.py +228 -101
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +40 -22
- glaip_sdk/client/main.py +2 -6
- glaip_sdk/client/mcps.py +13 -5
- glaip_sdk/client/run_rendering.py +90 -111
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +2 -3
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/models/__init__.py +56 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models.py +8 -7
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/utils/client_utils.py +13 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/import_export.py +6 -9
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
- glaip_sdk/utils/rendering/renderer/base.py +217 -1476
- glaip_sdk/utils/rendering/renderer/debug.py +24 -1
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -439
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +26 -15
- glaip_sdk/utils/validation.py +13 -21
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.4.0.dist-info}/METADATA +24 -2
- glaip_sdk-0.4.0.dist-info/RECORD +110 -0
- glaip_sdk-0.2.2.dist-info/RECORD +0 -87
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.4.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -6,16 +6,12 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from collections.abc import Callable
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from datetime import datetime, timezone
|
|
9
|
+
from collections.abc import Callable
|
|
12
10
|
from pathlib import Path
|
|
13
11
|
from typing import Any
|
|
14
12
|
|
|
15
13
|
import click
|
|
16
14
|
from rich.console import Console
|
|
17
|
-
from rich.markdown import Markdown
|
|
18
|
-
from rich.text import Text
|
|
19
15
|
|
|
20
16
|
try: # pragma: no cover - optional dependency
|
|
21
17
|
import questionary
|
|
@@ -25,34 +21,20 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
25
21
|
Choice = None # type: ignore[assignment]
|
|
26
22
|
|
|
27
23
|
from glaip_sdk.cli.transcript.cache import suggest_filename
|
|
28
|
-
from glaip_sdk.
|
|
29
|
-
from glaip_sdk.
|
|
30
|
-
from glaip_sdk.utils.rendering.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
|
|
25
|
+
from glaip_sdk.utils.rendering.layout.progress import is_delegation_tool
|
|
26
|
+
from glaip_sdk.utils.rendering.viewer import (
|
|
27
|
+
ViewerContext as PresenterViewerContext,
|
|
28
|
+
prepare_viewer_snapshot as presenter_prepare_viewer_snapshot,
|
|
29
|
+
render_post_run_view as presenter_render_post_run_view,
|
|
30
|
+
render_transcript_events as presenter_render_transcript_events,
|
|
31
|
+
render_transcript_view as presenter_render_transcript_view,
|
|
34
32
|
)
|
|
35
|
-
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
36
|
-
from glaip_sdk.utils.rendering.renderer.panels import create_final_panel
|
|
37
|
-
from glaip_sdk.utils.rendering.renderer.progress import (
|
|
38
|
-
format_elapsed_time,
|
|
39
|
-
is_delegation_tool,
|
|
40
|
-
)
|
|
41
|
-
from glaip_sdk.utils.rendering.steps import StepManager
|
|
42
33
|
|
|
43
34
|
EXPORT_CANCELLED_MESSAGE = "[dim]Export cancelled.[/dim]"
|
|
44
35
|
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
class ViewerContext:
|
|
48
|
-
"""Runtime context for the viewer session."""
|
|
49
|
-
|
|
50
|
-
manifest_entry: dict[str, Any]
|
|
51
|
-
events: list[dict[str, Any]]
|
|
52
|
-
default_output: str
|
|
53
|
-
final_output: str
|
|
54
|
-
stream_started_at: float | None
|
|
55
|
-
meta: dict[str, Any]
|
|
37
|
+
ViewerContext = PresenterViewerContext
|
|
56
38
|
|
|
57
39
|
|
|
58
40
|
class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
@@ -108,66 +90,12 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
108
90
|
self.console.print(f"[dim]{' · '.join(subtitle_parts)}[/]")
|
|
109
91
|
self.console.print()
|
|
110
92
|
|
|
111
|
-
query = self._get_user_query()
|
|
112
|
-
|
|
113
93
|
if self._view_mode == "default":
|
|
114
|
-
self.
|
|
94
|
+
presenter_render_post_run_view(self.console, self.ctx)
|
|
115
95
|
else:
|
|
116
|
-
self.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"""Render the default summary view.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
query: Optional user query to display.
|
|
123
|
-
"""
|
|
124
|
-
if query:
|
|
125
|
-
self._render_user_query(query)
|
|
126
|
-
self._render_steps_summary()
|
|
127
|
-
self._render_final_panel()
|
|
128
|
-
|
|
129
|
-
def _render_transcript_view(self, query: str | None) -> None:
|
|
130
|
-
"""Render the full transcript view with events.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
query: Optional user query to display.
|
|
134
|
-
"""
|
|
135
|
-
if not self.ctx.events:
|
|
136
|
-
self.console.print("[dim]No SSE events were captured for this run.[/dim]")
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
if query:
|
|
140
|
-
self._render_user_query(query)
|
|
141
|
-
|
|
142
|
-
self._render_steps_summary()
|
|
143
|
-
self._render_final_panel()
|
|
144
|
-
|
|
145
|
-
self.console.print("[bold]Transcript Events[/bold]")
|
|
146
|
-
self.console.print("[dim]────────────────────────────────────────────────────────[/dim]")
|
|
147
|
-
|
|
148
|
-
base_received_ts: datetime | None = None
|
|
149
|
-
for event in self.ctx.events:
|
|
150
|
-
received_ts = self._parse_received_timestamp(event)
|
|
151
|
-
if base_received_ts is None and received_ts is not None:
|
|
152
|
-
base_received_ts = received_ts
|
|
153
|
-
render_debug_event(
|
|
154
|
-
event,
|
|
155
|
-
self.console,
|
|
156
|
-
received_ts=received_ts,
|
|
157
|
-
baseline_ts=base_received_ts,
|
|
158
|
-
)
|
|
159
|
-
self.console.print()
|
|
160
|
-
|
|
161
|
-
def _render_final_panel(self) -> None:
|
|
162
|
-
"""Render the final result panel."""
|
|
163
|
-
content = self.ctx.final_output or self.ctx.default_output or "No response content captured."
|
|
164
|
-
title = "Final Result"
|
|
165
|
-
duration_text = self._extract_final_duration()
|
|
166
|
-
if duration_text:
|
|
167
|
-
title += f" · {duration_text}"
|
|
168
|
-
panel = create_final_panel(content, title=title, theme="dark")
|
|
169
|
-
self.console.print(panel)
|
|
170
|
-
self.console.print()
|
|
96
|
+
snapshot, state = presenter_prepare_viewer_snapshot(self.ctx, glyphs=None)
|
|
97
|
+
presenter_render_transcript_view(self.console, snapshot)
|
|
98
|
+
presenter_render_transcript_events(self.console, state.events)
|
|
171
99
|
|
|
172
100
|
# ------------------------------------------------------------------
|
|
173
101
|
# Interaction loops
|
|
@@ -259,38 +187,10 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
259
187
|
|
|
260
188
|
def _prompt_export_choice(self, default_path: Path, default_display: str) -> tuple[str, Any] | None:
|
|
261
189
|
"""Render interactive export menu with numeric shortcuts."""
|
|
262
|
-
if not self.console.is_terminal
|
|
263
|
-
return None
|
|
264
|
-
|
|
265
|
-
try:
|
|
266
|
-
answer = questionary.select(
|
|
267
|
-
"Export transcript",
|
|
268
|
-
choices=[
|
|
269
|
-
Choice(
|
|
270
|
-
title=f"Save to default ({default_display})",
|
|
271
|
-
value=("default", default_path),
|
|
272
|
-
shortcut_key="1",
|
|
273
|
-
),
|
|
274
|
-
Choice(
|
|
275
|
-
title="Choose a different path",
|
|
276
|
-
value=("custom", None),
|
|
277
|
-
shortcut_key="2",
|
|
278
|
-
),
|
|
279
|
-
Choice(
|
|
280
|
-
title="Cancel",
|
|
281
|
-
value=("cancel", None),
|
|
282
|
-
shortcut_key="3",
|
|
283
|
-
),
|
|
284
|
-
],
|
|
285
|
-
use_shortcuts=True,
|
|
286
|
-
instruction="Press 1-3 (or arrows) then Enter.",
|
|
287
|
-
).ask()
|
|
288
|
-
except Exception:
|
|
190
|
+
if not self.console.is_terminal:
|
|
289
191
|
return None
|
|
290
192
|
|
|
291
|
-
|
|
292
|
-
return ("cancel", None)
|
|
293
|
-
return answer
|
|
193
|
+
return prompt_export_choice_questionary(default_path, default_display)
|
|
294
194
|
|
|
295
195
|
def _prompt_custom_destination(self) -> Path | None:
|
|
296
196
|
"""Prompt for custom export path with filesystem completion."""
|
|
@@ -298,11 +198,12 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
298
198
|
return None
|
|
299
199
|
|
|
300
200
|
try:
|
|
301
|
-
|
|
201
|
+
question = questionary.path(
|
|
302
202
|
"Destination path (Tab to autocomplete):",
|
|
303
203
|
default="",
|
|
304
204
|
only_directories=False,
|
|
305
|
-
)
|
|
205
|
+
)
|
|
206
|
+
response = questionary_safe_ask(question)
|
|
306
207
|
except Exception:
|
|
307
208
|
return None
|
|
308
209
|
|
|
@@ -364,523 +265,6 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
364
265
|
self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
|
|
365
266
|
self.console.print()
|
|
366
267
|
|
|
367
|
-
def _get_user_query(self) -> str | None:
|
|
368
|
-
"""Extract user query from metadata or manifest.
|
|
369
|
-
|
|
370
|
-
Returns:
|
|
371
|
-
User query string or None.
|
|
372
|
-
"""
|
|
373
|
-
meta = self.ctx.meta or {}
|
|
374
|
-
manifest = self.ctx.manifest_entry or {}
|
|
375
|
-
return meta.get("input_message") or meta.get("query") or meta.get("message") or manifest.get("input_message")
|
|
376
|
-
|
|
377
|
-
def _render_user_query(self, query: str) -> None:
|
|
378
|
-
"""Render user query in a panel.
|
|
379
|
-
|
|
380
|
-
Args:
|
|
381
|
-
query: User query string to render.
|
|
382
|
-
"""
|
|
383
|
-
panel = AIPPanel(
|
|
384
|
-
Markdown(f"Query: {query}"),
|
|
385
|
-
title="User Request",
|
|
386
|
-
border_style="#d97706",
|
|
387
|
-
)
|
|
388
|
-
self.console.print(panel)
|
|
389
|
-
self.console.print()
|
|
390
|
-
|
|
391
|
-
def _render_steps_summary(self) -> None:
|
|
392
|
-
"""Render steps summary panel."""
|
|
393
|
-
stored_lines = self.ctx.meta.get("transcript_step_lines")
|
|
394
|
-
if stored_lines:
|
|
395
|
-
body = Text("\n".join(stored_lines), style="dim")
|
|
396
|
-
else:
|
|
397
|
-
tree_text = self._build_tree_summary_text()
|
|
398
|
-
if tree_text is not None:
|
|
399
|
-
body = tree_text
|
|
400
|
-
else:
|
|
401
|
-
panel_content = self._format_steps_summary(self._build_step_summary())
|
|
402
|
-
body = Text(panel_content, style="dim")
|
|
403
|
-
panel = AIPPanel(body, title="Steps", border_style="blue")
|
|
404
|
-
self.console.print(panel)
|
|
405
|
-
self.console.print()
|
|
406
|
-
|
|
407
|
-
@staticmethod
|
|
408
|
-
def _format_steps_summary(steps: list[dict[str, Any]]) -> str:
|
|
409
|
-
"""Format steps summary as text.
|
|
410
|
-
|
|
411
|
-
Args:
|
|
412
|
-
steps: List of step dictionaries.
|
|
413
|
-
|
|
414
|
-
Returns:
|
|
415
|
-
Formatted text string.
|
|
416
|
-
"""
|
|
417
|
-
if not steps:
|
|
418
|
-
return " No steps yet"
|
|
419
|
-
|
|
420
|
-
lines = []
|
|
421
|
-
for step in steps:
|
|
422
|
-
icon = ICON_DELEGATE if step.get("is_delegate") else ICON_TOOL_STEP
|
|
423
|
-
duration = step.get("duration")
|
|
424
|
-
duration_str = f" [{duration}]" if duration else ""
|
|
425
|
-
status = " ✓" if step.get("finished") else ""
|
|
426
|
-
title = step.get("title") or step.get("name") or "Step"
|
|
427
|
-
lines.append(f" {icon} {title}{duration_str}{status}")
|
|
428
|
-
return "\n".join(lines)
|
|
429
|
-
|
|
430
|
-
@staticmethod
|
|
431
|
-
def _extract_event_time(event: dict[str, Any]) -> float | None:
|
|
432
|
-
"""Extract timestamp from event metadata.
|
|
433
|
-
|
|
434
|
-
Args:
|
|
435
|
-
event: Event dictionary.
|
|
436
|
-
|
|
437
|
-
Returns:
|
|
438
|
-
Time value as float or None.
|
|
439
|
-
"""
|
|
440
|
-
metadata = event.get("metadata") or {}
|
|
441
|
-
time_value = metadata.get("time")
|
|
442
|
-
try:
|
|
443
|
-
if isinstance(time_value, (int, float)):
|
|
444
|
-
return float(time_value)
|
|
445
|
-
except Exception:
|
|
446
|
-
return None
|
|
447
|
-
return None
|
|
448
|
-
|
|
449
|
-
@staticmethod
|
|
450
|
-
def _parse_received_timestamp(event: dict[str, Any]) -> datetime | None:
|
|
451
|
-
"""Parse received timestamp from event.
|
|
452
|
-
|
|
453
|
-
Args:
|
|
454
|
-
event: Event dictionary.
|
|
455
|
-
|
|
456
|
-
Returns:
|
|
457
|
-
Parsed datetime or None.
|
|
458
|
-
"""
|
|
459
|
-
value = event.get("received_at")
|
|
460
|
-
if not value:
|
|
461
|
-
return None
|
|
462
|
-
if isinstance(value, str):
|
|
463
|
-
try:
|
|
464
|
-
normalised = value.replace("Z", "+00:00")
|
|
465
|
-
parsed = datetime.fromisoformat(normalised)
|
|
466
|
-
except ValueError:
|
|
467
|
-
return None
|
|
468
|
-
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
|
469
|
-
return None
|
|
470
|
-
|
|
471
|
-
def _extract_final_duration(self) -> str | None:
|
|
472
|
-
"""Extract final duration from events.
|
|
473
|
-
|
|
474
|
-
Returns:
|
|
475
|
-
Duration string or None.
|
|
476
|
-
"""
|
|
477
|
-
for event in self.ctx.events:
|
|
478
|
-
metadata = event.get("metadata") or {}
|
|
479
|
-
if metadata.get("kind") == "final_response":
|
|
480
|
-
time_value = metadata.get("time")
|
|
481
|
-
try:
|
|
482
|
-
if isinstance(time_value, (int, float)):
|
|
483
|
-
return f"{float(time_value):.2f}s"
|
|
484
|
-
except Exception:
|
|
485
|
-
return None
|
|
486
|
-
return None
|
|
487
|
-
|
|
488
|
-
def _build_step_summary(self) -> list[dict[str, Any]]:
|
|
489
|
-
"""Build step summary from stored steps or events.
|
|
490
|
-
|
|
491
|
-
Returns:
|
|
492
|
-
List of step dictionaries.
|
|
493
|
-
"""
|
|
494
|
-
stored = self.ctx.meta.get("transcript_steps")
|
|
495
|
-
if isinstance(stored, list) and stored:
|
|
496
|
-
return [
|
|
497
|
-
{
|
|
498
|
-
"title": entry.get("display_name") or entry.get("name") or "Step",
|
|
499
|
-
"is_delegate": entry.get("kind") == "delegate",
|
|
500
|
-
"finished": entry.get("status") == "finished",
|
|
501
|
-
"duration": self._format_duration_from_ms(entry.get("duration_ms")),
|
|
502
|
-
}
|
|
503
|
-
for entry in stored
|
|
504
|
-
]
|
|
505
|
-
|
|
506
|
-
steps: dict[str, dict[str, Any]] = {}
|
|
507
|
-
order: list[str] = []
|
|
508
|
-
|
|
509
|
-
for event in self.ctx.events:
|
|
510
|
-
metadata = event.get("metadata") or {}
|
|
511
|
-
if not self._is_step_event(metadata):
|
|
512
|
-
continue
|
|
513
|
-
|
|
514
|
-
for name, info in self._iter_step_candidates(event, metadata):
|
|
515
|
-
step = self._ensure_step_entry(steps, order, name)
|
|
516
|
-
self._apply_step_update(step, metadata, info, event)
|
|
517
|
-
|
|
518
|
-
return [steps[name] for name in order]
|
|
519
|
-
|
|
520
|
-
def _build_tree_summary_text(self) -> Text | None:
|
|
521
|
-
"""Render hierarchical tree from captured SSE events when available."""
|
|
522
|
-
manager = StepManager()
|
|
523
|
-
processed = False
|
|
524
|
-
|
|
525
|
-
for event in self.ctx.events:
|
|
526
|
-
payload = self._coerce_step_event(event)
|
|
527
|
-
if not payload:
|
|
528
|
-
continue
|
|
529
|
-
try:
|
|
530
|
-
manager.apply_event(payload)
|
|
531
|
-
processed = True
|
|
532
|
-
except ValueError:
|
|
533
|
-
continue
|
|
534
|
-
|
|
535
|
-
if not processed or not manager.order:
|
|
536
|
-
return None
|
|
537
|
-
|
|
538
|
-
lines: list[str] = []
|
|
539
|
-
roots = manager.order
|
|
540
|
-
total_roots = len(roots)
|
|
541
|
-
for index, root_id in enumerate(roots):
|
|
542
|
-
self._render_tree_branch(
|
|
543
|
-
manager=manager,
|
|
544
|
-
step_id=root_id,
|
|
545
|
-
ancestor_state=(),
|
|
546
|
-
is_last=index == total_roots - 1,
|
|
547
|
-
lines=lines,
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
if not lines:
|
|
551
|
-
return None
|
|
552
|
-
|
|
553
|
-
self._decorate_root_presentation(manager, roots[0], lines)
|
|
554
|
-
|
|
555
|
-
return Text("\n".join(lines), style="dim")
|
|
556
|
-
|
|
557
|
-
def _render_tree_branch(
|
|
558
|
-
self,
|
|
559
|
-
*,
|
|
560
|
-
manager: StepManager,
|
|
561
|
-
step_id: str,
|
|
562
|
-
ancestor_state: tuple[bool, ...],
|
|
563
|
-
is_last: bool,
|
|
564
|
-
lines: list[str],
|
|
565
|
-
) -> None:
|
|
566
|
-
"""Recursively render a tree branch of steps.
|
|
567
|
-
|
|
568
|
-
Args:
|
|
569
|
-
manager: StepManager instance.
|
|
570
|
-
step_id: ID of step to render.
|
|
571
|
-
ancestor_state: Tuple of ancestor branch states.
|
|
572
|
-
is_last: Whether this is the last child.
|
|
573
|
-
lines: List to append rendered lines to.
|
|
574
|
-
"""
|
|
575
|
-
step = manager.by_id.get(step_id)
|
|
576
|
-
if not step:
|
|
577
|
-
return
|
|
578
|
-
suppress = self._should_hide_step(step)
|
|
579
|
-
children = manager.children.get(step_id, [])
|
|
580
|
-
|
|
581
|
-
if not suppress:
|
|
582
|
-
branch_state = ancestor_state
|
|
583
|
-
if branch_state:
|
|
584
|
-
branch_state = branch_state + (is_last,)
|
|
585
|
-
lines.append(self._format_tree_line(step, branch_state))
|
|
586
|
-
next_ancestor_state = ancestor_state + (is_last,)
|
|
587
|
-
else:
|
|
588
|
-
next_ancestor_state = ancestor_state
|
|
589
|
-
|
|
590
|
-
if not children:
|
|
591
|
-
return
|
|
592
|
-
|
|
593
|
-
total_children = len(children)
|
|
594
|
-
for idx, child_id in enumerate(children):
|
|
595
|
-
self._render_tree_branch(
|
|
596
|
-
manager=manager,
|
|
597
|
-
step_id=child_id,
|
|
598
|
-
ancestor_state=next_ancestor_state if not suppress else ancestor_state,
|
|
599
|
-
is_last=idx == total_children - 1,
|
|
600
|
-
lines=lines,
|
|
601
|
-
)
|
|
602
|
-
|
|
603
|
-
def _should_hide_step(self, step: Any) -> bool:
|
|
604
|
-
"""Check if a step should be hidden.
|
|
605
|
-
|
|
606
|
-
Args:
|
|
607
|
-
step: Step object.
|
|
608
|
-
|
|
609
|
-
Returns:
|
|
610
|
-
True if step should be hidden.
|
|
611
|
-
"""
|
|
612
|
-
if getattr(step, "parent_id", None) is None:
|
|
613
|
-
return False
|
|
614
|
-
name = getattr(step, "name", "") or ""
|
|
615
|
-
return self._looks_like_uuid(name)
|
|
616
|
-
|
|
617
|
-
def _decorate_root_presentation(
|
|
618
|
-
self,
|
|
619
|
-
manager: StepManager,
|
|
620
|
-
root_id: str,
|
|
621
|
-
lines: list[str],
|
|
622
|
-
) -> None:
|
|
623
|
-
"""Decorate root step presentation with friendly label.
|
|
624
|
-
|
|
625
|
-
Args:
|
|
626
|
-
manager: StepManager instance.
|
|
627
|
-
root_id: Root step ID.
|
|
628
|
-
lines: Lines list to modify.
|
|
629
|
-
"""
|
|
630
|
-
if not lines:
|
|
631
|
-
return
|
|
632
|
-
|
|
633
|
-
root_step = manager.by_id.get(root_id)
|
|
634
|
-
if not root_step:
|
|
635
|
-
return
|
|
636
|
-
|
|
637
|
-
original_label = getattr(root_step, "display_label", None)
|
|
638
|
-
root_step.display_label = self._friendly_root_label(root_step, original_label)
|
|
639
|
-
lines[0] = self._format_tree_line(root_step, ())
|
|
640
|
-
if original_label is not None:
|
|
641
|
-
root_step.display_label = original_label
|
|
642
|
-
|
|
643
|
-
query = self._get_user_query()
|
|
644
|
-
if query:
|
|
645
|
-
lines.insert(1, f" {query}")
|
|
646
|
-
|
|
647
|
-
def _coerce_step_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
648
|
-
"""Coerce event to step event format.
|
|
649
|
-
|
|
650
|
-
Args:
|
|
651
|
-
event: Event dictionary.
|
|
652
|
-
|
|
653
|
-
Returns:
|
|
654
|
-
Step event dictionary or None.
|
|
655
|
-
"""
|
|
656
|
-
metadata = event.get("metadata")
|
|
657
|
-
if not isinstance(metadata, dict):
|
|
658
|
-
return None
|
|
659
|
-
if not isinstance(metadata.get("step_id"), str):
|
|
660
|
-
return None
|
|
661
|
-
return {
|
|
662
|
-
"metadata": metadata,
|
|
663
|
-
"status": event.get("status"),
|
|
664
|
-
"task_state": event.get("task_state"),
|
|
665
|
-
"content": event.get("content"),
|
|
666
|
-
"task_id": event.get("task_id"),
|
|
667
|
-
"context_id": event.get("context_id"),
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
def _format_tree_line(self, step: Any, branch_state: tuple[bool, ...]) -> str:
|
|
671
|
-
"""Format a tree line for a step.
|
|
672
|
-
|
|
673
|
-
Args:
|
|
674
|
-
step: Step object.
|
|
675
|
-
branch_state: Branch state tuple.
|
|
676
|
-
|
|
677
|
-
Returns:
|
|
678
|
-
Formatted line string.
|
|
679
|
-
"""
|
|
680
|
-
prefix = build_connector_prefix(branch_state)
|
|
681
|
-
raw_label = normalise_display_label(getattr(step, "display_label", None))
|
|
682
|
-
title, summary = self._split_label(raw_label)
|
|
683
|
-
line = f"{prefix}{title}"
|
|
684
|
-
|
|
685
|
-
if summary:
|
|
686
|
-
line += f" — {self._truncate_summary(summary)}"
|
|
687
|
-
|
|
688
|
-
badge = self._format_duration_badge(step)
|
|
689
|
-
if badge:
|
|
690
|
-
line += f" {badge}"
|
|
691
|
-
|
|
692
|
-
glyph = glyph_for_status(getattr(step, "status_icon", None))
|
|
693
|
-
failure_reason = getattr(step, "failure_reason", None)
|
|
694
|
-
if glyph and glyph != "spinner":
|
|
695
|
-
if failure_reason and glyph == "✗":
|
|
696
|
-
line += f" {glyph} {failure_reason}"
|
|
697
|
-
else:
|
|
698
|
-
line += f" {glyph}"
|
|
699
|
-
elif failure_reason:
|
|
700
|
-
line += f" ✗ {failure_reason}"
|
|
701
|
-
|
|
702
|
-
return line
|
|
703
|
-
|
|
704
|
-
def _friendly_root_label(self, step: Any, fallback: str | None) -> str:
|
|
705
|
-
"""Generate friendly label for root step.
|
|
706
|
-
|
|
707
|
-
Args:
|
|
708
|
-
step: Step object.
|
|
709
|
-
fallback: Fallback label string.
|
|
710
|
-
|
|
711
|
-
Returns:
|
|
712
|
-
Friendly label string.
|
|
713
|
-
"""
|
|
714
|
-
agent_name = self.ctx.manifest_entry.get("agent_name") or (self.ctx.meta or {}).get("agent_name")
|
|
715
|
-
agent_id = self.ctx.manifest_entry.get("agent_id") or getattr(step, "name", "")
|
|
716
|
-
|
|
717
|
-
if not agent_name:
|
|
718
|
-
return fallback or agent_id or ICON_AGENT
|
|
719
|
-
|
|
720
|
-
parts = [ICON_AGENT, agent_name]
|
|
721
|
-
if agent_id and agent_id != agent_name:
|
|
722
|
-
parts.append(f"({agent_id})")
|
|
723
|
-
return " ".join(parts)
|
|
724
|
-
|
|
725
|
-
@staticmethod
|
|
726
|
-
def _format_duration_badge(step: Any) -> str | None:
|
|
727
|
-
"""Format duration badge for a step.
|
|
728
|
-
|
|
729
|
-
Args:
|
|
730
|
-
step: Step object.
|
|
731
|
-
|
|
732
|
-
Returns:
|
|
733
|
-
Duration badge string or None.
|
|
734
|
-
"""
|
|
735
|
-
duration_ms = getattr(step, "duration_ms", None)
|
|
736
|
-
if duration_ms is None:
|
|
737
|
-
return None
|
|
738
|
-
try:
|
|
739
|
-
duration_ms = int(duration_ms)
|
|
740
|
-
except Exception:
|
|
741
|
-
return None
|
|
742
|
-
|
|
743
|
-
if duration_ms <= 0:
|
|
744
|
-
payload = "<1ms"
|
|
745
|
-
elif duration_ms >= 1000:
|
|
746
|
-
payload = f"{duration_ms / 1000:.2f}s"
|
|
747
|
-
else:
|
|
748
|
-
payload = f"{duration_ms}ms"
|
|
749
|
-
|
|
750
|
-
return f"[{payload}]"
|
|
751
|
-
|
|
752
|
-
@staticmethod
|
|
753
|
-
def _split_label(label: str) -> tuple[str, str | None]:
|
|
754
|
-
"""Split label into title and summary.
|
|
755
|
-
|
|
756
|
-
Args:
|
|
757
|
-
label: Label string.
|
|
758
|
-
|
|
759
|
-
Returns:
|
|
760
|
-
Tuple of (title, summary).
|
|
761
|
-
"""
|
|
762
|
-
if " — " in label:
|
|
763
|
-
title, summary = label.split(" — ", 1)
|
|
764
|
-
return title.strip(), summary.strip()
|
|
765
|
-
return label.strip(), None
|
|
766
|
-
|
|
767
|
-
@staticmethod
|
|
768
|
-
def _truncate_summary(summary: str, limit: int = 48) -> str:
|
|
769
|
-
"""Truncate summary to specified length.
|
|
770
|
-
|
|
771
|
-
Args:
|
|
772
|
-
summary: Summary string.
|
|
773
|
-
limit: Maximum length.
|
|
774
|
-
|
|
775
|
-
Returns:
|
|
776
|
-
Truncated summary string.
|
|
777
|
-
"""
|
|
778
|
-
summary = summary.strip()
|
|
779
|
-
if len(summary) <= limit:
|
|
780
|
-
return summary
|
|
781
|
-
return summary[: limit - 1].rstrip() + "…"
|
|
782
|
-
|
|
783
|
-
@staticmethod
|
|
784
|
-
def _looks_like_uuid(value: str) -> bool:
|
|
785
|
-
"""Check if string looks like a UUID.
|
|
786
|
-
|
|
787
|
-
Args:
|
|
788
|
-
value: String to check.
|
|
789
|
-
|
|
790
|
-
Returns:
|
|
791
|
-
True if value looks like UUID.
|
|
792
|
-
"""
|
|
793
|
-
stripped = value.replace("-", "").replace(" ", "")
|
|
794
|
-
if len(stripped) not in {32, 36}:
|
|
795
|
-
return False
|
|
796
|
-
return all(ch in "0123456789abcdefABCDEF" for ch in stripped)
|
|
797
|
-
|
|
798
|
-
@staticmethod
|
|
799
|
-
def _format_duration_from_ms(value: Any) -> str | None:
|
|
800
|
-
"""Format duration from milliseconds.
|
|
801
|
-
|
|
802
|
-
Args:
|
|
803
|
-
value: Duration value in milliseconds.
|
|
804
|
-
|
|
805
|
-
Returns:
|
|
806
|
-
Formatted duration string or None.
|
|
807
|
-
"""
|
|
808
|
-
try:
|
|
809
|
-
if value is None:
|
|
810
|
-
return None
|
|
811
|
-
duration_ms = float(value)
|
|
812
|
-
except Exception:
|
|
813
|
-
return None
|
|
814
|
-
|
|
815
|
-
if duration_ms <= 0:
|
|
816
|
-
return "<1ms"
|
|
817
|
-
if duration_ms < 1000:
|
|
818
|
-
return f"{int(duration_ms)}ms"
|
|
819
|
-
return f"{duration_ms / 1000:.2f}s"
|
|
820
|
-
|
|
821
|
-
@staticmethod
|
|
822
|
-
def _is_step_event(metadata: dict[str, Any]) -> bool:
|
|
823
|
-
"""Check if metadata represents a step event.
|
|
824
|
-
|
|
825
|
-
Args:
|
|
826
|
-
metadata: Event metadata dictionary.
|
|
827
|
-
|
|
828
|
-
Returns:
|
|
829
|
-
True if metadata represents a step event.
|
|
830
|
-
"""
|
|
831
|
-
kind = metadata.get("kind")
|
|
832
|
-
return kind in {"agent_step", "agent_thinking_step"}
|
|
833
|
-
|
|
834
|
-
def _iter_step_candidates(
|
|
835
|
-
self, event: dict[str, Any], metadata: dict[str, Any]
|
|
836
|
-
) -> Iterable[tuple[str, dict[str, Any]]]:
|
|
837
|
-
"""Iterate step candidates from event.
|
|
838
|
-
|
|
839
|
-
Args:
|
|
840
|
-
event: Event dictionary.
|
|
841
|
-
metadata: Event metadata dictionary.
|
|
842
|
-
|
|
843
|
-
Yields:
|
|
844
|
-
Tuples of (step_name, step_info).
|
|
845
|
-
"""
|
|
846
|
-
tool_info = metadata.get("tool_info") or {}
|
|
847
|
-
|
|
848
|
-
yielded = False
|
|
849
|
-
for candidate in self._iter_tool_call_candidates(tool_info):
|
|
850
|
-
yielded = True
|
|
851
|
-
yield candidate
|
|
852
|
-
|
|
853
|
-
if yielded:
|
|
854
|
-
return
|
|
855
|
-
|
|
856
|
-
direct_tool = self._extract_direct_tool(tool_info)
|
|
857
|
-
if direct_tool is not None:
|
|
858
|
-
yield direct_tool
|
|
859
|
-
return
|
|
860
|
-
|
|
861
|
-
completed = self._extract_completed_name(event)
|
|
862
|
-
if completed is not None:
|
|
863
|
-
yield completed, {}
|
|
864
|
-
|
|
865
|
-
@staticmethod
|
|
866
|
-
def _iter_tool_call_candidates(
|
|
867
|
-
tool_info: dict[str, Any],
|
|
868
|
-
) -> Iterable[tuple[str, dict[str, Any]]]:
|
|
869
|
-
"""Iterate tool call candidates from tool_info.
|
|
870
|
-
|
|
871
|
-
Args:
|
|
872
|
-
tool_info: Tool info dictionary.
|
|
873
|
-
|
|
874
|
-
Yields:
|
|
875
|
-
Tuples of (tool_name, tool_call_info).
|
|
876
|
-
"""
|
|
877
|
-
tool_calls = tool_info.get("tool_calls")
|
|
878
|
-
if isinstance(tool_calls, list):
|
|
879
|
-
for call in tool_calls:
|
|
880
|
-
name = call.get("name")
|
|
881
|
-
if name:
|
|
882
|
-
yield name, call
|
|
883
|
-
|
|
884
268
|
@staticmethod
|
|
885
269
|
def _extract_direct_tool(
|
|
886
270
|
tool_info: dict[str, Any],
|
|
@@ -975,49 +359,6 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
975
359
|
if duration is not None:
|
|
976
360
|
step["duration"] = duration
|
|
977
361
|
|
|
978
|
-
@staticmethod
|
|
979
|
-
def _is_step_finished(metadata: dict[str, Any], event: dict[str, Any]) -> bool:
|
|
980
|
-
"""Check if step is finished.
|
|
981
|
-
|
|
982
|
-
Args:
|
|
983
|
-
metadata: Event metadata.
|
|
984
|
-
event: Event dictionary.
|
|
985
|
-
|
|
986
|
-
Returns:
|
|
987
|
-
True if step is finished.
|
|
988
|
-
"""
|
|
989
|
-
status = metadata.get("status")
|
|
990
|
-
return status == "finished" or bool(event.get("final"))
|
|
991
|
-
|
|
992
|
-
def _compute_step_duration(
|
|
993
|
-
self, step: dict[str, Any], info: dict[str, Any], metadata: dict[str, Any]
|
|
994
|
-
) -> str | None:
|
|
995
|
-
"""Calculate a formatted duration string for a step if possible."""
|
|
996
|
-
event_time = metadata.get("time")
|
|
997
|
-
started_at = step.get("started_at")
|
|
998
|
-
duration_value: float | None = None
|
|
999
|
-
|
|
1000
|
-
if isinstance(event_time, (int, float)) and isinstance(started_at, (int, float)):
|
|
1001
|
-
try:
|
|
1002
|
-
delta = float(event_time) - float(started_at)
|
|
1003
|
-
if delta >= 0:
|
|
1004
|
-
duration_value = delta
|
|
1005
|
-
except Exception:
|
|
1006
|
-
duration_value = None
|
|
1007
|
-
|
|
1008
|
-
if duration_value is None:
|
|
1009
|
-
exec_time = info.get("execution_time")
|
|
1010
|
-
if isinstance(exec_time, (int, float)):
|
|
1011
|
-
duration_value = float(exec_time)
|
|
1012
|
-
|
|
1013
|
-
if duration_value is None:
|
|
1014
|
-
return None
|
|
1015
|
-
|
|
1016
|
-
try:
|
|
1017
|
-
return format_elapsed_time(duration_value)
|
|
1018
|
-
except Exception:
|
|
1019
|
-
return None
|
|
1020
|
-
|
|
1021
362
|
|
|
1022
363
|
def run_viewer_session(
|
|
1023
364
|
console: Console,
|