glaip-sdk 0.0.20__py3-none-any.whl → 0.7.7__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 (216) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1250 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +271 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  12. glaip_sdk/cli/commands/agents/_common.py +561 -0
  13. glaip_sdk/cli/commands/agents/create.py +151 -0
  14. glaip_sdk/cli/commands/agents/delete.py +64 -0
  15. glaip_sdk/cli/commands/agents/get.py +89 -0
  16. glaip_sdk/cli/commands/agents/list.py +129 -0
  17. glaip_sdk/cli/commands/agents/run.py +264 -0
  18. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  19. glaip_sdk/cli/commands/agents/update.py +112 -0
  20. glaip_sdk/cli/commands/common_config.py +104 -0
  21. glaip_sdk/cli/commands/configure.py +734 -143
  22. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  23. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  24. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  25. glaip_sdk/cli/commands/mcps/create.py +152 -0
  26. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  27. glaip_sdk/cli/commands/mcps/get.py +212 -0
  28. glaip_sdk/cli/commands/mcps/list.py +69 -0
  29. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  30. glaip_sdk/cli/commands/mcps/update.py +190 -0
  31. glaip_sdk/cli/commands/models.py +14 -12
  32. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  33. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  34. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  35. glaip_sdk/cli/commands/tools/_common.py +80 -0
  36. glaip_sdk/cli/commands/tools/create.py +228 -0
  37. glaip_sdk/cli/commands/tools/delete.py +61 -0
  38. glaip_sdk/cli/commands/tools/get.py +103 -0
  39. glaip_sdk/cli/commands/tools/list.py +69 -0
  40. glaip_sdk/cli/commands/tools/script.py +49 -0
  41. glaip_sdk/cli/commands/tools/update.py +102 -0
  42. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  43. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  44. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  45. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  46. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  47. glaip_sdk/cli/commands/update.py +164 -23
  48. glaip_sdk/cli/config.py +49 -7
  49. glaip_sdk/cli/constants.py +38 -0
  50. glaip_sdk/cli/context.py +8 -0
  51. glaip_sdk/cli/core/__init__.py +79 -0
  52. glaip_sdk/cli/core/context.py +124 -0
  53. glaip_sdk/cli/core/output.py +851 -0
  54. glaip_sdk/cli/core/prompting.py +649 -0
  55. glaip_sdk/cli/core/rendering.py +187 -0
  56. glaip_sdk/cli/display.py +45 -32
  57. glaip_sdk/cli/entrypoint.py +20 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +14 -17
  60. glaip_sdk/cli/main.py +344 -167
  61. glaip_sdk/cli/masking.py +21 -33
  62. glaip_sdk/cli/mcp_validators.py +5 -15
  63. glaip_sdk/cli/pager.py +15 -22
  64. glaip_sdk/cli/parsers/__init__.py +1 -3
  65. glaip_sdk/cli/parsers/json_input.py +11 -22
  66. glaip_sdk/cli/resolution.py +5 -10
  67. glaip_sdk/cli/rich_helpers.py +1 -3
  68. glaip_sdk/cli/slash/__init__.py +0 -9
  69. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  70. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  71. glaip_sdk/cli/slash/agent_session.py +65 -29
  72. glaip_sdk/cli/slash/prompt.py +24 -10
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +827 -232
  75. glaip_sdk/cli/slash/tui/__init__.py +34 -0
  76. glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
  77. glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
  78. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  79. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  80. glaip_sdk/cli/slash/tui/context.py +59 -0
  81. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  82. glaip_sdk/cli/slash/tui/loading.py +58 -0
  83. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  84. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  85. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  86. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  87. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  88. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  89. glaip_sdk/cli/slash/tui/toast.py +123 -0
  90. glaip_sdk/cli/transcript/__init__.py +12 -52
  91. glaip_sdk/cli/transcript/cache.py +258 -60
  92. glaip_sdk/cli/transcript/capture.py +72 -21
  93. glaip_sdk/cli/transcript/history.py +815 -0
  94. glaip_sdk/cli/transcript/launcher.py +1 -3
  95. glaip_sdk/cli/transcript/viewer.py +79 -329
  96. glaip_sdk/cli/update_notifier.py +385 -24
  97. glaip_sdk/cli/validators.py +16 -18
  98. glaip_sdk/client/__init__.py +3 -1
  99. glaip_sdk/client/_schedule_payloads.py +89 -0
  100. glaip_sdk/client/agent_runs.py +147 -0
  101. glaip_sdk/client/agents.py +370 -100
  102. glaip_sdk/client/base.py +78 -35
  103. glaip_sdk/client/hitl.py +136 -0
  104. glaip_sdk/client/main.py +25 -10
  105. glaip_sdk/client/mcps.py +166 -27
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +583 -79
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +214 -56
  113. glaip_sdk/client/validators.py +20 -48
  114. glaip_sdk/config/constants.py +11 -0
  115. glaip_sdk/exceptions.py +1 -3
  116. glaip_sdk/hitl/__init__.py +48 -0
  117. glaip_sdk/hitl/base.py +64 -0
  118. glaip_sdk/hitl/callback.py +43 -0
  119. glaip_sdk/hitl/local.py +121 -0
  120. glaip_sdk/hitl/remote.py +523 -0
  121. glaip_sdk/icons.py +9 -3
  122. glaip_sdk/mcps/__init__.py +21 -0
  123. glaip_sdk/mcps/base.py +345 -0
  124. glaip_sdk/models/__init__.py +107 -0
  125. glaip_sdk/models/agent.py +47 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/mcp.py +33 -0
  129. glaip_sdk/models/schedule.py +224 -0
  130. glaip_sdk/models/tool.py +33 -0
  131. glaip_sdk/payload_schemas/__init__.py +1 -13
  132. glaip_sdk/payload_schemas/agent.py +1 -3
  133. glaip_sdk/registry/__init__.py +55 -0
  134. glaip_sdk/registry/agent.py +164 -0
  135. glaip_sdk/registry/base.py +139 -0
  136. glaip_sdk/registry/mcp.py +253 -0
  137. glaip_sdk/registry/tool.py +445 -0
  138. glaip_sdk/rich_components.py +58 -2
  139. glaip_sdk/runner/__init__.py +76 -0
  140. glaip_sdk/runner/base.py +84 -0
  141. glaip_sdk/runner/deps.py +112 -0
  142. glaip_sdk/runner/langgraph.py +872 -0
  143. glaip_sdk/runner/logging_config.py +77 -0
  144. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  145. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  146. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  147. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  148. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  149. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  150. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  151. glaip_sdk/schedules/__init__.py +22 -0
  152. glaip_sdk/schedules/base.py +291 -0
  153. glaip_sdk/tools/__init__.py +22 -0
  154. glaip_sdk/tools/base.py +468 -0
  155. glaip_sdk/utils/__init__.py +59 -12
  156. glaip_sdk/utils/a2a/__init__.py +34 -0
  157. glaip_sdk/utils/a2a/event_processor.py +188 -0
  158. glaip_sdk/utils/agent_config.py +4 -14
  159. glaip_sdk/utils/bundler.py +403 -0
  160. glaip_sdk/utils/client.py +111 -0
  161. glaip_sdk/utils/client_utils.py +46 -28
  162. glaip_sdk/utils/datetime_helpers.py +58 -0
  163. glaip_sdk/utils/discovery.py +78 -0
  164. glaip_sdk/utils/display.py +25 -21
  165. glaip_sdk/utils/export.py +143 -0
  166. glaip_sdk/utils/general.py +1 -36
  167. glaip_sdk/utils/import_export.py +15 -16
  168. glaip_sdk/utils/import_resolver.py +524 -0
  169. glaip_sdk/utils/instructions.py +101 -0
  170. glaip_sdk/utils/rendering/__init__.py +115 -1
  171. glaip_sdk/utils/rendering/formatting.py +38 -23
  172. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  173. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  174. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  175. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  176. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  177. glaip_sdk/utils/rendering/models.py +18 -8
  178. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  179. glaip_sdk/utils/rendering/renderer/base.py +534 -882
  180. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  181. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  182. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  183. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  184. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  185. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  186. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  187. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  188. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  189. glaip_sdk/utils/rendering/state.py +204 -0
  190. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  191. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  192. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  193. glaip_sdk/utils/rendering/steps/format.py +176 -0
  194. glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
  195. glaip_sdk/utils/rendering/timing.py +36 -0
  196. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  197. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  198. glaip_sdk/utils/resource_refs.py +29 -26
  199. glaip_sdk/utils/runtime_config.py +425 -0
  200. glaip_sdk/utils/serialization.py +32 -46
  201. glaip_sdk/utils/sync.py +162 -0
  202. glaip_sdk/utils/tool_detection.py +301 -0
  203. glaip_sdk/utils/tool_storage_provider.py +140 -0
  204. glaip_sdk/utils/validation.py +20 -28
  205. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
  206. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  207. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  208. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  209. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  210. glaip_sdk/cli/commands/agents.py +0 -1412
  211. glaip_sdk/cli/commands/mcps.py +0 -1225
  212. glaip_sdk/cli/commands/tools.py +0 -597
  213. glaip_sdk/cli/utils.py +0 -1330
  214. glaip_sdk/models.py +0 -259
  215. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  216. glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
