glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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.
Files changed (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -6,16 +6,12 @@ Authors:
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from collections.abc import Callable, Iterable
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,21 @@ 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.icons import ICON_DELEGATE, ICON_TOOL_STEP
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,
24
+ from glaip_sdk.cli.core.prompting 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.layout.transcript import DEFAULT_TRANSCRIPT_THEME
27
+ from glaip_sdk.utils.rendering.viewer import (
28
+ ViewerContext as PresenterViewerContext,
29
+ prepare_viewer_snapshot as presenter_prepare_viewer_snapshot,
30
+ render_post_run_view as presenter_render_post_run_view,
31
+ render_transcript_events as presenter_render_transcript_events,
32
+ render_transcript_view as presenter_render_transcript_view,
34
33
  )
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
34
 
43
35
  EXPORT_CANCELLED_MESSAGE = "[dim]Export cancelled.[/dim]"
44
36
 
45
37
 
46
- @dataclass(slots=True)
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]
38
+ ViewerContext = PresenterViewerContext
56
39
 
57
40
 
58
41
  class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
@@ -63,12 +46,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
63
46
  console: Console,
64
47
  ctx: ViewerContext,
65
48
  export_callback: Callable[[Path], Path],
49
+ *,
50
+ initial_view: str = "default",
66
51
  ) -> None:
67
52
  """Initialize viewer state for a captured transcript."""
68
53
  self.console = console
69
54
  self.ctx = ctx
70
55
  self._export_callback = export_callback
71
- self._view_mode = "default"
56
+ self._view_mode = initial_view if initial_view in {"default", "transcript"} else "default"
72
57
 
73
58
  def run(self) -> None:
74
59
  """Enter the interactive loop."""
@@ -83,6 +68,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
83
68
  # Rendering helpers
84
69
  # ------------------------------------------------------------------
85
70
  def _render(self) -> None:
71
+ """Render the transcript viewer interface."""
86
72
  try:
87
73
  if self.console.is_terminal:
88
74
  self.console.clear()
@@ -105,60 +91,19 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
105
91
  self.console.print(f"[dim]{' · '.join(subtitle_parts)}[/]")
106
92
  self.console.print()
107
93
 
108
- query = self._get_user_query()
109
-
110
94
  if self._view_mode == "default":
111
- self._render_default_view(query)
95
+ presenter_render_post_run_view(self.console, self.ctx)
112
96
  else:
113
- self._render_transcript_view(query)
114
-
115
- def _render_default_view(self, query: str | None) -> None:
116
- if query:
117
- self._render_user_query(query)
118
- self._render_steps_summary()
119
- self._render_final_panel()
120
-
121
- def _render_transcript_view(self, query: str | None) -> None:
122
- if not self.ctx.events:
123
- self.console.print("[dim]No SSE events were captured for this run.[/dim]")
124
- return
125
-
126
- if query:
127
- self._render_user_query(query)
128
-
129
- self._render_steps_summary()
130
- self._render_final_panel()
131
-
132
- self.console.print("[bold]Transcript Events[/bold]")
133
- self.console.print("[dim]────────────────────────────────────────────────────────[/dim]")
134
-
135
- base_received_ts: datetime | None = None
136
- for event in self.ctx.events:
137
- received_ts = self._parse_received_timestamp(event)
138
- if base_received_ts is None and received_ts is not None:
139
- base_received_ts = received_ts
140
- render_debug_event(
141
- event,
142
- self.console,
143
- received_ts=received_ts,
144
- baseline_ts=base_received_ts,
145
- )
146
- self.console.print()
147
-
148
- def _render_final_panel(self) -> None:
149
- content = self.ctx.final_output or self.ctx.default_output or "No response content captured."
150
- title = "Final Result"
151
- duration_text = self._extract_final_duration()
152
- if duration_text:
153
- title += f" · {duration_text}"
154
- panel = create_final_panel(content, title=title, theme="dark")
155
- self.console.print(panel)
156
- self.console.print()
97
+ theme = DEFAULT_TRANSCRIPT_THEME
98
+ snapshot, state = presenter_prepare_viewer_snapshot(self.ctx, glyphs=None, theme=theme)
99
+ presenter_render_transcript_view(self.console, snapshot, theme=theme)
100
+ presenter_render_transcript_events(self.console, state.events)
157
101
 
