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,74 @@
1
+ """Summary panel helpers shared between renderer and viewer.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping
10
+ from typing import Any
11
+
12
+ from glaip_sdk.utils.rendering.layout.transcript import (
13
+ DEFAULT_TRANSCRIPT_THEME,
14
+ build_transcript_snapshot,
15
+ build_transcript_view,
16
+ normalise_meta_payload,
17
+ )
18
+ from glaip_sdk.utils.rendering.state import RendererState
19
+ from glaip_sdk.utils.rendering.steps import StepManager
20
+
21
+
22
+ def render_summary_panels(
23
+ state: RendererState,
24
+ steps: StepManager,
25
+ *,
26
+ theme: str | None = None,
27
+ summary_window: int | None = None,
28
+ include_query_panel: bool = True,
29
+ include_final_panel: bool = True,
30
+ step_status_overrides: dict[str, str] | None = None,
31
+ ) -> list[Any]:
32
+ """Return shared summary panels for renderer and offline viewer."""
33
+ resolved_theme = theme or DEFAULT_TRANSCRIPT_THEME
34
+ snapshot_source = state.to_snapshot() if hasattr(state, "to_snapshot") else state
35
+ if isinstance(snapshot_source, Mapping):
36
+ raw_meta = snapshot_source.get("meta")
37
+ else:
38
+ raw_meta = getattr(state, "meta", None)
39
+ snapshot_meta = normalise_meta_payload(raw_meta)
40
+ snapshot = build_transcript_snapshot(
41
+ snapshot_source,
42
+ steps,
43
+ meta=snapshot_meta,
44
+ summary_window=summary_window,
45
+ theme=resolved_theme,
46
+ step_status_overrides=step_status_overrides,
47
+ )
48
+ _header, body = build_transcript_view(snapshot, theme=resolved_theme)
49
+
50
+ return [
51
+ renderable
52
+ for renderable in body
53
+ if _should_include_summary_panel(
54
+ renderable,
55
+ include_query_panel=include_query_panel,
56
+ include_final_panel=include_final_panel,
57
+ )
58
+ ]
59
+
60
+
61
+ def _should_include_summary_panel(
62
+ renderable: Any,
63
+ *,
64
+ include_query_panel: bool,
65
+ include_final_panel: bool,
66
+ ) -> bool:
67
+ """Return True when the panel should be included in the summary list."""
68
+ title = getattr(renderable, "title", "")
69
+ normalised = title.lower() if isinstance(title, str) else ""
70
+ if not include_query_panel and normalised == "user request":
71
+ return False
72
+ if not include_final_panel and normalised.startswith("final result"):
73
+ return False
74
+ return True
@@ -0,0 +1,606 @@
1
+ """Shared transcript presentation helpers.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from collections.abc import Mapping, Sequence
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+ from rich.console import Group
15
+ from rich.markdown import Markdown
16
+ from rich.text import Text
17
+
18
+ from glaip_sdk.icons import ICON_AGENT
19
+ from glaip_sdk.rich_components import AIPPanel
20
+ from glaip_sdk.utils.rendering.layout.panels import create_final_panel
21
+ from glaip_sdk.utils.rendering.renderer.summary_window import clamp_step_nodes
22
+ from glaip_sdk.utils.rendering.state import RendererState
23
+ from glaip_sdk.utils.rendering.steps import (
24
+ StepManager,
25
+ StepPresentation,
26
+ build_connector_prefix,
27
+ format_step,
28
+ format_step_label,
29
+ humanize_tool_name,
30
+ )
31
+
32
+ DEFAULT_TRANSCRIPT_THEME = "dark"
33
+ _NO_STEPS_TEXT = Text("No steps yet", style="dim")
34
+
35
+
36
+ @dataclass(slots=True)
37
+ class TranscriptGlyphs:
38
+ """Glyph overrides for transcript presentation."""
39
+
40
+ branch_fill: str = "│ "
41
+ branch_empty: str = " "
42
+ branch_item: str = "├─ "
43
+ branch_last: str = "└─ "
44
+ query_prefix: str = " "
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class TranscriptRow:
49
+ """Renderable row metadata for the transcript/summary tree."""
50
+
51
+ prefix: str
52
+ presentation: StepPresentation
53
+
54
+
55
+ @dataclass(slots=True)
56
+ class TranscriptSnapshot:
57
+ """Snapshot consumed by presenter/viewer components."""
58
+
59
+ rows: list[TranscriptRow]
60
+ final_panel: Any | None
61
+ events: list[dict[str, Any]]
62
+ agent_label: str | None = None
63
+ model_label: str | None = None
64
+ run_id: str | None = None
65
+ query_text: str | None = None
66
+ duration_text: str | None = None
67
+ window_header: Text | None = None
68
+ window_footer: Text | None = None
69
+
70
+
71
+ def format_final_panel_title(state: RendererState | Mapping[str, Any], base_title: str = "Final Result") -> str:
72
+ """Return the final panel title including duration if available."""
73
+ if isinstance(state, RendererState):
74
+ duration_text = state.final_duration_text
75
+ else:
76
+ duration_text = state.get("final_duration_text") if isinstance(state, Mapping) else None
77
+ if duration_text:
78
+ return f"{base_title} · {duration_text}"
79
+ return base_title
80
+
81
+
82
+ def build_final_panel(
83
+ state: RendererState | Mapping[str, Any],
84
+ *,
85
+ theme: str = DEFAULT_TRANSCRIPT_THEME,
86
+ title: str | None = None,
87
+ ) -> Any | None:
88
+ """Create a Rich panel for the renderer/viewer final output."""
89
+ if isinstance(state, RendererState):
90
+ body = (state.final_text or state.buffer.render() or "").strip()
91
+ else:
92
+ final_text = str(state.get("final_text", "")) if isinstance(state, Mapping) else ""
93
+ buffer_text = str(state.get("buffer_text", "")) if isinstance(state, Mapping) else ""
94
+ body = (final_text or buffer_text).strip()
95
+ if not body:
96
+ return None
97
+ panel_title = title or format_final_panel_title(state)
98
+ return create_final_panel(body, title=panel_title, theme=theme)
99
+
100
+
101
+ def build_transcript_snapshot(
102
+ state: RendererState | Mapping[str, Any],
103
+ steps: StepManager,
104
+ *,
105
+ glyphs: TranscriptGlyphs | None = None,
106
+ query_text: str | None = None,
107
+ meta: Mapping[str, Any] | Sequence[tuple[str, Any]] | None = None,
108
+ summary_window: int | None = None,
109
+ theme: str = DEFAULT_TRANSCRIPT_THEME,
110
+ step_status_overrides: dict[str, str] | None = None,
111
+ ) -> TranscriptSnapshot:
112
+ """Compose a snapshot consumable by renderers/viewers alike."""
113
+ glyphs = glyphs or TranscriptGlyphs()
114
+ final_text, buffer_text, events, meta_payload, duration_text = _resolve_state_payload(state, meta)
115
+ query_value = query_text or extract_query_from_meta(meta_payload)
116
+ rows, window_header, window_footer = _build_rows(
117
+ steps,
118
+ glyphs,
119
+ meta_payload,
120
+ summary_window,
121
+ query_text=query_value,
122
+ step_status_overrides=step_status_overrides,
123
+ )
124
+ if not rows:
125
+ stored = meta_payload.get("transcript_steps")
126
+ if isinstance(stored, list) and stored:
127
+ rows, window_header, window_footer = _rows_from_stored_steps(stored)
128
+ panel_state: RendererState | Mapping[str, Any]
129
+ if isinstance(state, RendererState):
130
+ panel_state = state
131
+ else:
132
+ panel_state = {
133
+ "final_text": final_text,
134
+ "buffer_text": buffer_text,
135
+ "final_duration_text": duration_text,
136
+ }
137
+ final_panel = build_final_panel(panel_state, theme=theme)
138
+
139
+ return TranscriptSnapshot(
140
+ rows=rows,
141
+ final_panel=final_panel,
142
+ events=events,
143
+ agent_label=_friendly_agent_label(meta_payload),
144
+ model_label=meta_payload.get("model"),
145
+ run_id=meta_payload.get("run_id"),
146
+ query_text=query_value,
147
+ duration_text=duration_text,
148
+ window_header=window_header,
149
+ window_footer=window_footer,
150
+ )
151
+
152
+
153
+ def build_transcript_view(
154
+ snapshot: TranscriptSnapshot,
155
+ *,
156
+ theme: str = DEFAULT_TRANSCRIPT_THEME,
157
+ ) -> tuple[list[Any], list[Any]]:
158
+ """Return header + body renderables for a transcript snapshot."""
159
+ header_renderables: list[Any] = []
160
+ body_renderables: list[Any] = []
161
+
162
+ header_text = _compose_header_text(snapshot)
163
+ if header_text is not None:
164
+ if theme != DEFAULT_TRANSCRIPT_THEME:
165
+ header_text.style = "bold black"
166
+ header_renderables.append(header_text)
167
+
168
+ if snapshot.query_text:
169
+ body_renderables.append(
170
+ _build_query_panel(snapshot.query_text),
171
+ )
172
+
173
+ body_renderables.append(
174
+ _build_steps_panel(
175
+ snapshot.rows,
176
+ window_header=snapshot.window_header,
177
+ window_footer=snapshot.window_footer,
178
+ theme=theme,
179
+ )
180
+ )
181
+
182
+ if snapshot.final_panel is not None:
183
+ body_renderables.append(snapshot.final_panel)
184
+
185
+ return header_renderables, body_renderables
186
+
187
+
188
+ def render_final_panel(
189
+ console: Any,
190
+ state: RendererState | Mapping[str, Any],
191
+ *,
192
+ theme: str = DEFAULT_TRANSCRIPT_THEME,
193
+ title: str | None = None,
194
+ ) -> bool:
195
+ """Print the shared final panel, returning True when rendered."""
196
+ panel = build_final_panel(state, theme=theme, title=title)
197
+ if panel is None:
198
+ return False
199
+ console.print(panel)
200
+ console.print()
201
+ return True
202
+
203
+
204
+ __all__ = [
205
+ "DEFAULT_TRANSCRIPT_THEME",
206
+ "TranscriptGlyphs",
207
+ "TranscriptRow",
208
+ "TranscriptSnapshot",
209
+ "build_transcript_snapshot",
210
+ "build_transcript_view",
211
+ "render_final_panel",
212
+ "build_final_panel",
213
+ "format_final_panel_title",
214
+ "extract_query_from_meta",
215
+ ]
216
+
217
+
218
+ def _resolve_state_payload(
219
+ state: RendererState | Mapping[str, Any],
220
+ meta_override: Mapping[str, Any] | Sequence[tuple[str, Any]] | None,
221
+ ) -> tuple[str, str, list[dict[str, Any]], dict[str, Any], str | None]:
222
+ if isinstance(state, RendererState):
223
+ final_text = state.final_text
224
+ buffer_text = state.buffer.render()
225
+ events = list(state.events)
226
+ meta_payload = normalise_meta_payload(meta_override or getattr(state, "meta", None))
227
+ duration_text = state.final_duration_text
228
+ else:
229
+ mapping_state = dict(state) if isinstance(state, Mapping) else {}
230
+ final_text = str(mapping_state.get("final_text") or "")
231
+ buffer_text = str(mapping_state.get("buffer_text") or "")
232
+ events = list(mapping_state.get("events") or [])
233
+ base_meta = mapping_state.get("meta")
234
+ meta_payload = normalise_meta_payload(meta_override or base_meta)
235
+ duration_text = mapping_state.get("final_duration_text")
236
+ return final_text, buffer_text, events, meta_payload, duration_text
237
+
238
+
239
+ def _build_rows(
240
+ steps: StepManager,
241
+ glyphs: TranscriptGlyphs,
242
+ meta_payload: dict[str, Any],
243
+ summary_window: int | None,
244
+ *,
245
+ query_text: str | None,
246
+ step_status_overrides: dict[str, str] | None = None,
247
+ ) -> tuple[list[TranscriptRow], Text | None, Text | None]:
248
+ nodes = list(steps.iter_tree())
249
+ header_notice: Text | None = None
250
+ footer_notice: Text | None = None
251
+ if summary_window is not None and summary_window > 0:
252
+ nodes, header_notice, footer_notice = _apply_summary_window(steps, nodes, summary_window)
253
+
254
+ rows: list[TranscriptRow] = []
255
+ for index, (step_id, branch_state) in enumerate(nodes):
256
+ row = _create_step_row(
257
+ steps,
258
+ glyphs,
259
+ meta_payload,
260
+ step_id,
261
+ branch_state,
262
+ step_status_overrides=step_status_overrides,
263
+ )
264
+ if row is not None:
265
+ rows.append(row)
266
+ if index == 0:
267
+ query_row = _create_query_hint_row(glyphs, query_text)
268
+ if query_row is not None:
269
+ rows.append(query_row)
270
+
271
+ return rows, header_notice, footer_notice
272
+
273
+
274
+ def _apply_summary_window(
275
+ steps: StepManager,
276
+ nodes: list[tuple[str, tuple[bool, ...]]],
277
+ summary_window: int,
278
+ ) -> tuple[list[tuple[str, tuple[bool, ...]]], Text | None, Text | None]:
279
+ """Apply summary window clamping to step nodes."""
280
+
281
+ def _get_label(step_id: str) -> str:
282
+ step = steps.by_id.get(step_id)
283
+ return format_step_label(step) if step else ""
284
+
285
+ def _get_parent(step_id: str) -> str | None:
286
+ step = steps.by_id.get(step_id)
287
+ return step.parent_id if step else None
288
+
289
+ return clamp_step_nodes(
290
+ nodes,
291
+ window=summary_window,
292
+ get_label=_get_label,
293
+ get_parent=_get_parent,
294
+ )
295
+
296
+
297
+ def _create_step_row(
298
+ steps: StepManager,
299
+ glyphs: TranscriptGlyphs,
300
+ meta_payload: dict[str, Any],
301
+ step_id: str,
302
+ branch_state: tuple[bool, ...],
303
+ *,
304
+ step_status_overrides: dict[str, str] | None = None,
305
+ ) -> TranscriptRow | None:
306
+ """Create a transcript row from a step."""
307
+ step = steps.by_id.get(step_id)
308
+ if not step:
309
+ return None
310
+ override = None
311
+ if not branch_state and _should_override_root_label(meta_payload):
312
+ override = _friendly_root_label(meta_payload, step, getattr(step, "display_label", None))
313
+ presentation = format_step(step, glyphs=glyphs, label=override)
314
+ if step_status_overrides:
315
+ status_text = step_status_overrides.get(step_id)
316
+ if status_text:
317
+ presentation.status_text = status_text
318
+ prefix = build_connector_prefix(branch_state)
319
+ return TranscriptRow(prefix=prefix, presentation=presentation)
320
+
321
+
322
+ def _create_query_hint_row(glyphs: TranscriptGlyphs, query_text: str | None) -> TranscriptRow | None:
323
+ """Create a query hint row mirroring the documented transcript output."""
324
+ if not query_text:
325
+ return None
326
+ return TranscriptRow(
327
+ prefix=glyphs.query_prefix,
328
+ presentation=StepPresentation(
329
+ step_id="query",
330
+ title=query_text,
331
+ glyph=None,
332
+ status_style=None,
333
+ args_text=None,
334
+ failure_reason=None,
335
+ duration_ms=None,
336
+ ),
337
+ )
338
+
339
+
340
+ def _rows_from_stored_steps(entries: list[dict[str, Any]]) -> tuple[list[TranscriptRow], Text | None, Text | None]:
341
+ rows: list[TranscriptRow] = []
342
+ for index, entry in enumerate(entries):
343
+ title = entry.get("display_name") or entry.get("name") or "Step"
344
+ finished = entry.get("status") == "finished"
345
+ duration_ms = entry.get("duration_ms")
346
+ presentation = StepPresentation(
347
+ step_id=f"stored-{index}",
348
+ title=str(title),
349
+ glyph="✓" if finished else None,
350
+ status_style="green" if finished else None,
351
+ args_text=None,
352
+ failure_reason=None,
353
+ duration_ms=duration_ms,
354
+ )
355
+ rows.append(TranscriptRow(prefix=" ", presentation=presentation))
356
+ return rows, None, None
357
+
358
+
359
+ def _compose_header_text(snapshot: TranscriptSnapshot) -> Text | None:
360
+ parts: list[str] = []
361
+ if snapshot.agent_label:
362
+ parts.append(snapshot.agent_label)
363
+ if snapshot.model_label:
364
+ parts.append(snapshot.model_label)
365
+ if snapshot.duration_text:
366
+ parts.append(snapshot.duration_text)
367
+ if snapshot.run_id:
368
+ parts.append(snapshot.run_id)
369
+ if not parts:
370
+ return None
371
+ return Text(" · ".join(parts), style="bold")
372
+
373
+
374
+ def _build_query_panel(query_text: str) -> AIPPanel:
375
+ """Build a query panel."""
376
+ return AIPPanel(
377
+ Markdown(f"**Query:** {query_text.strip()}"),
378
+ title="User Request",
379
+ border_style="#d97706",
380
+ padding=(0, 1),
381
+ )
382
+
383
+
384
+ def _build_steps_panel(
385
+ rows: Sequence[TranscriptRow],
386
+ *,
387
+ window_header: Text | None = None,
388
+ window_footer: Text | None = None,
389
+ theme: str = DEFAULT_TRANSCRIPT_THEME,
390
+ ) -> AIPPanel:
391
+ if not rows:
392
+ steps_body: Text | Group = _NO_STEPS_TEXT.copy()
393
+ else:
394
+ rendered = [_format_row_text(row) for row in rows]
395
+ style = "dim" if theme == DEFAULT_TRANSCRIPT_THEME else "default"
396
+ steps_body = Text("\n".join(rendered), style=style)
397
+
398
+ renderables: list[Any] = []
399
+ if window_header is not None:
400
+ renderables.append(window_header)
401
+ renderables.append(steps_body)
402
+ if window_footer is not None:
403
+ renderables.append(window_footer)
404
+
405
+ if len(renderables) == 1:
406
+ body: Any = renderables[0]
407
+ else:
408
+ body = Group(*renderables)
409
+
410
+ return AIPPanel(body, title="Steps", border_style="blue")
411
+
412
+
413
+ def _format_row_text(row: TranscriptRow) -> str:
414
+ prefix = row.prefix
415
+ title, summary = _split_label(row.presentation.title)
416
+ line = f"{prefix}{title}".rstrip()
417
+
418
+ args_lines = _extract_args_lines(row)
419
+ has_args = bool(args_lines)
420
+
421
+ if summary:
422
+ line += f" — {_truncate_summary(summary)}"
423
+ elif has_args:
424
+ line += " —"
425
+
426
+ badge = _format_duration_badge(row.presentation.duration_ms)
427
+ status_text = row.presentation.status_text
428
+ if status_text:
429
+ line += f" {status_text}"
430
+ elif badge:
431
+ line += f" {badge}"
432
+
433
+ if row.presentation.glyph:
434
+ line += f" {row.presentation.glyph}"
435
+
436
+ if row.presentation.failure_reason:
437
+ line += f" {row.presentation.failure_reason}"
438
+
439
+ if has_args:
440
+ for args_line in args_lines:
441
+ line += f"\n{prefix} {args_line}"
442
+
443
+ return line
444
+
445
+
446
+ def _format_duration_badge(duration_ms: int | None) -> str | None:
447
+ if duration_ms is None:
448
+ return None
449
+ try:
450
+ duration_ms = int(duration_ms)
451
+ except Exception:
452
+ return None
453
+ if duration_ms <= 0:
454
+ value = "<1ms"
455
+ elif duration_ms < 1000:
456
+ value = f"{duration_ms}ms"
457
+ else:
458
+ seconds = duration_ms / 1000
459
+ value = f"{seconds:.2f}s"
460
+ return f"[{value}]"
461
+
462
+
463
+ def _extract_args_lines(row: TranscriptRow) -> list[str]:
464
+ args_text = row.presentation.args_text
465
+ if _should_skip_args_summary(row, args_text):
466
+ return []
467
+
468
+ parsed = _parse_args_payload(args_text or "")
469
+ title = row.presentation.title or ""
470
+
471
+ if isinstance(parsed, dict) and parsed:
472
+ if title.startswith(ICON_AGENT) and set(parsed.keys()) == {"query"}:
473
+ return [str(parsed["query"])]
474
+ return [f"{key}: {json.dumps(value, ensure_ascii=False)}" for key, value in parsed.items()]
475
+
476
+ if isinstance(parsed, list):
477
+ return [json.dumps(parsed, ensure_ascii=False)]
478
+
479
+ return [args_text or ""]
480
+
481
+
482
+ def _should_skip_args_summary(row: TranscriptRow, args_text: str | None) -> bool:
483
+ if not args_text or args_text == "{}":
484
+ return True
485
+ title = (row.presentation.title or "").strip()
486
+ if title.startswith("💭 Thinking…"):
487
+ return True
488
+ if not row.prefix.strip():
489
+ return True
490
+ if args_text.strip() == '{"reason":"deterministic_timeline"}':
491
+ return True
492
+ return False
493
+
494
+
495
+ def _parse_args_payload(args_text: str) -> Any | None:
496
+ stripped = args_text.lstrip()
497
+ if stripped.startswith("{") or stripped.startswith("["):
498
+ try:
499
+ return json.loads(args_text)
500
+ except Exception:
501
+ return None
502
+ return None
503
+
504
+
505
+ def _split_label(label: Any) -> tuple[str, str | None]:
506
+ if not isinstance(label, str):
507
+ try:
508
+ label = str(label)
509
+ except Exception:
510
+ return "Step", None
511
+ if "—" not in label:
512
+ return label, None
513
+ title, summary = label.split("—", 1)
514
+ return title.strip(), summary.strip()
515
+
516
+
517
+ def _truncate_summary(summary: str, limit: int = 80) -> str:
518
+ if len(summary) <= limit:
519
+ return summary
520
+ return summary[: limit - 1] + "…"
521
+
522
+
523
+ def _friendly_agent_label(meta: Mapping[str, Any]) -> str | None:
524
+ """Return a user-facing agent label for headers."""
525
+ raw_name = _string_or_none(meta.get("agent_name"))
526
+ if raw_name:
527
+ friendly = humanize_tool_name(raw_name)
528
+ if friendly:
529
+ return friendly
530
+ return _string_or_none(meta.get("agent_id"))
531
+
532
+
533
+ def _friendly_root_label(meta: dict[str, Any], step: Any, fallback: str | None) -> str:
534
+ fallback_label = _string_or_none(fallback)
535
+ raw_agent_name = _string_or_none(meta.get("agent_name"))
536
+ agent_name = humanize_tool_name(raw_agent_name) if raw_agent_name else None
537
+ if agent_name:
538
+ agent_name = agent_name.title()
539
+ agent_name = agent_name or fallback_label
540
+ agent_id = _string_or_none(meta.get("agent_id") or getattr(step, "name", ""))
541
+
542
+ if not agent_name:
543
+ return fallback_label or agent_id or ICON_AGENT
544
+
545
+ parts = [ICON_AGENT, agent_name]
546
+ if agent_id and agent_id != agent_name:
547
+ parts.append(f"({agent_id})")
548
+ return " ".join(parts)
549
+
550
+
551
+ def extract_query_from_meta(meta: Mapping[str, Any] | None) -> str | None:
552
+ """Return the canonical query string embedded in renderer metadata."""
553
+ if not meta:
554
+ return None
555
+
556
+ payload = dict(meta)
557
+ nested_meta = payload.get("meta") or {}
558
+ candidate = (
559
+ payload.get("input_message")
560
+ or payload.get("query")
561
+ or payload.get("message")
562
+ or nested_meta.get("input_message")
563
+ )
564
+ if isinstance(candidate, str):
565
+ candidate = candidate.strip()
566
+ return candidate or None
567
+
568
+
569
+ def _should_override_root_label(meta: Mapping[str, Any] | None) -> bool:
570
+ if not meta:
571
+ return False
572
+ if meta.get("agent_name") or meta.get("agent_id"):
573
+ return True
574
+ return False
575
+
576
+
577
+ def _string_or_none(value: Any) -> str | None:
578
+ if value is None:
579
+ return None
580
+ try:
581
+ text = str(value).strip()
582
+ except Exception:
583
+ return None
584
+ return text or None
585
+
586
+
587
+ def normalise_meta_payload(meta: Any) -> dict[str, Any]:
588
+ """Return a defensive dictionary for arbitrary metadata payloads."""
589
+ if not meta:
590
+ return {}
591
+ if isinstance(meta, dict):
592
+ return dict(meta)
593
+ if isinstance(meta, Mapping):
594
+ try:
595
+ return dict(meta)
596
+ except Exception:
597
+ return {}
598
+ if isinstance(meta, Sequence) and not isinstance(meta, (str, bytes)):
599
+ try:
600
+ return dict(meta)
601
+ except Exception:
602
+ return {}
603
+ try:
604
+ return dict(meta)
605
+ except Exception:
606
+ return {}