@@ -13,21 +13,15 @@ from dataclasses import dataclass
13
13
  class RendererConfig:
14
14
  """Configuration for the RichStreamRenderer."""
15
15
 
16
- # Style and layout
17
- theme: str = "dark" # dark|light
18
- style: str = "pretty" # pretty|debug|minimal
19
-
20
16
  # Performance
21
- think_threshold: float = 0.7
22
17
  refresh_debounce: float = 0.25
23
18
  render_thinking: bool = True
24
19
  live: bool = True
25
20
  persist_live: bool = True
26
-
27
- # Debug visibility toggles
28
- show_delegate_tool_panels: bool = False
21
+ summary_display_window: int = 20
29
22
 
30
23
  # Scrollback/append options
24
+ summary_max_steps: int = 0
31
25
  append_finished_snapshots: bool = False
32
- snapshot_max_chars: int = 12000
33
- snapshot_max_lines: int = 200
26
+ snapshot_max_chars: int = 0
27
+ snapshot_max_lines: int = 0
@@ -7,46 +7,23 @@ Authors:
7
7
  import json
8
8
  from datetime import datetime, timezone
9
9
  from typing import Any
10
+ from collections.abc import Callable, Iterable
10
11
 
11
12
  from rich.console import Console
12
13
  from rich.markdown import Markdown