158
102
  # ------------------------------------------------------------------
159
103
  # Interaction loops
160
104
  # ------------------------------------------------------------------
161
105
  def _fallback_loop(self) -> None:
106
+ """Fallback interaction loop for non-interactive terminals."""
162
107
  while True:
163
108
  try:
164
109
  ch = click.getchar()
@@ -179,6 +124,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
179
124
  continue
180
125
 
181
126
  def _handle_command(self, raw: str) -> bool:
127
+ """Handle a command input.
128
+
129
+ Args:
130
+ raw: Raw command string.
131
+
132
+ Returns:
133
+ True to continue, False to exit.
134
+ """
182
135
  lowered = raw.lower()
183
136
  if lowered in {"exit", "quit", "q"}:
184
137
  return True
@@ -236,38 +189,10 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
236
189
 
237
190
  def _prompt_export_choice(self, default_path: Path, default_display: str) -> tuple[str, Any] | None:
238
191
  """Render interactive export menu with numeric shortcuts."""
239
- if not self.console.is_terminal or questionary is None or Choice is None:
240
- return None
241
-
242
- try:
243
- answer = questionary.select(
244
- "Export transcript",
245
- choices=[
246
- Choice(
247
- title=f"Save to default ({default_display})",
248
- value=("default", default_path),
249
- shortcut_key="1",
250
- ),
251
- Choice(
252
- title="Choose a different path",
253
- value=("custom", None),
254
- shortcut_key="2",
255
- ),
256
- Choice(
257
- title="Cancel",
258
- value=("cancel", None),
259
- shortcut_key="3",
260
- ),
261
- ],
262
- use_shortcuts=True,
263
- instruction="Press 1-3 (or arrows) then Enter.",
264
- ).ask()
265
- except Exception:
192
+ if not self.console.is_terminal:
266
193
  return None
267
194
 
268
- if answer is None:
269
- return ("cancel", None)
270
- return answer
195
+ return prompt_export_choice_questionary(default_path, default_display)
271
196
 
272
197
  def _prompt_custom_destination(self) -> Path | None:
273
198
  """Prompt for custom export path with filesystem completion."""
@@ -275,11 +200,12 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
275
200
  return None
276
201
 
277
202
  try:
278
- response = questionary.path(
203
+ question = questionary.path(
279
204
  "Destination path (Tab to autocomplete):",
280
205
  default="",
281
206
  only_directories=False,
282
- ).ask()
207
+ )
208
+ response = questionary_safe_ask(question)
283
209
  except Exception:
284
210
  return None
285
211
 
@@ -337,333 +263,22 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
337
263
  self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
338
264
 
339
265
  def _print_command_hint(self) -> None:
266
+ """Print command hint for user interaction."""
340
267
  self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
341
268
  self.console.print()
342
269
 
