glaip-sdk 0.6.15b2__py3-none-any.whl → 0.6.16__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 (154) hide show
  1. glaip_sdk/agents/__init__.py +27 -0
  2. glaip_sdk/agents/base.py +1196 -0
  3. glaip_sdk/cli/__init__.py +9 -0
  4. glaip_sdk/cli/account_store.py +540 -0
  5. glaip_sdk/cli/agent_config.py +78 -0
  6. glaip_sdk/cli/auth.py +699 -0
  7. glaip_sdk/cli/commands/__init__.py +5 -0
  8. glaip_sdk/cli/commands/accounts.py +746 -0
  9. glaip_sdk/cli/commands/agents.py +1509 -0
  10. glaip_sdk/cli/commands/common_config.py +104 -0
  11. glaip_sdk/cli/commands/configure.py +896 -0
  12. glaip_sdk/cli/commands/mcps.py +1356 -0
  13. glaip_sdk/cli/commands/models.py +69 -0
  14. glaip_sdk/cli/commands/tools.py +576 -0
  15. glaip_sdk/cli/commands/transcripts.py +755 -0
  16. glaip_sdk/cli/commands/update.py +61 -0
  17. glaip_sdk/cli/config.py +95 -0
  18. glaip_sdk/cli/constants.py +38 -0
  19. glaip_sdk/cli/context.py +150 -0
  20. glaip_sdk/cli/core/__init__.py +79 -0
  21. glaip_sdk/cli/core/context.py +124 -0
  22. glaip_sdk/cli/core/output.py +851 -0
  23. glaip_sdk/cli/core/prompting.py +649 -0
  24. glaip_sdk/cli/core/rendering.py +187 -0
  25. glaip_sdk/cli/display.py +355 -0
  26. glaip_sdk/cli/hints.py +57 -0
  27. glaip_sdk/cli/io.py +112 -0
  28. glaip_sdk/cli/main.py +615 -0
  29. glaip_sdk/cli/masking.py +136 -0
  30. glaip_sdk/cli/mcp_validators.py +287 -0
  31. glaip_sdk/cli/pager.py +266 -0
  32. glaip_sdk/cli/parsers/__init__.py +7 -0
  33. glaip_sdk/cli/parsers/json_input.py +177 -0
  34. glaip_sdk/cli/resolution.py +67 -0
  35. glaip_sdk/cli/rich_helpers.py +27 -0
  36. glaip_sdk/cli/slash/__init__.py +15 -0
  37. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  38. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  39. glaip_sdk/cli/slash/agent_session.py +285 -0
  40. glaip_sdk/cli/slash/prompt.py +256 -0
  41. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  42. glaip_sdk/cli/slash/session.py +1708 -0
  43. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  44. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  45. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  46. glaip_sdk/cli/slash/tui/loading.py +58 -0
  47. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  48. glaip_sdk/cli/transcript/__init__.py +31 -0
  49. glaip_sdk/cli/transcript/cache.py +536 -0
  50. glaip_sdk/cli/transcript/capture.py +329 -0
  51. glaip_sdk/cli/transcript/export.py +38 -0
  52. glaip_sdk/cli/transcript/history.py +815 -0
  53. glaip_sdk/cli/transcript/launcher.py +77 -0
  54. glaip_sdk/cli/transcript/viewer.py +374 -0
  55. glaip_sdk/cli/update_notifier.py +290 -0
  56. glaip_sdk/cli/utils.py +263 -0
  57. glaip_sdk/cli/validators.py +238 -0
  58. glaip_sdk/client/__init__.py +11 -0
  59. glaip_sdk/client/_agent_payloads.py +520 -0
  60. glaip_sdk/client/agent_runs.py +147 -0
  61. glaip_sdk/client/agents.py +1335 -0
  62. glaip_sdk/client/base.py +502 -0
  63. glaip_sdk/client/main.py +249 -0
  64. glaip_sdk/client/mcps.py +370 -0
  65. glaip_sdk/client/run_rendering.py +700 -0
  66. glaip_sdk/client/shared.py +21 -0
  67. glaip_sdk/client/tools.py +661 -0
  68. glaip_sdk/client/validators.py +198 -0
  69. glaip_sdk/config/constants.py +52 -0
  70. glaip_sdk/mcps/__init__.py +21 -0
  71. glaip_sdk/mcps/base.py +345 -0
  72. glaip_sdk/models/__init__.py +90 -0
  73. glaip_sdk/models/agent.py +47 -0
  74. glaip_sdk/models/agent_runs.py +116 -0
  75. glaip_sdk/models/common.py +42 -0
  76. glaip_sdk/models/mcp.py +33 -0
  77. glaip_sdk/models/tool.py +33 -0
  78. glaip_sdk/payload_schemas/__init__.py +7 -0
  79. glaip_sdk/payload_schemas/agent.py +85 -0
  80. glaip_sdk/registry/__init__.py +55 -0
  81. glaip_sdk/registry/agent.py +164 -0
  82. glaip_sdk/registry/base.py +139 -0
  83. glaip_sdk/registry/mcp.py +253 -0
  84. glaip_sdk/registry/tool.py +232 -0
  85. glaip_sdk/runner/__init__.py +59 -0
  86. glaip_sdk/runner/base.py +84 -0
  87. glaip_sdk/runner/deps.py +112 -0
  88. glaip_sdk/runner/langgraph.py +782 -0
  89. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  90. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  91. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  92. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  93. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  94. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  95. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  96. glaip_sdk/tools/__init__.py +22 -0
  97. glaip_sdk/tools/base.py +435 -0
  98. glaip_sdk/utils/__init__.py +86 -0
  99. glaip_sdk/utils/a2a/__init__.py +34 -0
  100. glaip_sdk/utils/a2a/event_processor.py +188 -0
  101. glaip_sdk/utils/agent_config.py +194 -0
  102. glaip_sdk/utils/bundler.py +267 -0
  103. glaip_sdk/utils/client.py +111 -0
  104. glaip_sdk/utils/client_utils.py +486 -0
  105. glaip_sdk/utils/datetime_helpers.py +58 -0
  106. glaip_sdk/utils/discovery.py +78 -0
  107. glaip_sdk/utils/display.py +135 -0
  108. glaip_sdk/utils/export.py +143 -0
  109. glaip_sdk/utils/general.py +61 -0
  110. glaip_sdk/utils/import_export.py +168 -0
  111. glaip_sdk/utils/import_resolver.py +492 -0
  112. glaip_sdk/utils/instructions.py +101 -0
  113. glaip_sdk/utils/rendering/__init__.py +115 -0
  114. glaip_sdk/utils/rendering/formatting.py +264 -0
  115. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  116. glaip_sdk/utils/rendering/layout/panels.py +156 -0
  117. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  118. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  119. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  120. glaip_sdk/utils/rendering/models.py +85 -0
  121. glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
  122. glaip_sdk/utils/rendering/renderer/base.py +1024 -0
  123. glaip_sdk/utils/rendering/renderer/config.py +27 -0
  124. glaip_sdk/utils/rendering/renderer/console.py +55 -0
  125. glaip_sdk/utils/rendering/renderer/debug.py +178 -0
  126. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  127. glaip_sdk/utils/rendering/renderer/stream.py +202 -0
  128. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  129. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  130. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  131. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  132. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  133. glaip_sdk/utils/rendering/state.py +204 -0
  134. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  135. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  136. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  137. glaip_sdk/utils/rendering/steps/format.py +176 -0
  138. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  139. glaip_sdk/utils/rendering/timing.py +36 -0
  140. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  141. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  142. glaip_sdk/utils/resource_refs.py +195 -0
  143. glaip_sdk/utils/run_renderer.py +41 -0
  144. glaip_sdk/utils/runtime_config.py +425 -0
  145. glaip_sdk/utils/serialization.py +424 -0
  146. glaip_sdk/utils/sync.py +142 -0
  147. glaip_sdk/utils/tool_detection.py +33 -0
  148. glaip_sdk/utils/validation.py +264 -0
  149. {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/METADATA +4 -5
  150. glaip_sdk-0.6.16.dist-info/RECORD +160 -0
  151. glaip_sdk-0.6.15b2.dist-info/RECORD +0 -12
  152. {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/WHEEL +0 -0
  153. {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/entry_points.txt +0 -0
  154. {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,442 @@
1
+ """Tool panel controller logic extracted from the renderer.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from time import monotonic
11
+ from typing import Any, TYPE_CHECKING
12
+
13
+ from rich.console import Console
14
+
15
+ from glaip_sdk.utils.rendering.layout.panels import create_tool_panel
16
+ from glaip_sdk.utils.rendering.layout.progress import format_tool_title, is_delegation_tool
17
+ from glaip_sdk.utils.rendering.layout.transcript import DEFAULT_TRANSCRIPT_THEME
18
+ from glaip_sdk.utils.rendering.models import Step
19
+ from glaip_sdk.utils.rendering.renderer.config import RendererConfig
20
+ from glaip_sdk.utils.rendering.steps import StepManager
21
+
22
+ if TYPE_CHECKING: # pragma: no cover - typing helper
23
+ from glaip_sdk.utils.rendering.renderer.stream import StreamProcessor
24
+
25
+
26
+ class ToolPanelController:
27
+ """Encapsulates tool panel lifecycle management."""
28
+
29
+ OUTPUT_PREFIX = "**Output:**\n"
30
+
31
+ def __init__(
32
+ self,
33
+ *,
34
+ steps: StepManager,
35
+ stream_processor: StreamProcessor,
36
+ console: Console,
37
+ cfg: RendererConfig,
38
+ step_server_start_times: dict[str, float],
39
+ output_prefix: str | None = None,
40
+ ) -> None:
41
+ """Initialize the tool panel controller.
42
+
43
+ Args:
44
+ steps: Step manager instance for tracking steps
45
+ stream_processor: Stream processor for handling events
46
+ console: Rich console instance for rendering
47
+ cfg: Renderer configuration
48
+ step_server_start_times: Dictionary mapping step IDs to server start times
49
+ output_prefix: Optional prefix for tool output (defaults to OUTPUT_PREFIX)
50
+ """
51
+ self._steps = steps
52
+ self._stream_processor = stream_processor
53
+ self._console = console
54
+ self._cfg = cfg
55
+ self._step_server_start_times = step_server_start_times
56
+ self.panels: dict[str, dict[str, Any]] = {}
57
+ self._panel_output_prefix = output_prefix or self.OUTPUT_PREFIX
58
+
59
+ # Public API -------------------------------------------------------
60
+ def update_console(self, console: Console) -> None:
61
+ """Update the console reference used for snapshot printing."""
62
+ self._console = console
63
+
64
+ def update_config(self, cfg: RendererConfig) -> None:
65
+ """Update configuration reference (useful when overrides occur)."""
66
+ self._cfg = cfg
67
+
68
+ def finish_all_panels(self) -> None:
69
+ """Mark all panels as finished (used during cleanup/finalization)."""
70
+ try:
71
+ items = list(self.panels.items())
72
+ except Exception: # pragma: no cover - defensive guard
73
+ return
74
+
75
+ for _sid, meta in items:
76
+ if meta.get("status") != "finished":
77
+ meta["status"] = "finished"
78
+
79
+ def handle_agent_step(
80
+ self,
81
+ event: dict[str, Any],
82
+ tool_name: str | None,
83
+ tool_args: Any,
84
+ _tool_out: Any,
85
+ tool_calls_info: list[tuple[str, Any, Any]],
86
+ *,
87
+ tracked_step: Step | None = None,
88
+ ) -> None:
89
+ """Handle agent step tool bookkeeping."""
90
+ metadata = event.get("metadata", {})
91
+ task_id = event.get("task_id") or metadata.get("task_id")
92
+ context_id = event.get("context_id") or metadata.get("context_id")
93
+ content = event.get("content", "")
94
+
95
+ if tool_name:
96
+ tool_sid = self._ensure_tool_panel(tool_name, tool_args, task_id, context_id)
97
+ self._start_tool_step(
98
+ task_id,
99
+ context_id,
100
+ tool_name,
101
+ tool_args,
102
+ tool_sid,
103
+ tracked_step=tracked_step,
104
+ )
105
+
106
+ self._process_additional_tool_calls(tool_calls_info, tool_name, task_id, context_id)
107
+
108
+ (
109
+ is_tool_finished,
110
+ finished_tool_name,
111
+ finished_tool_output,
112
+ ) = self._detect_tool_completion(metadata, content)
113
+
114
+ if not (is_tool_finished and finished_tool_name):
115
+ return
116
+
117
+ self._finish_tool_panel(finished_tool_name, finished_tool_output, task_id, context_id)
118
+ self._finish_tool_step(
119
+ finished_tool_name,
120
+ finished_tool_output,
121
+ task_id,
122
+ context_id,
123
+ tracked_step=tracked_step,
124
+ )
125
+ self._create_tool_snapshot(finished_tool_name, task_id, context_id)
126
+
127
+ # Internal helpers -------------------------------------------------
128
+ def _ensure_tool_panel(self, name: str, args: Any, task_id: str, context_id: str) -> str:
129
+ formatted_title = format_tool_title(name)
130
+ is_delegation = is_delegation_tool(name)
131
+ tool_sid = self._session_id(name, task_id, context_id)
132
+
133
+ if tool_sid not in self.panels:
134
+ self.panels[tool_sid] = {
135
+ "title": formatted_title,
136
+ "status": "running",
137
+ "started_at": monotonic(),
138
+ "server_started_at": self._stream_processor.server_elapsed_time,
139
+ "chunks": [],
140
+ "args": args or {},
141
+ "output": None,
142
+ "is_delegation": is_delegation,
143
+ }
144
+ if args:
145
+ try:
146
+ args_content = "**Args:**\n```json\n" + json.dumps(args, indent=2) + "\n```\n\n"
147
+ except Exception:
148
+ args_content = f"**Args:**\n{args}\n\n"
149
+ self.panels[tool_sid]["chunks"].append(args_content)
150
+
151
+ return tool_sid
152
+
153
+ def _start_tool_step(
154
+ self,
155
+ task_id: str,
156
+ context_id: str,
157
+ tool_name: str,
158
+ tool_args: Any,
159
+ _tool_sid: str,
160
+ *,
161
+ tracked_step: Step | None = None,
162
+ ) -> Step | None:
163
+ if tracked_step is not None:
164
+ return tracked_step
165
+
166
+ if is_delegation_tool(tool_name):
167
+ step = self._steps.start_or_get(
168
+ task_id=task_id,
169
+ context_id=context_id,
170
+ kind="delegate",
171
+ name=tool_name,
172
+ args=tool_args,
173
+ )
174
+ else:
175
+ step = self._steps.start_or_get(
176
+ task_id=task_id,
177
+ context_id=context_id,
178
+ kind="tool",
179
+ name=tool_name,
180
+ args=tool_args,
181
+ )
182
+
183
+ if step and self._stream_processor.server_elapsed_time is not None:
184
+ self._step_server_start_times[step.step_id] = self._stream_processor.server_elapsed_time
185
+
186
+ return step
187
+
188
+ def _process_additional_tool_calls(
189
+ self,
190
+ tool_calls_info: list[tuple[str, Any, Any]],
191
+ tool_name: str | None,
192
+ task_id: str,
193
+ context_id: str,
194
+ ) -> None:
195
+ for call_name, call_args, _ in tool_calls_info or []:
196
+ if call_name and call_name != tool_name:
197
+ self._process_single_tool_call(call_name, call_args, task_id, context_id)
198
+
199
+ def _process_single_tool_call(self, call_name: str, call_args: Any, task_id: str, context_id: str) -> None:
200
+ self._ensure_tool_panel(call_name, call_args, task_id, context_id)
201
+ step = self._create_step_for_tool_call(call_name, call_args, task_id, context_id)
202
+ if step and self._stream_processor.server_elapsed_time is not None:
203
+ self._step_server_start_times[step.step_id] = self._stream_processor.server_elapsed_time
204
+
205
+ def _create_step_for_tool_call(self, call_name: str, call_args: Any, task_id: str, context_id: str) -> Any:
206
+ if is_delegation_tool(call_name):
207
+ return self._steps.start_or_get(
208
+ task_id=task_id,
209
+ context_id=context_id,
210
+ kind="delegate",
211
+ name=call_name,
212
+ args=call_args,
213
+ )
214
+ return self._steps.start_or_get(
215
+ task_id=task_id,
216
+ context_id=context_id,
217
+ kind="tool",
218
+ name=call_name,
219
+ args=call_args,
220
+ )
221
+
222
+ def _detect_tool_completion(self, metadata: dict[str, Any], content: str) -> tuple[bool, str | None, Any]:
223
+ tool_info = metadata.get("tool_info", {}) if isinstance(metadata, dict) else {}
224
+
225
+ if tool_info.get("status") == "finished" and tool_info.get("name"):
226
+ return True, tool_info.get("name"), tool_info.get("output")
227
+ if content and isinstance(content, str) and content.startswith("Completed "):
228
+ tool_name = content.replace("Completed ", "").strip()
229
+ if tool_name:
230
+ output = tool_info.get("output") if tool_info.get("name") == tool_name else None
231
+ return True, tool_name, output
232
+ if metadata.get("status") == "finished" and tool_info.get("name"):
233
+ return True, tool_info.get("name"), tool_info.get("output")
234
+ return False, None, None
235
+
236
+ def _finish_tool_panel(
237
+ self,
238
+ finished_tool_name: str,
239
+ finished_tool_output: Any,
240
+ task_id: str,
241
+ context_id: str,
242
+ ) -> None:
243
+ tool_sid = self._session_id(finished_tool_name, task_id, context_id)
244
+ if tool_sid not in self.panels:
245
+ return
246
+
247
+ meta = self.panels[tool_sid]
248
+ self._mark_panel_as_finished(meta, tool_sid)
249
+ self._add_tool_output_to_panel(meta, finished_tool_output, finished_tool_name)
250
+
251
+ def _finish_tool_step(
252
+ self,
253
+ finished_tool_name: str,
254
+ finished_tool_output: Any,
255
+ task_id: str,
256
+ context_id: str,
257
+ *,
258
+ tracked_step: Step | None = None,
259
+ ) -> None:
260
+ if tracked_step is not None:
261
+ return
262
+
263
+ duration = self._get_step_duration(finished_tool_name, task_id, context_id)
264
+ if is_delegation_tool(finished_tool_name):
265
+ self._steps.finish(
266
+ task_id=task_id,
267
+ context_id=context_id,
268
+ kind="delegate",
269
+ name=finished_tool_name,
270
+ output=finished_tool_output,
271
+ duration_raw=duration,
272
+ )
273
+ return
274
+
275
+ self._steps.finish(
276
+ task_id=task_id,
277
+ context_id=context_id,
278
+ kind="tool",
279
+ name=finished_tool_name,
280
+ output=finished_tool_output,
281
+ duration_raw=duration,
282
+ )
283
+
284
+ def _mark_panel_as_finished(self, meta: dict[str, Any], tool_sid: str) -> None:
285
+ if meta.get("status") != "finished":
286
+ meta["status"] = "finished"
287
+ dur = self._calculate_tool_duration(meta)
288
+ self._update_tool_metadata(meta, dur)
289
+ self._stream_processor.current_event_finished_panels.add(tool_sid)
290
+
291
+ def _calculate_tool_duration(self, meta: dict[str, Any]) -> float | None:
292
+ started_at = meta.get("server_started_at")
293
+ finished_at = (
294
+ self._stream_processor.server_elapsed_time
295
+ if isinstance(self._stream_processor.server_elapsed_time, (int, float))
296
+ else None
297
+ )
298
+ if not isinstance(started_at, (int, float)) or finished_at is None:
299
+ started_at = meta.get("started_at")
300
+ finished_at = meta.get("finished_at")
301
+ try:
302
+ if isinstance(started_at, (int, float)) and isinstance(finished_at, (int, float)):
303
+ return max(0.0, float(finished_at) - float(started_at))
304
+ except Exception:
305
+ return None
306
+ return None
307
+
308
+ def _update_tool_metadata(self, meta: dict[str, Any], dur: float | None) -> None:
309
+ if dur is not None:
310
+ meta["duration_seconds"] = dur
311
+ meta["server_finished_at"] = (
312
+ self._stream_processor.server_elapsed_time
313
+ if isinstance(self._stream_processor.server_elapsed_time, (int, float))
314
+ else None
315
+ )
316
+ meta["finished_at"] = monotonic()
317
+
318
+ def _add_tool_output_to_panel(
319
+ self, meta: dict[str, Any], finished_tool_output: Any, finished_tool_name: str
320
+ ) -> None:
321
+ if finished_tool_output is None:
322
+ return
323
+ meta.setdefault("chunks", []).append(self._format_output_block(finished_tool_output, finished_tool_name))
324
+ meta["output"] = finished_tool_output
325
+
326
+ def _get_step_duration(self, finished_tool_name: str, task_id: str, context_id: str) -> float | None:
327
+ tool_sid = self._session_id(finished_tool_name, task_id, context_id)
328
+ return self.panels.get(tool_sid, {}).get("duration_seconds")
329
+
330
+ def _should_create_snapshot(self, tool_sid: str) -> bool:
331
+ return self._cfg.append_finished_snapshots and not self.panels.get(tool_sid, {}).get("snapshot_printed")
332
+
333
+ def _create_tool_snapshot(self, finished_tool_name: str, task_id: str, context_id: str) -> None:
334
+ tool_sid = self._session_id(finished_tool_name, task_id, context_id)
335
+ if not self._should_create_snapshot(tool_sid):
336
+ return
337
+
338
+ meta = self.panels[tool_sid]
339
+ adjusted_title = self._get_snapshot_title(meta, finished_tool_name)
340
+ body_text = "".join(meta.get("chunks") or [])
341
+ body_text = self._clamp_snapshot_body(body_text)
342
+
343
+ snapshot_panel = create_tool_panel(
344
+ title=adjusted_title,
345
+ content=body_text or "(no output)",
346
+ status="finished",
347
+ theme=self._panel_theme(),
348
+ is_delegation=is_delegation_tool(finished_tool_name),
349
+ )
350
+ self._console.print(snapshot_panel)
351
+ self.panels[tool_sid]["snapshot_printed"] = True
352
+
353
+ def _get_snapshot_title(self, meta: dict[str, Any], finished_tool_name: str) -> str:
354
+ adjusted_title = meta.get("title") or finished_tool_name
355
+ dur = meta.get("duration_seconds")
356
+ if isinstance(dur, (int, float)):
357
+ elapsed = self._format_snapshot_duration(dur)
358
+ adjusted_title = f"{adjusted_title} · {elapsed}"
359
+ return adjusted_title
360
+
361
+ def _format_snapshot_duration(self, dur: int | float) -> str:
362
+ try:
363
+ if not isinstance(dur, (int, float)):
364
+ return "<1ms"
365
+ if dur >= 1:
366
+ return f"{dur:.2f}s"
367
+ if int(dur * 1000) > 0:
368
+ return f"{int(dur * 1000)}ms"
369
+ return "<1ms"
370
+ except (TypeError, ValueError, OverflowError):
371
+ return "<1ms"
372
+
373
+ def _clamp_snapshot_body(self, body_text: str) -> str:
374
+ max_lines = int(self._cfg.snapshot_max_lines or 0)
375
+ lines = body_text.splitlines()
376
+ if max_lines > 0 and len(lines) > max_lines:
377
+ lines = lines[:max_lines] + ["… (truncated)"]
378
+ body_text = "\n".join(lines)
379
+
380
+ max_chars = int(self._cfg.snapshot_max_chars or 0)
381
+ if max_chars > 0 and len(body_text) > max_chars:
382
+ suffix = "\n… (truncated)"
383
+ body_text = body_text[: max_chars - len(suffix)] + suffix
384
+
385
+ return body_text
386
+
387
+ def _session_id(self, tool_name: str, task_id: str, context_id: str) -> str:
388
+ return f"tool_{tool_name}_{task_id}_{context_id}"
389
+
390
+ # Output formatting helpers ---------------------------------------
391
+ def _format_output_block(self, output_value: Any, tool_name: str | None) -> str:
392
+ if isinstance(output_value, (dict, list)):
393
+ return self._format_dict_or_list_output(output_value)
394
+ if isinstance(output_value, str):
395
+ return self._format_string_output(output_value, tool_name)
396
+ return self._format_other_output(output_value)
397
+
398
+ def _format_dict_or_list_output(self, output_value: dict | list) -> str:
399
+ try:
400
+ return self._panel_output_prefix + "```json\n" + json.dumps(output_value, indent=2) + "\n```\n"
401
+ except Exception:
402
+ return self._panel_output_prefix + str(output_value) + "\n"
403
+
404
+ def _format_string_output(self, output: str, tool_name: str | None) -> str:
405
+ s = output.strip()
406
+ s = self._clean_sub_agent_prefix(s, tool_name)
407
+ if (s.startswith("{") and s.endswith("}")) or (s.startswith("[") and s.endswith("]")):
408
+ return self._format_json_string_output(s)
409
+ return self._panel_output_prefix + s + "\n"
410
+
411
+ def _format_other_output(self, output_value: Any) -> str:
412
+ try:
413
+ return self._panel_output_prefix + json.dumps(output_value, indent=2) + "\n"
414
+ except Exception:
415
+ return self._panel_output_prefix + str(output_value) + "\n"
416
+
417
+ def _format_json_string_output(self, output: str) -> str:
418
+ try:
419
+ parsed = json.loads(output)
420
+ return self._panel_output_prefix + "```json\n" + json.dumps(parsed, indent=2) + "\n```\n"
421
+ except Exception:
422
+ return self._panel_output_prefix + output + "\n"
423
+
424
+ def _clean_sub_agent_prefix(self, output: str, tool_name: str | None) -> str:
425
+ if not (tool_name and is_delegation_tool(tool_name)):
426
+ return output
427
+
428
+ sub = tool_name
429
+ if tool_name.startswith("delegate_to_"):
430
+ sub = tool_name.replace("delegate_to_", "")
431
+ elif tool_name.startswith("delegate_"):
432
+ sub = tool_name.replace("delegate_", "")
433
+ prefix = f"[{sub}]"
434
+ if output.startswith(prefix):
435
+ return output[len(prefix) :].lstrip()
436
+ return output
437
+
438
+ def _panel_theme(self) -> str:
439
+ return DEFAULT_TRANSCRIPT_THEME
440
+
441
+
442
+ __all__ = ["ToolPanelController"]
@@ -0,0 +1,162 @@
1
+ """Transcript mode utilities extracted from the renderer.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
13
+ from glaip_sdk.utils.rendering.state import coerce_received_at
14
+
15
+
16
+ class TranscriptModeMixin:
17
+ """Provides transcript-mode toggling, hints, and replay helpers."""
18
+
19
+ def __init__(self, *args, **kwargs) -> None:
20
+ """Initialize transcript mode mixin.
21
+
22
+ Args:
23
+ *args: Positional arguments passed to parent class
24
+ **kwargs: Keyword arguments passed to parent class
25
+ """
26
+ super().__init__(*args, **kwargs)
27
+ self._transcript_mode_enabled: bool = False
28
+ self._transcript_render_cursor: int = 0
29
+ self.transcript_controller: Any | None = None
30
+ self._transcript_hint_message = "[dim]Transcript view · Press Ctrl+T to return to the summary.[/dim]"
31
+ self._summary_hint_message = "[dim]Press Ctrl+T to inspect raw transcript events.[/dim]"
32
+ self._summary_hint_printed_once: bool = False
33
+ self._transcript_hint_printed_once: bool = False
34
+ self._transcript_header_printed: bool = False
35
+ self._transcript_enabled_message_printed: bool = False
36
+
37
+ # ------------------------------------------------------------------
38
+ # Public controls
39
+ # ------------------------------------------------------------------
40
+ @property
41
+ def transcript_mode_enabled(self) -> bool:
42
+ """Return True when transcript mode is currently active."""
43
+ return self._transcript_mode_enabled
44
+
45
+ def toggle_transcript_mode(self) -> None:
46
+ """Flip transcript mode on/off."""
47
+ self.set_transcript_mode(not self._transcript_mode_enabled)
48
+
49
+ def set_transcript_mode(self, enabled: bool) -> None:
50
+ """Set transcript mode explicitly."""
51
+ if enabled == self._transcript_mode_enabled:
52
+ return
53
+
54
+ self._transcript_mode_enabled = enabled
55
+ self.apply_verbosity(enabled)
56
+
57
+ if enabled:
58
+ self._summary_hint_printed_once = False
59
+ self._transcript_hint_printed_once = False
60
+ self._transcript_header_printed = False
61
+ self._transcript_enabled_message_printed = False
62
+ self._stop_live_display()
63
+ self._clear_console_safe()
64
+ self._print_transcript_enabled_message()
65
+ self._render_transcript_backfill()
66
+ else:
67
+ self._transcript_hint_printed_once = False
68
+ self._transcript_header_printed = False
69
+ self._transcript_enabled_message_printed = False
70
+ self._clear_console_safe()
71
+
72
+ self._render_summary_static_sections()
73
+ summary_notice = (
74
+ "[dim]Returning to the summary view. Streaming will continue here.[/dim]"
75
+ if not self.state.finalizing_ui
76
+ else "[dim]Returning to the summary view.[/dim]"
77
+ )
78
+ self.console.print(summary_notice)
79
+ self._render_summary_after_transcript_toggle()
80
+ if not self.state.finalizing_ui:
81
+ self._print_summary_hint(force=True)
82
+
83
+ # ------------------------------------------------------------------
84
+ # Internal helpers
85
+ # ------------------------------------------------------------------
86
+ def _clear_console_safe(self) -> None:
87
+ try:
88
+ self.console.clear()
89
+ except Exception:
90
+ pass
91
+
92
+ def _print_transcript_hint(self) -> None:
93
+ if not self._transcript_mode_enabled:
94
+ return
95
+ try:
96
+ self.console.print(self._transcript_hint_message)
97
+ except Exception:
98
+ pass
99
+ else:
100
+ self._transcript_hint_printed_once = True
101
+
102
+ def _print_transcript_enabled_message(self) -> None:
103
+ if self._transcript_enabled_message_printed:
104
+ return
105
+ try:
106
+ self.console.print("[dim]Transcript mode enabled — streaming raw transcript events.[/dim]")
107
+ except Exception:
108
+ pass
109
+ else:
110
+ self._transcript_enabled_message_printed = True
111
+
112
+ def _ensure_transcript_header(self) -> None:
113
+ if self._transcript_header_printed:
114
+ return
115
+ try:
116
+ self.console.rule("Transcript Events")
117
+ except Exception:
118
+ self._transcript_header_printed = True
119
+ return
120
+ self._transcript_header_printed = True
121
+
122
+ def _print_summary_hint(self, force: bool = False) -> None:
123
+ controller = getattr(self, "transcript_controller", None)
124
+ if controller and not getattr(controller, "enabled", False):
125
+ if not force:
126
+ self._summary_hint_printed_once = True
127
+ return
128
+ if not force and self._summary_hint_printed_once:
129
+ return
130
+ try:
131
+ self.console.print(self._summary_hint_message)
132
+ except Exception:
133
+ return
134
+ self._summary_hint_printed_once = True
135
+
136
+ def _render_transcript_backfill(self) -> None:
137
+ pending = self.state.events[self._transcript_render_cursor :]
138
+ self._ensure_transcript_header()
139
+ if not pending:
140
+ self._print_transcript_hint()
141
+ return
142
+
143
+ baseline = self.state.streaming_started_event_ts
144
+ for ev in pending:
145
+ received_ts = coerce_received_at(ev.get("received_at"))
146
+ render_debug_event(
147
+ ev,
148
+ self.console,
149
+ received_ts=received_ts,
150
+ baseline_ts=baseline,
151
+ )
152
+
153
+ self._transcript_render_cursor = len(self.state.events)
154
+ self._print_transcript_hint()
155
+
156
+ def _capture_event(self, ev: dict[str, Any], received_at: datetime | None = None) -> None:
157
+ self.state.record_event(ev, received_at=received_at)
158
+ if self._transcript_mode_enabled:
159
+ self._transcript_render_cursor = len(self.state.events)
160
+
161
+
162
+ __all__ = ["TranscriptModeMixin"]