13
14
 
14
15
  from glaip_sdk.branding import PRIMARY, SUCCESS, WARNING
15
16
  from glaip_sdk.rich_components import AIPPanel
17
+ from glaip_sdk.utils.datetime_helpers import coerce_datetime
16
18
 
17
19
 
18
- def _coerce_datetime(value: Any) -> datetime | None:
19
- """Attempt to coerce an arbitrary value to an aware datetime."""
20
- if value is None:
21
- return None
22
-
23
- if isinstance(value, datetime):
24
- return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
25
-
26
- if isinstance(value, str):
27
- try:
28
- normalised = value.replace("Z", "+00:00")
29
- dt = datetime.fromisoformat(normalised)
30
- except ValueError:
31
- return None
32
- return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
33
-
34
- return None
35
-
36
-
37
- def _parse_event_timestamp(
38
- event: dict[str, Any], received_ts: datetime | None = None
39
- ) -> datetime | None:
20
+ def _parse_event_timestamp(event: dict[str, Any], received_ts: datetime | None = None) -> datetime | None:
40
21
  """Resolve the most accurate timestamp available for the event."""
41
22
  if received_ts is not None:
42
- return (
43
- received_ts
44
- if received_ts.tzinfo
45
- else received_ts.replace(tzinfo=timezone.utc)
46
- )
23
+ return received_ts if received_ts.tzinfo else received_ts.replace(tzinfo=timezone.utc)
47
24
 
48
25
  ts_value = event.get("timestamp") or (event.get("metadata") or {}).get("timestamp")
49
- return _coerce_datetime(ts_value)
26
+ return coerce_datetime(ts_value)
50
27
 
51
28
 
52
29
  def _format_timestamp_for_display(dt: datetime) -> str:
@@ -86,9 +63,7 @@ def _get_event_metadata(event: dict[str, Any]) -> tuple[str, str | None]:
86
63
  return sse_kind, status_str
87
64
 
88
65
 
89
- def _build_debug_title(
90
- sse_kind: str, status_str: str | None, ts_ms: str, rel: float
91
- ) -> str:
66
+ def _build_debug_title(sse_kind: str, status_str: str | None, ts_ms: str, rel: float) -> str:
92
67
  """Build the debug event title."""
93
68
  if status_str:
94
69
  return f"SSE: {sse_kind} — {status_str} @ {ts_ms} (+{rel:.2f}s)"
@@ -104,9 +79,7 @@ def _dejson_value(obj: Any) -> Any:
104
79
  return [_dejson_value(x) for x in obj]
105
80
  if isinstance(obj, str):
106
81
  s = obj.strip()
107
- if (s.startswith("{") and s.endswith("}")) or (
108
- s.startswith("[") and s.endswith("]")
109
- ):
82
+ if (s.startswith("{") and s.endswith("}")) or (s.startswith("[") and s.endswith("]")):
110
83
  try:
111
84
  return _dejson_value(json.loads(s))
112
85
  except Exception:
@@ -180,3 +153,26 @@ def render_debug_event(
180
153
  except Exception as e:
181
154
  # Debug helpers must not break streaming
182
155
  print(f"Debug error: {e}") # Fallback debug output
156
+
157
+
158
+ def render_debug_event_stream(
159
+ events: Iterable[dict[str, Any]],
160
+ console: Console,
161
+ *,
162
+ resolve_timestamp: Callable[[dict[str, Any]], datetime | None],
163
+ ) -> None:
164
+ """Render a sequence of SSE events with baseline-aware timestamps."""
165
+ baseline: datetime | None = None
166
+ for event in events:
167
+ try:
168
+ received_ts = resolve_timestamp(event)
169
+ if baseline is None and received_ts is not None:
170
+ baseline = received_ts
171
+ render_debug_event(
172
+ event,
173
+ console,
174
+ received_ts=received_ts,
175
+ baseline_ts=baseline,
176
+ )
177
+ except Exception as exc: # pragma: no cover - debug stream resilience
178
+ console.print(f"[red]Debug stream error: {exc}[/red]")
@@ -0,0 +1,138 @@
1
+ """Renderer factory helpers for CLI, SDK, and slash sessions.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import io
10
+ from dataclasses import dataclass, is_dataclass, replace
11
+ from inspect import signature
12
+ from typing import Any
13
+ from collections.abc import Callable
14
+
15
+ from rich.console import Console
16
+
17
+ from glaip_sdk.utils.rendering.renderer.base import RichStreamRenderer
18
+ from glaip_sdk.utils.rendering.renderer.config import RendererConfig
19
+ from glaip_sdk.utils.rendering.state import TranscriptBuffer
20
+
21
+
22
+ @dataclass(slots=True)
23
+ class RendererFactoryOptions:
24
+ """Shared options for renderer factories."""
25
+
26
+ console: Console | None = None
27
+ cfg_overrides: dict[str, Any] | None = None
28
+ verbose: bool | None = None
29
+ transcript_buffer: TranscriptBuffer | None = None
30
+ callbacks: dict[str, Any] | None = None
31
+
32
+ def build(self, factory: Callable[..., RichStreamRenderer]) -> RichStreamRenderer:
33
+ """Instantiate a renderer using the provided factory and stored options."""
34
+ params = signature(factory).parameters
35
+ kwargs: dict[str, Any] = {}
36
+ if self.console is not None and "console" in params:
37
+ kwargs["console"] = self.console
38
+ if self.cfg_overrides is not None and "cfg_overrides" in params:
39
+ kwargs["cfg_overrides"] = self.cfg_overrides
40
+ if self.verbose is not None and "verbose" in params:
41
+ kwargs["verbose"] = self.verbose
42
+ if self.transcript_buffer is not None and "transcript_buffer" in params:
43
+ kwargs["transcript_buffer"] = self.transcript_buffer
44
+ if self.callbacks is not None and "callbacks" in params:
45
+ kwargs["callbacks"] = self.callbacks
46
+ return factory(**kwargs)
47
+
48
+
49
+ def _build_config(base: RendererConfig, overrides: dict[str, Any] | None = None) -> RendererConfig:
50
+ cfg = replace(base) if is_dataclass(base) else base
51
+ if overrides:
52
+ for key, value in overrides.items():
53
+ if hasattr(cfg, key):
54
+ setattr(cfg, key, value)
55
+ return cfg
56
+
57
+
58
+ def make_default_renderer(
59
+ *,
60
+ console: Console | None = None,
61
+ cfg_overrides: dict[str, Any] | None = None,
62
+ verbose: bool = False,
63
+ transcript_buffer: TranscriptBuffer | None = None,
64
+ callbacks: dict[str, Any] | None = None,
65
+ ) -> RichStreamRenderer:
66
+ """Create the default renderer used by SDK and CLI flows."""
67
+ cfg = _build_config(RendererConfig(), cfg_overrides)
68
+ return RichStreamRenderer(
69
+ console=console or Console(),
70
+ cfg=cfg,
71
+ verbose=verbose,
72
+ transcript_buffer=transcript_buffer,
73
+ callbacks=callbacks,
74
+ )
75
+
76
+
77
+ def make_verbose_renderer(
78
+ *,
79
+ console: Console | None = None,
80
+ cfg_overrides: dict[str, Any] | None = None,
81
+ transcript_buffer: TranscriptBuffer | None = None,
82
+ callbacks: dict[str, Any] | None = None,
83
+ ) -> RichStreamRenderer:
84
+ """Create a verbose renderer with snapshot appending disabled."""
85
+ verbose_cfg = RendererConfig(live=True, append_finished_snapshots=False)
86
+ cfg = _build_config(verbose_cfg, cfg_overrides)
87
+ return RichStreamRenderer(
88
+ console=console or Console(),
89
+ cfg=cfg,
90
+ verbose=True,
91
+ transcript_buffer=transcript_buffer,
92
+ callbacks=callbacks,
93
+ )
94
+
95
+
96
+ def make_minimal_renderer(
97
+ *,
98
+ console: Console | None = None,
99
+ cfg_overrides: dict[str, Any] | None = None,
100
+ transcript_buffer: TranscriptBuffer | None = None,
101
+ callbacks: dict[str, Any] | None = None,
102
+ ) -> RichStreamRenderer:
103
+ """Create a renderer that prints only essential output."""
104
+ minimal_cfg = RendererConfig(live=False, persist_live=False, render_thinking=False)
105
+ cfg = _build_config(minimal_cfg, cfg_overrides)
106
+ return RichStreamRenderer(
107
+ console=console or Console(),
108
+ cfg=cfg,
109
+ verbose=False,
110
+ transcript_buffer=transcript_buffer,
111
+ callbacks=callbacks,
112
+ )
113
+
114
+
115
+ def make_silent_renderer(
116
+ *,
117
+ console: Console | None = None,
118
+ cfg_overrides: dict[str, Any] | None = None,
119
+ transcript_buffer: TranscriptBuffer | None = None,
120
+ callbacks: dict[str, Any] | None = None,
121
+ ) -> RichStreamRenderer:
122
+ """Create a renderer that suppresses terminal output for background flows."""
123
+ cfg = _build_config(
124
+ RendererConfig(
125
+ live=False,
126
+ persist_live=False,
127
+ render_thinking=False,
128
+ ),
129
+ cfg_overrides,
130
+ )
131
+ silent_console = console or Console(file=io.StringIO(), force_terminal=False)
132
+ return RichStreamRenderer(
133
+ console=silent_console,
134
+ cfg=cfg,
135
+ verbose=False,
136
+ transcript_buffer=transcript_buffer,
137
+ callbacks=callbacks,
138
+ )
@@ -38,27 +38,25 @@ class StreamProcessor:
38
38
  Returns:
39
39
  Dictionary with extracted metadata
40
40
  """
41
- metadata = event.get("metadata", {})
41
+ metadata = event.get("metadata") or {}
42
42
  # Update server elapsed timing if backend provides it
43
43
  try:
44
44
  t = metadata.get("time")
45
- if isinstance(t, int | float):
45
+ if isinstance(t, (int, float)):
46
46
  self.server_elapsed_time = float(t)
47
47
  except Exception:
48
48
  pass
49
49
 
50
50
  return {
51
51
  "kind": metadata.get("kind") if metadata else event.get("kind"),
52
- "task_id": event.get("task_id"),
53
- "context_id": event.get("context_id"),
52
+ "task_id": metadata.get("task_id") or event.get("task_id"),
53
+ "context_id": metadata.get("context_id") or event.get("context_id"),
54
54
  "content": event.get("content", ""),
55
55
  "status": metadata.get("status") if metadata else event.get("status"),
56
56
  "metadata": metadata,
57
57
  }
58
58
 
59
- def _extract_metadata_tool_calls(
60
- self, metadata: dict[str, Any]
61
- ) -> tuple[str | None, dict, Any, list]:
59
+ def _extract_metadata_tool_calls(self, metadata: dict[str, Any]) -> tuple[str | None, dict, Any, list]:
62
60
  """Extract tool calls from metadata."""
63
61
  tool_calls = metadata.get("tool_calls", [])
64
62
  if not tool_calls:
@@ -84,9 +82,7 @@ class StreamProcessor:
84
82
 
85
83
  return tool_name, tool_args, tool_out, tool_calls_info
86
84
 
87
- def _extract_tool_info_calls(
88
- self, tool_info: dict[str, Any]
89
- ) -> tuple[str | None, dict, Any, list]:
85
+ def _extract_tool_info_calls(self, tool_info: dict[str, Any]) -> tuple[str | None, dict, Any, list]:
90
86
  """Extract tool calls from tool_info structure."""
91
87
  tool_calls_info = []
92
88
  tool_name = None
@@ -98,9 +94,7 @@ class StreamProcessor:
98
94
  if isinstance(ti_calls, list) and ti_calls:
99
95
  for call in ti_calls:
100
96
  if isinstance(call, dict) and call.get("name"):
101
- tool_calls_info.append(
102
- (call.get("name"), call.get("args", {}), call.get("output"))
103
- )
97
+ tool_calls_info.append((call.get("name"), call.get("args", {}), call.get("output")))
104
98
  if tool_calls_info:
105
99
  tool_name, tool_args, tool_out = tool_calls_info[0]
106
100
  return tool_name, tool_args, tool_out, tool_calls_info
@@ -114,9 +108,7 @@ class StreamProcessor:
114
108
 
115
109
  return tool_name, tool_args, tool_out, tool_calls_info
116
110
 
117
- def _extract_tool_calls_from_metadata(
118
- self, metadata: dict[str, Any]
119
- ) -> tuple[str | None, dict, Any, list]:
111
+ def _extract_tool_calls_from_metadata(self, metadata: dict[str, Any]) -> tuple[str | None, dict, Any, list]:
120
112
  """Extract tool calls from metadata structure."""
121
113
  tool_info = metadata.get("tool_info", {}) or {}
122
114
 
@@ -125,9 +117,7 @@ class StreamProcessor:
125
117
 
126
118
  return None, {}, None, []
127
119
 
128
- def parse_tool_calls(
129
- self, event: dict[str, Any]
130
- ) -> tuple[str | None, Any, Any, list[tuple[str, Any, Any]]]:
120
+ def parse_tool_calls(self, event: dict[str, Any]) -> tuple[str | None, Any, Any, list[tuple[str, Any, Any]]]:
131
121
  """Parse tool call information from an event.
132
122
 
133
123
  Args:
@@ -139,21 +129,13 @@ class StreamProcessor:
139
129
  metadata = event.get("metadata", {})
140
130
 
141
131
  # Try primary extraction method
142
- (
143
- tool_name,
144
- tool_args,
145
- tool_out,
146
- tool_calls_info,
147
- ) = self._extract_metadata_tool_calls(metadata)
132
+ tool_calls_result = self._extract_metadata_tool_calls(metadata)
133
+ tool_name, tool_args, tool_out, tool_calls_info = tool_calls_result
148
134
 
149
135
  # Fallback to nested metadata.tool_info (newer schema)
150
136
  if not tool_calls_info:
151
- (
152
- tool_name,
153
- tool_args,
154
- tool_out,
155
- tool_calls_info,
156
- ) = self._extract_tool_calls_from_metadata(metadata)
137
+ fallback_result = self._extract_tool_calls_from_metadata(metadata)
138
+ tool_name, tool_args, tool_out, tool_calls_info = fallback_result
157
139
 
158
140
  return tool_name, tool_args, tool_out, tool_calls_info
159
141
 
@@ -166,29 +148,6 @@ class StreamProcessor:
166
148
  if context_id:
167
149
  self.last_event_time_by_ctx[context_id] = monotonic()
168
150
 
169
- def should_insert_thinking_gap(
170
- self, task_id: str | None, context_id: str | None, think_threshold: float
171
- ) -> bool:
172
- """Determine if a thinking gap should be inserted.
173
-
174
- Args:
175
- task_id: Task identifier
176
- context_id: Context identifier
177
- think_threshold: Threshold for thinking gap
178
-
179
- Returns:
180
- True if thinking gap should be inserted
181
- """
182
- if not task_id or not context_id:
183
- return False
184
-
185
- last_time = self.last_event_time_by_ctx.get(context_id)
186
- if last_time is None:
187
- return True
188
-
189
- elapsed = monotonic() - last_time
190
- return elapsed >= think_threshold
191
-
192
151
  def track_tools_and_agents(
193
152
  self,
194
153
  tool_name: str | None,
@@ -0,0 +1,79 @@
1
+ """Helpers for clamping the steps summary view to a rolling window.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+
11
+ from rich.text import Text
12
+
13
+ Node = tuple[str, tuple[bool, ...]]
14
+ LabelFn = Callable[[str], str]
15
+ ParentFn = Callable[[str], str | None]
16
+
17
+
18
+ def clamp_step_nodes(
19
+ nodes: list[Node],
20
+ *,
21
+ window: int,
22
+ get_label: LabelFn,
23
+ get_parent: ParentFn,
24
+ ) -> tuple[list[Node], Text | None, Text | None]:
25
+ """Return a windowed slice of nodes plus optional header/footer notices."""
26
+ if window <= 0 or len(nodes) <= window:
27
+ return nodes, None, None
28
+
29
+ start_index = len(nodes) - window
30
+ first_visible_step_id = nodes[start_index][0]
31
+ header = _build_header(first_visible_step_id, window, len(nodes), get_label, get_parent)
32
+ footer = _build_footer(len(nodes) - window)
33
+ return nodes[start_index:], header, footer
34
+
35
+
36
+ def _build_header(
37
+ step_id: str,
38
+ window: int,
39
+ total: int,
40
+ get_label: LabelFn,
41
+ get_parent: ParentFn,
42
+ ) -> Text:
43
+ """Construct the leading notice for a truncated window."""
44
+ parts = [f"… (latest {window} of {total} steps shown"]
45
+ path = _collect_path_labels(step_id, get_label, get_parent)
46
+ if path:
47
+ parts.append("; continuing with ")
48
+ parts.append(" / ".join(path))
49
+ parts.append(")")
50
+ return Text("".join(parts), style="dim")
51
+
52
+
53
+ def _build_footer(hidden_count: int) -> Text:
54
+ """Construct the footer notice indicating hidden steps."""
55
+ noun = "step" if hidden_count == 1 else "steps"
56
+ message = f"{hidden_count} earlier {noun} hidden. Press Ctrl+T to inspect the full transcript."
57
+ return Text(message, style="dim")
58
+
59
+
60
+ def _collect_path_labels(
61
+ step_id: str,
62
+ get_label: LabelFn,
63
+ get_parent: ParentFn,
64
+ ) -> list[str]:
65
+ """Collect labels for the ancestry of the provided step."""
66
+ labels: list[str] = []
67
+ seen: set[str] = set()
68
+ current = step_id
69
+ while current and current not in seen:
70
+ seen.add(current)
71
+ label = get_label(current)
72
+ if label:
73
+ labels.append(label)
74
+ parent = get_parent(current)
75
+ if not parent:
76
+ break
77
+ current = parent
78
+ labels.reverse()
79
+ return labels