343
- def _get_user_query(self) -> str | None:
344
- meta = self.ctx.meta or {}
345
- manifest = self.ctx.manifest_entry or {}
346
- return meta.get("input_message") or meta.get("query") or meta.get("message") or manifest.get("input_message")
347
-
348
- def _render_user_query(self, query: str) -> None:
349
- panel = AIPPanel(
350
- Markdown(f"Query: {query}"),
351
- title="User Request",
352
- border_style="#d97706",
353
- )
354
- self.console.print(panel)
355
- self.console.print()
356
-
357
- def _render_steps_summary(self) -> None:
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")
365
- self.console.print(panel)
366
- self.console.print()
367
-
368
- @staticmethod
369
- def _format_steps_summary(steps: list[dict[str, Any]]) -> str:
370
- if not steps:
371
- return " No steps yet"
372
-
373
- lines = []
374
- for step in steps:
375
- icon = ICON_DELEGATE if step.get("is_delegate") else ICON_TOOL_STEP
376
- duration = step.get("duration")
377
- duration_str = f" [{duration}]" if duration else ""
378
- status = " ✓" if step.get("finished") else ""
379
- title = step.get("title") or step.get("name") or "Step"
380
- lines.append(f" {icon} {title}{duration_str}{status}")
381
- return "\n".join(lines)
382
-
383
- @staticmethod
384
- def _extract_event_time(event: dict[str, Any]) -> float | None:
385
- metadata = event.get("metadata") or {}
386
- time_value = metadata.get("time")
387
- try:
388
- if isinstance(time_value, (int, float)):
389
- return float(time_value)
390
- except Exception:
391
- return None
392
- return None
393
-
394
- @staticmethod
395
- def _parse_received_timestamp(event: dict[str, Any]) -> datetime | None:
396
- value = event.get("received_at")
397
- if not value:
398
- return None
399
- if isinstance(value, str):
400
- try:
401
- normalised = value.replace("Z", "+00:00")
402
- parsed = datetime.fromisoformat(normalised)
403
- except ValueError:
404
- return None
405
- return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
406
- return None
407
-
408
- def _extract_final_duration(self) -> str | None:
409
- for event in self.ctx.events:
410
- metadata = event.get("metadata") or {}
411
- if metadata.get("kind") == "final_response":
412
- time_value = metadata.get("time")
413
- try:
414
- if isinstance(time_value, (int, float)):
415
- return f"{float(time_value):.2f}s"
416
- except Exception:
417
- return None
418
- return None
419
-
420
- def _build_step_summary(self) -> list[dict[str, Any]]:
421
- stored = self.ctx.meta.get("transcript_steps")
422
- if isinstance(stored, list) and stored:
423
- return [
424
- {
425
- "title": entry.get("display_name") or entry.get("name") or "Step",
426
- "is_delegate": entry.get("kind") == "delegate",
427
- "finished": entry.get("status") == "finished",
428
- "duration": self._format_duration_from_ms(entry.get("duration_ms")),
429
- }
430
- for entry in stored
431
- ]
432
-
433
- steps: dict[str, dict[str, Any]] = {}
434
- order: list[str] = []
435
-
436
- for event in self.ctx.events:
437
- metadata = event.get("metadata") or {}
438
- if not self._is_step_event(metadata):
439
- continue
440
-
441
- for name, info in self._iter_step_candidates(event, metadata):
442
- step = self._ensure_step_entry(steps, order, name)
443
- self._apply_step_update(step, metadata, info, event)
444
-
445
- return [steps[name] for name in order]
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
-
610
- @staticmethod
611
- def _format_duration_from_ms(value: Any) -> str | None:
612
- try:
613
- if value is None:
614
- return None
615
- duration_ms = float(value)
616
- except Exception:
617
- return None
618
-
619
- if duration_ms <= 0:
620
- return "<1ms"
621
- if duration_ms < 1000:
622
- return f"{int(duration_ms)}ms"
623
- return f"{duration_ms / 1000:.2f}s"
624
-
625
- @staticmethod
626
- def _is_step_event(metadata: dict[str, Any]) -> bool:
627
- kind = metadata.get("kind")
628
- return kind in {"agent_step", "agent_thinking_step"}
629
-
630
- def _iter_step_candidates(
631
- self, event: dict[str, Any], metadata: dict[str, Any]
632
- ) -> Iterable[tuple[str, dict[str, Any]]]:
633
- tool_info = metadata.get("tool_info") or {}
634
-
635
- yielded = False
636
- for candidate in self._iter_tool_call_candidates(tool_info):
637
- yielded = True
638
- yield candidate
639
-
640
- if yielded:
641
- return
642
-
643
- direct_tool = self._extract_direct_tool(tool_info)
644
- if direct_tool is not None:
645
- yield direct_tool
646
- return
647
-
648
- completed = self._extract_completed_name(event)
649
- if completed is not None:
650
- yield completed, {}
651
-
652
- @staticmethod
653
- def _iter_tool_call_candidates(
654
- tool_info: dict[str, Any],
655
- ) -> Iterable[tuple[str, dict[str, Any]]]:
656
- tool_calls = tool_info.get("tool_calls")
657
- if isinstance(tool_calls, list):
658
- for call in tool_calls:
659
- name = call.get("name")
660
- if name:
661
- yield name, call
662
-
663
270
  @staticmethod
