glaip-sdk 0.0.20__py3-none-any.whl → 0.1.1__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/_version.py +1 -3
- glaip_sdk/branding.py +2 -6
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +11 -30
- glaip_sdk/cli/commands/agents.py +64 -107
- glaip_sdk/cli/commands/configure.py +12 -36
- glaip_sdk/cli/commands/mcps.py +25 -63
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/tools.py +22 -35
- glaip_sdk/cli/commands/update.py +3 -8
- glaip_sdk/cli/config.py +1 -3
- glaip_sdk/cli/display.py +4 -12
- glaip_sdk/cli/io.py +8 -14
- glaip_sdk/cli/main.py +10 -30
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +3 -9
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +3 -9
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/agent_session.py +5 -10
- glaip_sdk/cli/slash/prompt.py +3 -10
- glaip_sdk/cli/slash/session.py +46 -98
- glaip_sdk/cli/transcript/cache.py +6 -19
- glaip_sdk/cli/transcript/capture.py +6 -20
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +187 -46
- glaip_sdk/cli/update_notifier.py +165 -21
- glaip_sdk/cli/utils.py +33 -85
- glaip_sdk/cli/validators.py +11 -12
- glaip_sdk/client/_agent_payloads.py +10 -30
- glaip_sdk/client/agents.py +33 -63
- glaip_sdk/client/base.py +6 -22
- glaip_sdk/client/mcps.py +1 -3
- glaip_sdk/client/run_rendering.py +121 -24
- glaip_sdk/client/tools.py +8 -24
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/icons.py +9 -3
- glaip_sdk/models.py +14 -33
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/client_utils.py +7 -21
- glaip_sdk/utils/display.py +2 -6
- glaip_sdk/utils/general.py +1 -3
- glaip_sdk/utils/import_export.py +3 -9
- glaip_sdk/utils/rendering/formatting.py +52 -12
- glaip_sdk/utils/rendering/models.py +17 -8
- glaip_sdk/utils/rendering/renderer/__init__.py +1 -5
- glaip_sdk/utils/rendering/renderer/base.py +1107 -320
- glaip_sdk/utils/rendering/renderer/config.py +3 -5
- glaip_sdk/utils/rendering/renderer/debug.py +4 -14
- glaip_sdk/utils/rendering/renderer/panels.py +1 -3
- glaip_sdk/utils/rendering/renderer/progress.py +3 -11
- glaip_sdk/utils/rendering/renderer/stream.py +10 -22
- glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
- glaip_sdk/utils/rendering/step_tree_state.py +100 -0
- glaip_sdk/utils/rendering/steps.py +899 -25
- glaip_sdk/utils/resource_refs.py +4 -13
- glaip_sdk/utils/serialization.py +14 -46
- glaip_sdk/utils/validation.py +4 -4
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/METADATA +12 -1
- glaip_sdk-0.1.1.dist-info/RECORD +82 -0
- glaip_sdk-0.0.20.dist-info/RECORD +0 -80
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/entry_points.txt +0 -0
|
@@ -124,9 +124,7 @@ def _json_default(value: Any) -> Any:
|
|
|
124
124
|
return repr(value)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def _write_manifest(
|
|
128
|
-
entries: Iterable[dict[str, Any]], cache_dir: Path | None = None
|
|
129
|
-
) -> None:
|
|
127
|
+
def _write_manifest(entries: Iterable[dict[str, Any]], cache_dir: Path | None = None) -> None:
|
|
130
128
|
path = manifest_path(cache_dir)
|
|
131
129
|
with path.open("w", encoding="utf-8") as fh:
|
|
132
130
|
for entry in entries:
|
|
@@ -212,8 +210,7 @@ def latest_manifest_entry(cache_dir: Path | None = None) -> dict[str, Any] | Non
|
|
|
212
210
|
return None
|
|
213
211
|
return max(
|
|
214
212
|
entries,
|
|
215
|
-
key=lambda e: _parse_iso(e.get("created_at"))
|
|
216
|
-
or datetime.min.replace(tzinfo=timezone.utc),
|
|
213
|
+
key=lambda e: _parse_iso(e.get("created_at")) or datetime.min.replace(tzinfo=timezone.utc),
|
|
217
214
|
)
|
|
218
215
|
|
|
219
216
|
|
|
@@ -237,11 +234,7 @@ def export_transcript(
|
|
|
237
234
|
) -> Path:
|
|
238
235
|
"""Copy a cached transcript to the requested destination path."""
|
|
239
236
|
directory = ensure_cache_dir(cache_dir)
|
|
240
|
-
entry = (
|
|
241
|
-
resolve_manifest_entry(run_id, directory)
|
|
242
|
-
if run_id
|
|
243
|
-
else latest_manifest_entry(directory)
|
|
244
|
-
)
|
|
237
|
+
entry = resolve_manifest_entry(run_id, directory) if run_id else latest_manifest_entry(directory)
|
|
245
238
|
if entry is None:
|
|
246
239
|
raise FileNotFoundError("No cached transcripts available for export.")
|
|
247
240
|
|
|
@@ -259,9 +252,7 @@ def export_transcript(
|
|
|
259
252
|
lines = cache_file.read_text(encoding="utf-8").splitlines()
|
|
260
253
|
records = [json.loads(line) for line in lines if line.strip()]
|
|
261
254
|
except json.JSONDecodeError as exc:
|
|
262
|
-
raise FileNotFoundError(
|
|
263
|
-
f"Cached transcript file is corrupted: {cache_file}"
|
|
264
|
-
) from exc
|
|
255
|
+
raise FileNotFoundError(f"Cached transcript file is corrupted: {cache_file}") from exc
|
|
265
256
|
|
|
266
257
|
with destination.open("w", encoding="utf-8") as fh:
|
|
267
258
|
for idx, record in enumerate(records):
|
|
@@ -276,12 +267,8 @@ def export_transcript(
|
|
|
276
267
|
def suggest_filename(entry: dict[str, Any] | None = None) -> str:
|
|
277
268
|
"""Return a friendly filename suggestion for exporting a transcript."""
|
|
278
269
|
run_id = entry.get("run_id") if entry else uuid.uuid4().hex
|
|
279
|
-
created_at = (
|
|
280
|
-
|
|
281
|
-
)
|
|
282
|
-
timestamp = (
|
|
283
|
-
created_at.replace(":", "").replace("-", "").replace("T", "_").split("+")[0]
|
|
284
|
-
)
|
|
270
|
+
created_at = entry.get("created_at") if entry else datetime.now(timezone.utc).isoformat()
|
|
271
|
+
timestamp = created_at.replace(":", "").replace("-", "").replace("T", "_").split("+")[0]
|
|
285
272
|
return f"aip-run-{timestamp}-{run_id}.jsonl"
|
|
286
273
|
|
|
287
274
|
|
|
@@ -65,11 +65,7 @@ def compute_finished_at(renderer: Any) -> float | None:
|
|
|
65
65
|
|
|
66
66
|
if started_at is None:
|
|
67
67
|
stream_processor = getattr(renderer, "stream_processor", None)
|
|
68
|
-
started_at = (
|
|
69
|
-
getattr(stream_processor, "streaming_started_at", None)
|
|
70
|
-
if stream_processor is not None
|
|
71
|
-
else None
|
|
72
|
-
)
|
|
68
|
+
started_at = getattr(stream_processor, "streaming_started_at", None) if stream_processor is not None else None
|
|
73
69
|
if started_at is None or duration is None:
|
|
74
70
|
return None
|
|
75
71
|
try:
|
|
@@ -78,9 +74,7 @@ def compute_finished_at(renderer: Any) -> float | None:
|
|
|
78
74
|
return None
|
|
79
75
|
|
|
80
76
|
|
|
81
|
-
def extract_server_run_id(
|
|
82
|
-
meta: dict[str, Any], events: list[dict[str, Any]]
|
|
83
|
-
) -> str | None:
|
|
77
|
+
def extract_server_run_id(meta: dict[str, Any], events: list[dict[str, Any]]) -> str | None:
|
|
84
78
|
"""Derive a server-side run identifier from renderer metadata."""
|
|
85
79
|
run_id = meta.get("run_id") or meta.get("id")
|
|
86
80
|
if run_id:
|
|
@@ -107,9 +101,7 @@ def _coerce_meta(meta: Any) -> dict[str, Any]:
|
|
|
107
101
|
return {"value": coerce_result_text(meta)}
|
|
108
102
|
|
|
109
103
|
|
|
110
|
-
def register_last_transcript(
|
|
111
|
-
ctx: Any, payload: TranscriptPayload, store_result: TranscriptStoreResult
|
|
112
|
-
) -> None:
|
|
104
|
+
def register_last_transcript(ctx: Any, payload: TranscriptPayload, store_result: TranscriptStoreResult) -> None:
|
|
113
105
|
"""Persist last-run transcript references onto the Click context."""
|
|
114
106
|
ctx_obj = getattr(ctx, "obj", None)
|
|
115
107
|
if not isinstance(ctx_obj, dict):
|
|
@@ -213,9 +205,7 @@ def _derive_transcript_meta(
|
|
|
213
205
|
|
|
214
206
|
stream_processor = getattr(renderer, "stream_processor", None)
|
|
215
207
|
stream_started_at = (
|
|
216
|
-
getattr(stream_processor, "streaming_started_at", None)
|
|
217
|
-
if stream_processor is not None
|
|
218
|
-
else None
|
|
208
|
+
getattr(stream_processor, "streaming_started_at", None) if stream_processor is not None else None
|
|
219
209
|
)
|
|
220
210
|
finished_at = compute_finished_at(renderer)
|
|
221
211
|
model_name = meta.get("model") or model
|
|
@@ -236,16 +226,12 @@ def store_transcript_for_session(
|
|
|
236
226
|
if not hasattr(renderer, "get_transcript_events"):
|
|
237
227
|
return None
|
|
238
228
|
|
|
239
|
-
events, aggregated_output, final_output = _collect_renderer_outputs(
|
|
240
|
-
renderer, final_result
|
|
241
|
-
)
|
|
229
|
+
events, aggregated_output, final_output = _collect_renderer_outputs(renderer, final_result)
|
|
242
230
|
|
|
243
231
|
if not (events or aggregated_output or final_output):
|
|
244
232
|
return None
|
|
245
233
|
|
|
246
|
-
meta, stream_started_at, finished_at, model_name = _derive_transcript_meta(
|
|
247
|
-
renderer, model
|
|
248
|
-
)
|
|
234
|
+
meta, stream_started_at, finished_at, model_name = _derive_transcript_meta(renderer, model)
|
|
249
235
|
|
|
250
236
|
payload: TranscriptPayload = build_transcript_payload(
|
|
251
237
|
events=events,
|
|
@@ -20,9 +20,7 @@ from glaip_sdk.cli.transcript.capture import StoredTranscriptContext
|
|
|
20
20
|
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def should_launch_post_run_viewer(
|
|
24
|
-
ctx: Any, console: Console, *, slash_mode: bool
|
|
25
|
-
) -> bool:
|
|
23
|
+
def should_launch_post_run_viewer(ctx: Any, console: Console, *, slash_mode: bool) -> bool:
|
|
26
24
|
"""Return True if the viewer should open automatically."""
|
|
27
25
|
if slash_mode:
|
|
28
26
|
return False
|
|
@@ -27,12 +27,18 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
27
27
|
from glaip_sdk.cli.transcript.cache import suggest_filename
|
|
28
28
|
from glaip_sdk.icons import ICON_DELEGATE, ICON_TOOL_STEP
|
|
29
29
|
from glaip_sdk.rich_components import AIPPanel
|
|
30
|
+
from glaip_sdk.utils.rendering.formatting import (
|
|
31
|
+
build_connector_prefix,
|
|
32
|
+
glyph_for_status,
|
|
33
|
+
normalise_display_label,
|
|
34
|
+
)
|
|
30
35
|
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
31
36
|
from glaip_sdk.utils.rendering.renderer.panels import create_final_panel
|
|
32
37
|
from glaip_sdk.utils.rendering.renderer.progress import (
|
|
33
38
|
format_elapsed_time,
|
|
34
39
|
is_delegation_tool,
|
|
35
40
|
)
|
|
41
|
+
from glaip_sdk.utils.rendering.steps import StepManager
|
|
36
42
|
|
|
37
43
|
EXPORT_CANCELLED_MESSAGE = "[dim]Export cancelled.[/dim]"
|
|
38
44
|
|
|
@@ -66,9 +72,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
66
72
|
|
|
67
73
|
def run(self) -> None:
|
|
68
74
|
"""Enter the interactive loop."""
|
|
69
|
-
if not self.ctx.events and not (
|
|
70
|
-
self.ctx.default_output or self.ctx.final_output
|
|
71
|
-
):
|
|
75
|
+
if not self.ctx.events and not (self.ctx.default_output or self.ctx.final_output):
|
|
72
76
|
return
|
|
73
77
|
if self._view_mode == "transcript":
|
|
74
78
|
self._render()
|
|
@@ -85,9 +89,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
85
89
|
except Exception: # pragma: no cover - platform quirks
|
|
86
90
|
pass
|
|
87
91
|
|
|
88
|
-
header = (
|
|
89
|
-
f"Agent transcript viewer · run {self.ctx.manifest_entry.get('run_id')}"
|
|
90
|
-
)
|
|
92
|
+
header = f"Agent transcript viewer · run {self.ctx.manifest_entry.get('run_id')}"
|
|
91
93
|
agent_label = self.ctx.manifest_entry.get("agent_name") or "unknown agent"
|
|
92
94
|
model = self.ctx.manifest_entry.get("model") or self.ctx.meta.get("model")
|
|
93
95
|
agent_id = self.ctx.manifest_entry.get("agent_id")
|
|
@@ -128,9 +130,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
128
130
|
self._render_final_panel()
|
|
129
131
|
|
|
130
132
|
self.console.print("[bold]Transcript Events[/bold]")
|
|
131
|
-
self.console.print(
|
|
132
|
-
"[dim]────────────────────────────────────────────────────────[/dim]"
|
|
133
|
-
)
|
|
133
|
+
self.console.print("[dim]────────────────────────────────────────────────────────[/dim]")
|
|
134
134
|
|
|
135
135
|
base_received_ts: datetime | None = None
|
|
136
136
|
for event in self.ctx.events:
|
|
@@ -146,11 +146,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
146
146
|
self.console.print()
|
|
147
147
|
|
|
148
148
|
def _render_final_panel(self) -> None:
|
|
149
|
-
content =
|
|
150
|
-
self.ctx.final_output
|
|
151
|
-
or self.ctx.default_output
|
|
152
|
-
or "No response content captured."
|
|
153
|
-
)
|
|
149
|
+
content = self.ctx.final_output or self.ctx.default_output or "No response content captured."
|
|
154
150
|
title = "Final Result"
|
|
155
151
|
duration_text = self._extract_final_duration()
|
|
156
152
|
if duration_text:
|
|
@@ -212,9 +208,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
212
208
|
raw = str(path)
|
|
213
209
|
return raw if len(raw) <= 80 else f"…{raw[-77:]}"
|
|
214
210
|
|
|
215
|
-
selection = self._prompt_export_choice(
|
|
216
|
-
default_path, _display_path(default_path)
|
|
217
|
-
)
|
|
211
|
+
selection = self._prompt_export_choice(default_path, _display_path(default_path))
|
|
218
212
|
if selection is None:
|
|
219
213
|
self._legacy_export_prompt(default_path, _display_path)
|
|
220
214
|
return
|
|
@@ -240,9 +234,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
240
234
|
except Exception as exc: # pragma: no cover - unexpected IO failures
|
|
241
235
|
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
242
236
|
|
|
243
|
-
def _prompt_export_choice(
|
|
244
|
-
self, default_path: Path, default_display: str
|
|
245
|
-
) -> tuple[str, Any] | None:
|
|
237
|
+
def _prompt_export_choice(self, default_path: Path, default_display: str) -> tuple[str, Any] | None:
|
|
246
238
|
"""Render interactive export menu with numeric shortcuts."""
|
|
247
239
|
if not self.console.is_terminal or questionary is None or Choice is None:
|
|
248
240
|
return None
|
|
@@ -299,9 +291,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
299
291
|
candidate = Path.cwd() / candidate
|
|
300
292
|
return candidate
|
|
301
293
|
|
|
302
|
-
def _legacy_export_prompt(
|
|
303
|
-
self, default_path: Path, formatter: Callable[[Path], str]
|
|
304
|
-
) -> None:
|
|
294
|
+
def _legacy_export_prompt(self, default_path: Path, formatter: Callable[[Path], str]) -> None:
|
|
305
295
|
"""Fallback export workflow when interactive UI is unavailable."""
|
|
306
296
|
self.console.print("[dim]Export options (fallback mode)[/dim]")
|
|
307
297
|
self.console.print(f" 1. Save to default ({formatter(default_path)})")
|
|
@@ -347,20 +337,13 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
347
337
|
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
348
338
|
|
|
349
339
|
def _print_command_hint(self) -> None:
|
|
350
|
-
self.console.print(
|
|
351
|
-
"[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]"
|
|
352
|
-
)
|
|
340
|
+
self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
|
|
353
341
|
self.console.print()
|
|
354
342
|
|
|
355
343
|
def _get_user_query(self) -> str | None:
|
|
356
344
|
meta = self.ctx.meta or {}
|
|
357
345
|
manifest = self.ctx.manifest_entry or {}
|
|
358
|
-
return (
|
|
359
|
-
meta.get("input_message")
|
|
360
|
-
or meta.get("query")
|
|
361
|
-
or meta.get("message")
|
|
362
|
-
or manifest.get("input_message")
|
|
363
|
-
)
|
|
346
|
+
return meta.get("input_message") or meta.get("query") or meta.get("message") or manifest.get("input_message")
|
|
364
347
|
|
|
365
348
|
def _render_user_query(self, query: str) -> None:
|
|
366
349
|
panel = AIPPanel(
|
|
@@ -372,12 +355,13 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
372
355
|
self.console.print()
|
|
373
356
|
|
|
374
357
|
def _render_steps_summary(self) -> None:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
358
|
+
tree_text = self._build_tree_summary_text()
|
|
359
|
+
if tree_text is not None:
|
|
360
|
+
body = tree_text
|
|
361
|
+
else:
|
|
362
|
+
panel_content = self._format_steps_summary(self._build_step_summary())
|
|
363
|
+
body = Text(panel_content, style="dim")
|
|
364
|
+
panel = AIPPanel(body, title="Steps", border_style="blue")
|
|
381
365
|
self.console.print(panel)
|
|
382
366
|
self.console.print()
|
|
383
367
|
|
|
@@ -460,6 +444,169 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
460
444
|
|
|
461
445
|
return [steps[name] for name in order]
|
|
462
446
|
|
|
447
|
+
def _build_tree_summary_text(self) -> Text | None:
|
|
448
|
+
"""Render hierarchical tree from captured SSE events when available."""
|
|
449
|
+
manager = StepManager()
|
|
450
|
+
processed = False
|
|
451
|
+
|
|
452
|
+
for event in self.ctx.events:
|
|
453
|
+
payload = self._coerce_step_event(event)
|
|
454
|
+
if not payload:
|
|
455
|
+
continue
|
|
456
|
+
try:
|
|
457
|
+
manager.apply_event(payload)
|
|
458
|
+
processed = True
|
|
459
|
+
except ValueError:
|
|
460
|
+
continue
|
|
461
|
+
|
|
462
|
+
if not processed or not manager.order:
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
lines: list[str] = []
|
|
466
|
+
roots = manager.order
|
|
467
|
+
total_roots = len(roots)
|
|
468
|
+
for index, root_id in enumerate(roots):
|
|
469
|
+
self._render_tree_branch(
|
|
470
|
+
manager=manager,
|
|
471
|
+
step_id=root_id,
|
|
472
|
+
ancestor_state=(),
|
|
473
|
+
is_last=index == total_roots - 1,
|
|
474
|
+
lines=lines,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if not lines:
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
return Text("\n".join(lines), style="dim")
|
|
481
|
+
|
|
482
|
+
def _render_tree_branch(
|
|
483
|
+
self,
|
|
484
|
+
*,
|
|
485
|
+
manager: StepManager,
|
|
486
|
+
step_id: str,
|
|
487
|
+
ancestor_state: tuple[bool, ...],
|
|
488
|
+
is_last: bool,
|
|
489
|
+
lines: list[str],
|
|
490
|
+
) -> None:
|
|
491
|
+
step = manager.by_id.get(step_id)
|
|
492
|
+
if not step:
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
suppress = self._should_hide_step(step)
|
|
496
|
+
children = manager.children.get(step_id, [])
|
|
497
|
+
|
|
498
|
+
if not suppress:
|
|
499
|
+
branch_state = ancestor_state
|
|
500
|
+
if branch_state:
|
|
501
|
+
branch_state = branch_state + (is_last,)
|
|
502
|
+
lines.append(self._format_tree_line(step, branch_state))
|
|
503
|
+
next_ancestor_state = ancestor_state + (is_last,)
|
|
504
|
+
else:
|
|
505
|
+
next_ancestor_state = ancestor_state
|
|
506
|
+
|
|
507
|
+
if not children:
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
total_children = len(children)
|
|
511
|
+
for idx, child_id in enumerate(children):
|
|
512
|
+
self._render_tree_branch(
|
|
513
|
+
manager=manager,
|
|
514
|
+
step_id=child_id,
|
|
515
|
+
ancestor_state=next_ancestor_state if not suppress else ancestor_state,
|
|
516
|
+
is_last=idx == total_children - 1,
|
|
517
|
+
lines=lines,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
def _should_hide_step(self, step: Any) -> bool:
|
|
521
|
+
if getattr(step, "parent_id", None) is not None:
|
|
522
|
+
return False
|
|
523
|
+
if getattr(step, "kind", None) == "thinking":
|
|
524
|
+
return True
|
|
525
|
+
if getattr(step, "kind", None) == "agent":
|
|
526
|
+
return True
|
|
527
|
+
name = getattr(step, "name", "") or ""
|
|
528
|
+
return self._looks_like_uuid(name)
|
|
529
|
+
|
|
530
|
+
def _coerce_step_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
531
|
+
metadata = event.get("metadata")
|
|
532
|
+
if not isinstance(metadata, dict):
|
|
533
|
+
return None
|
|
534
|
+
if not isinstance(metadata.get("step_id"), str):
|
|
535
|
+
return None
|
|
536
|
+
return {
|
|
537
|
+
"metadata": metadata,
|
|
538
|
+
"status": event.get("status"),
|
|
539
|
+
"task_state": event.get("task_state"),
|
|
540
|
+
"content": event.get("content"),
|
|
541
|
+
"task_id": event.get("task_id"),
|
|
542
|
+
"context_id": event.get("context_id"),
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
def _format_tree_line(self, step: Any, branch_state: tuple[bool, ...]) -> str:
|
|
546
|
+
prefix = build_connector_prefix(branch_state)
|
|
547
|
+
raw_label = normalise_display_label(getattr(step, "display_label", None))
|
|
548
|
+
title, summary = self._split_label(raw_label)
|
|
549
|
+
line = f"{prefix}{title}"
|
|
550
|
+
|
|
551
|
+
if summary:
|
|
552
|
+
line += f" — {self._truncate_summary(summary)}"
|
|
553
|
+
|
|
554
|
+
badge = self._format_duration_badge(step)
|
|
555
|
+
if badge:
|
|
556
|
+
line += f" {badge}"
|
|
557
|
+
|
|
558
|
+
glyph = glyph_for_status(getattr(step, "status_icon", None))
|
|
559
|
+
failure_reason = getattr(step, "failure_reason", None)
|
|
560
|
+
if glyph and glyph != "spinner":
|
|
561
|
+
if failure_reason and glyph == "✗":
|
|
562
|
+
line += f" {glyph} {failure_reason}"
|
|
563
|
+
else:
|
|
564
|
+
line += f" {glyph}"
|
|
565
|
+
elif failure_reason:
|
|
566
|
+
line += f" ✗ {failure_reason}"
|
|
567
|
+
|
|
568
|
+
return line
|
|
569
|
+
|
|
570
|
+
@staticmethod
|
|
571
|
+
def _format_duration_badge(step: Any) -> str | None:
|
|
572
|
+
duration_ms = getattr(step, "duration_ms", None)
|
|
573
|
+
if duration_ms is None:
|
|
574
|
+
return None
|
|
575
|
+
try:
|
|
576
|
+
duration_ms = int(duration_ms)
|
|
577
|
+
except Exception:
|
|
578
|
+
return None
|
|
579
|
+
|
|
580
|
+
if duration_ms <= 0:
|
|
581
|
+
payload = "<1ms"
|
|
582
|
+
elif duration_ms >= 1000:
|
|
583
|
+
payload = f"{duration_ms / 1000:.2f}s"
|
|
584
|
+
else:
|
|
585
|
+
payload = f"{duration_ms}ms"
|
|
586
|
+
|
|
587
|
+
return f"[{payload}]"
|
|
588
|
+
|
|
589
|
+
@staticmethod
|
|
590
|
+
def _split_label(label: str) -> tuple[str, str | None]:
|
|
591
|
+
if " — " in label:
|
|
592
|
+
title, summary = label.split(" — ", 1)
|
|
593
|
+
return title.strip(), summary.strip()
|
|
594
|
+
return label.strip(), None
|
|
595
|
+
|
|
596
|
+
@staticmethod
|
|
597
|
+
def _truncate_summary(summary: str, limit: int = 48) -> str:
|
|
598
|
+
summary = summary.strip()
|
|
599
|
+
if len(summary) <= limit:
|
|
600
|
+
return summary
|
|
601
|
+
return summary[: limit - 1].rstrip() + "…"
|
|
602
|
+
|
|
603
|
+
@staticmethod
|
|
604
|
+
def _looks_like_uuid(value: str) -> bool:
|
|
605
|
+
stripped = value.replace("-", "")
|
|
606
|
+
if len(stripped) not in {32, 36}:
|
|
607
|
+
return False
|
|
608
|
+
return all(ch in "0123456789abcdefABCDEF" for ch in stripped)
|
|
609
|
+
|
|
463
610
|
@staticmethod
|
|
464
611
|
def _format_duration_from_ms(value: Any) -> str | None:
|
|
465
612
|
try:
|
|
@@ -560,11 +707,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
560
707
|
status = metadata.get("status")
|
|
561
708
|
event_time = metadata.get("time")
|
|
562
709
|
|
|
563
|
-
if (
|
|
564
|
-
status == "running"
|
|
565
|
-
and step.get("started_at") is None
|
|
566
|
-
and isinstance(event_time, (int, float))
|
|
567
|
-
):
|
|
710
|
+
if status == "running" and step.get("started_at") is None and isinstance(event_time, (int, float)):
|
|
568
711
|
try:
|
|
569
712
|
step["started_at"] = float(event_time)
|
|
570
713
|
except Exception:
|
|
@@ -590,9 +733,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
590
733
|
started_at = step.get("started_at")
|
|
591
734
|
duration_value: float | None = None
|
|
592
735
|
|
|
593
|
-
if isinstance(event_time, (int, float)) and isinstance(
|
|
594
|
-
started_at, (int, float)
|
|
595
|
-
):
|
|
736
|
+
if isinstance(event_time, (int, float)) and isinstance(started_at, (int, float)):
|
|
596
737
|
try:
|
|
597
738
|
delta = float(event_time) - float(started_at)
|
|
598
739
|
if delta >= 0:
|