664
271
  def _extract_direct_tool(
665
272
  tool_info: dict[str, Any],
666
273
  ) -> tuple[str, dict[str, Any]] | None:
274
+ """Extract direct tool from tool_info.
275
+
276
+ Args:
277
+ tool_info: Tool info dictionary.
278
+
279
+ Returns:
280
+ Tuple of (tool_name, tool_info) or None.
281
+ """
667
282
  if isinstance(tool_info, dict):
668
283
  name = tool_info.get("name")
669
284
  if name:
@@ -672,6 +287,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
672
287
 
673
288
  @staticmethod
674
289
  def _extract_completed_name(event: dict[str, Any]) -> str | None:
290
+ """Extract completed tool name from event content.
291
+
292
+ Args:
293
+ event: Event dictionary.
294
+
295
+ Returns:
296
+ Tool name or None.
297
+ """
675
298
  content = event.get("content") or ""
676
299
  if isinstance(content, str) and content.startswith("Completed "):
677
300
  name = content.replace("Completed ", "").strip()
@@ -685,6 +308,16 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
685
308
  order: list[str],
686
309
  name: str,
687
310
  ) -> dict[str, Any]:
311
+ """Ensure step entry exists, creating if needed.
312
+
313
+ Args:
314
+ steps: Steps dictionary.
315
+ order: Order list.
316
+ name: Step name.
317
+
318
+ Returns:
319
+ Step dictionary.
320
+ """
688
321
  if name not in steps:
689
322
  steps[name] = {
690
323
  "name": name,
@@ -704,6 +337,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
704
337
  info: dict[str, Any],
705
338
  event: dict[str, Any],
706
339
  ) -> None:
340
+ """Apply update to step from event metadata.
341
+
342
+ Args:
343
+ step: Step dictionary to update.
344
+ metadata: Event metadata.
345
+ info: Step info dictionary.
346
+ event: Event dictionary.
347
+ """
707
348
  status = metadata.get("status")
708
349
  event_time = metadata.get("time")
709
350
 
@@ -720,46 +361,14 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
720
361
  if duration is not None:
721
362
  step["duration"] = duration
722
363
 
723
- @staticmethod
724
- def _is_step_finished(metadata: dict[str, Any], event: dict[str, Any]) -> bool:
725
- status = metadata.get("status")
726
- return status == "finished" or bool(event.get("final"))
727
-
728
- def _compute_step_duration(
729
- self, step: dict[str, Any], info: dict[str, Any], metadata: dict[str, Any]
730
- ) -> str | None:
731
- """Calculate a formatted duration string for a step if possible."""
732
- event_time = metadata.get("time")
733
- started_at = step.get("started_at")
734
- duration_value: float | None = None
735
-
736
- if isinstance(event_time, (int, float)) and isinstance(started_at, (int, float)):
737
- try:
738
- delta = float(event_time) - float(started_at)
739
- if delta >= 0:
740
- duration_value = delta
741
- except Exception:
742
- duration_value = None
743
-
744
- if duration_value is None:
745
- exec_time = info.get("execution_time")
746
- if isinstance(exec_time, (int, float)):
747
- duration_value = float(exec_time)
748
-
749
- if duration_value is None:
750
- return None
751
-
752
- try:
753
- return format_elapsed_time(duration_value)
754
- except Exception:
755
- return None
756
-
757
364
 
758
365
  def run_viewer_session(
759
366
  console: Console,
760
367
  ctx: ViewerContext,
761
368
  export_callback: Callable[[Path], Path],
369
+ *,
370
+ initial_view: str = "default",
762
371
  ) -> None:
763
372
  """Entry point for creating and running the post-run viewer."""
764
- viewer = PostRunViewer(console, ctx, export_callback)
373
+ viewer = PostRunViewer(console, ctx, export_callback, initial_view=initial_view)
765
374
  viewer.run()