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,27 @@
1
+ """Configuration types for the renderer package.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class RendererConfig:
14
+ """Configuration for the RichStreamRenderer."""
15
+
16
+ # Performance
17
+ refresh_debounce: float = 0.25
18
+ render_thinking: bool = True
19
+ live: bool = True
20
+ persist_live: bool = True
21
+ summary_display_window: int = 20
22
+
23
+ # Scrollback/append options
24
+ summary_max_steps: int = 0
25
+ append_finished_snapshots: bool = False
26
+ snapshot_max_chars: int = 0
27
+ snapshot_max_lines: int = 0
@@ -0,0 +1,55 @@
1
+ """Console handling utilities for the renderer package.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import io
10
+ from typing import Any
11
+
12
+ from rich.console import Console as RichConsole
13
+
14
+
15
+ class CapturingConsole:
16
+ """Console wrapper that captures all output for saving."""
17
+
18
+ def __init__(self, original_console: RichConsole, capture: bool = False) -> None:
19
+ """Initialize the capturing console.
20
+
21
+ Args:
22
+ original_console: The original Rich console instance
23
+ capture: Whether to capture output in addition to displaying it
24
+ """
25
+ self.original_console = original_console
26
+ self.capture = capture
27
+ self.captured_output: list[str] = []
28
+
29
+ def print(self, *args: Any, **kwargs: Any) -> None:
30
+ """Print to both original console and capture buffer if capturing."""
31
+ # Always print to original console
32
+ self.original_console.print(*args, **kwargs)
33
+
34
+ if self.capture:
35
+ # Capture the output as text
36
+ # Create a temporary console to capture output
37
+ temp_output = io.StringIO()
38
+ temp_console = RichConsole(
39
+ file=temp_output,
40
+ width=self.original_console.size.width,
41
+ legacy_windows=False,
42
+ force_terminal=False,
43
+ )
44
+ temp_console.print(*args, **kwargs)
45
+ self.captured_output.append(temp_output.getvalue())
46
+
47
+ def get_captured_output(self) -> str:
48
+ """Get the captured output as plain text."""
49
+ if self.capture:
50
+ return "".join(self.captured_output)
51
+ return ""
52
+
53
+ def __getattr__(self, name: str) -> Any:
54
+ """Delegate all other attributes to the original console."""
55
+ return getattr(self.original_console, name)
@@ -0,0 +1,178 @@
1
+ """Debug rendering utilities for verbose SSE event display.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ import json
8
+ from datetime import datetime, timezone
9
+ from typing import Any
10
+ from collections.abc import Callable, Iterable
11
+
12
+ from rich.console import Console
13
+ from rich.markdown import Markdown
14
+
15
+ from glaip_sdk.branding import PRIMARY, SUCCESS, WARNING
16
+ from glaip_sdk.rich_components import AIPPanel
17
+ from glaip_sdk.utils.datetime_helpers import coerce_datetime
18
+
19
+
20
+ def _parse_event_timestamp(event: dict[str, Any], received_ts: datetime | None = None) -> datetime | None:
21
+ """Resolve the most accurate timestamp available for the event."""
22
+ if received_ts is not None:
23
+ return received_ts if received_ts.tzinfo else received_ts.replace(tzinfo=timezone.utc)
24
+
25
+ ts_value = event.get("timestamp") or (event.get("metadata") or {}).get("timestamp")
26
+ return coerce_datetime(ts_value)
27
+
28
+
29
+ def _format_timestamp_for_display(dt: datetime) -> str:
30
+ """Format timestamp for panel title, including timezone offset."""
31
+ local_dt = dt.astimezone()
32
+ ts_ms = local_dt.strftime("%H:%M:%S.%f")[:-3]
33
+ offset = local_dt.strftime("%z")
34
+ # offset is always non-empty for timezone-aware datetimes
35
+ offset = f"{offset[:3]}:{offset[3:]}"
36
+ return f"{ts_ms} {offset}"
37
+
38
+
39
+ def _calculate_relative_time(
40
+ event_ts: datetime | None,
41
+ baseline_ts: datetime | None,
42
+ ) -> tuple[float, str]:
43
+ """Calculate relative time since start and format event timestamp."""
44
+ rel = 0.0
45
+
46
+ # Determine display timestamp - use event timestamp when present, otherwise current time
47
+ display_ts: datetime | None = event_ts
48
+ if display_ts is None:
49
+ display_ts = datetime.now(timezone.utc)
50
+
51
+ if event_ts is not None and baseline_ts is not None:
52
+ rel = max(0.0, (event_ts - baseline_ts).total_seconds())
53
+
54
+ ts_ms = _format_timestamp_for_display(display_ts)
55
+
56
+ return rel, ts_ms
57
+
58
+
59
+ def _get_event_metadata(event: dict[str, Any]) -> tuple[str, str | None]:
60
+ """Extract event kind and status."""
61
+ sse_kind = (event.get("metadata") or {}).get("kind") or "event"
62
+ status_str = event.get("status") or (event.get("metadata") or {}).get("status")
63
+ return sse_kind, status_str
64
+
65
+
66
+ def _build_debug_title(sse_kind: str, status_str: str | None, ts_ms: str, rel: float) -> str:
67
+ """Build the debug event title."""
68
+ if status_str:
69
+ return f"SSE: {sse_kind} — {status_str} @ {ts_ms} (+{rel:.2f}s)"
70
+ else:
71
+ return f"SSE: {sse_kind} @ {ts_ms} (+{rel:.2f}s)"
72
+
73
+
74
+ def _dejson_value(obj: Any) -> Any:
75
+ """Deep-parse JSON strings in nested objects."""
76
+ if isinstance(obj, dict):
77
+ return {k: _dejson_value(v) for k, v in obj.items()}
78
+ if isinstance(obj, list):
79
+ return [_dejson_value(x) for x in obj]
80
+ if isinstance(obj, str):
81
+ s = obj.strip()
82
+ if (s.startswith("{") and s.endswith("}")) or (s.startswith("[") and s.endswith("]")):
83
+ try:
84
+ return _dejson_value(json.loads(s))
85
+ except Exception:
86
+ return obj
87
+ return obj
88
+ return obj
89
+
90
+
91
+ def _format_event_json(event: dict[str, Any]) -> str:
92
+ """Format event as JSON with deep parsing."""
93
+ try:
94
+ return json.dumps(_dejson_value(event), indent=2, ensure_ascii=False)
95
+ except Exception:
96
+ return str(event)
97
+
98
+
99
+ def _get_border_color(sse_kind: str) -> str:
100
+ """Get border color for event type."""
101
+ border_map = {
102
+ "agent_step": PRIMARY,
103
+ "content": SUCCESS,
104
+ "final_response": SUCCESS,
105
+ "status": WARNING,
106
+ "artifact": "grey42",
107
+ }
108
+ return border_map.get(sse_kind, "grey42")
109
+
110
+
111
+ def _create_debug_panel(title: str, event_json: str, border: str) -> AIPPanel:
112
+ """Create the debug panel."""
113
+ md = Markdown(f"```json\n{event_json}\n```", code_theme="monokai")
114
+ return AIPPanel(md, title=title, border_style=border)
115
+
116
+
117
+ def render_debug_event(
118
+ event: dict[str, Any],
119
+ console: Console,
120
+ *,
121
+ received_ts: datetime | None = None,
122
+ baseline_ts: datetime | None = None,
123
+ ) -> None:
124
+ """Render a debug panel for an SSE event.
125
+
126
+ Args:
127
+ event: The SSE event data
128
+ console: Rich console to print to
129
+ received_ts: Client-side receipt timestamp, if available
130
+ baseline_ts: Baseline event timestamp for elapsed timing
131
+ """
132
+ try:
133
+ # Calculate timing information
134
+ event_ts = _parse_event_timestamp(event, received_ts)
135
+ rel, ts_ms = _calculate_relative_time(event_ts, baseline_ts)
136
+
137
+ # Extract event metadata
138
+ sse_kind, status_str = _get_event_metadata(event)
139
+
140
+ # Build title
141
+ title = _build_debug_title(sse_kind, status_str, ts_ms, rel)
142
+
143
+ # Format event JSON
144
+ event_json = _format_event_json(event)
145
+
146
+ # Get border color
147
+ border = _get_border_color(sse_kind)
148
+
149
+ # Create and print panel
150
+ panel = _create_debug_panel(title, event_json, border)
151
+ console.print(panel)
152
+
153
+ except Exception as e:
154
+ # Debug helpers must not break streaming
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
+ )
@@ -0,0 +1,202 @@
1
+ """Event routing and parsing utilities for the renderer package.
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
+ from time import monotonic
11
+ from typing import Any
12
+
13
+
14
+ class StreamProcessor:
15
+ """Handles event routing and parsing for streaming agent execution."""
16
+
17
+ def __init__(self) -> None:
18
+ """Initialize the stream processor."""
19
+ self.streaming_started_at: float | None = None
20
+ self.server_elapsed_time: float | None = None
21
+ self.current_event_tools: set[str] = set()
22
+ self.current_event_sub_agents: set[str] = set()
23
+ self.current_event_finished_panels: set[str] = set()
24
+ self.last_event_time_by_ctx: dict[str, float] = {}
25
+
26
+ def reset_event_tracking(self) -> None:
27
+ """Reset tracking for the current event."""
28
+ self.current_event_tools.clear()
29
+ self.current_event_sub_agents.clear()
30
+ self.current_event_finished_panels.clear()
31
+
32
+ def extract_event_metadata(self, event: dict[str, Any]) -> dict[str, Any]:
33
+ """Extract metadata from an event.
34
+
35
+ Args:
36
+ event: Event dictionary
37
+
38
+ Returns:
39
+ Dictionary with extracted metadata
40
+ """
41
+ metadata = event.get("metadata") or {}
42
+ # Update server elapsed timing if backend provides it
43
+ try:
44
+ t = metadata.get("time")
45
+ if isinstance(t, (int, float)):
46
+ self.server_elapsed_time = float(t)
47
+ except Exception:
48
+ pass
49
+
50
+ return {
51
+ "kind": metadata.get("kind") if metadata else event.get("kind"),
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
+ "content": event.get("content", ""),
55
+ "status": metadata.get("status") if metadata else event.get("status"),
56
+ "metadata": metadata,
57
+ }
58
+
59
+ def _extract_metadata_tool_calls(self, metadata: dict[str, Any]) -> tuple[str | None, dict, Any, list]:
60
+ """Extract tool calls from metadata."""
61
+ tool_calls = metadata.get("tool_calls", [])
62
+ if not tool_calls:
63
+ return None, {}, None, []
64
+
65
+ # Take the first tool call if multiple exist
66
+ first_call = tool_calls[0] if isinstance(tool_calls, list) else tool_calls
67
+ tool_name = first_call.get("name")
68
+ tool_args = first_call.get("arguments", {})
69
+ tool_out = first_call.get("output")
70
+
71
+ # Collect info for all tool calls
72
+ tool_calls_info = []
73
+ for call in tool_calls if isinstance(tool_calls, list) else [tool_calls]:
74
+ if isinstance(call, dict) and "name" in call:
75
+ tool_calls_info.append(
76
+ (
77
+ call.get("name", ""),
78
+ call.get("arguments", {}),
79
+ call.get("output"),
80
+ )
81
+ )
82
+
83
+ return tool_name, tool_args, tool_out, tool_calls_info
84
+
85
+ def _extract_tool_info_calls(self, tool_info: dict[str, Any]) -> tuple[str | None, dict, Any, list]:
86
+ """Extract tool calls from tool_info structure."""
87
+ tool_calls_info = []
88
+ tool_name = None
89
+ tool_args = {}
90
+ tool_out = None
91
+
92
+ # Case 1: tool_info.tool_calls
93
+ ti_calls = tool_info.get("tool_calls")
94
+ if isinstance(ti_calls, list) and ti_calls:
95
+ for call in ti_calls:
96
+ if isinstance(call, dict) and call.get("name"):
97
+ tool_calls_info.append((call.get("name"), call.get("args", {}), call.get("output")))
98
+ if tool_calls_info:
99
+ tool_name, tool_args, tool_out = tool_calls_info[0]
100
+ return tool_name, tool_args, tool_out, tool_calls_info
101
+
102
+ # Case 2: single tool_info name/args/output
103
+ if tool_info.get("name"):
104
+ tool_name = tool_info.get("name")
105
+ tool_args = tool_info.get("args", {})
106
+ tool_out = tool_info.get("output")
107
+ tool_calls_info.append((tool_name, tool_args, tool_out))
108
+
109
+ return tool_name, tool_args, tool_out, tool_calls_info
110
+
111
+ def _extract_tool_calls_from_metadata(self, metadata: dict[str, Any]) -> tuple[str | None, dict, Any, list]:
112
+ """Extract tool calls from metadata structure."""
113
+ tool_info = metadata.get("tool_info", {}) or {}
114
+
115
+ if tool_info:
116
+ return self._extract_tool_info_calls(tool_info)
117
+
118
+ return None, {}, None, []
119
+
120
+ def parse_tool_calls(self, event: dict[str, Any]) -> tuple[str | None, Any, Any, list[tuple[str, Any, Any]]]:
121
+ """Parse tool call information from an event.
122
+
123
+ Args:
124
+ event: Event dictionary
125
+
126
+ Returns:
127
+ Tuple of (tool_name, tool_args, tool_output, tool_calls_info)
128
+ """
129
+ metadata = event.get("metadata", {})
130
+
131
+ # Try primary extraction method
132
+ tool_calls_result = self._extract_metadata_tool_calls(metadata)
133
+ tool_name, tool_args, tool_out, tool_calls_info = tool_calls_result
134
+
135
+ # Fallback to nested metadata.tool_info (newer schema)
136
+ if not tool_calls_info:
137
+ fallback_result = self._extract_tool_calls_from_metadata(metadata)
138
+ tool_name, tool_args, tool_out, tool_calls_info = fallback_result
139
+
140
+ return tool_name, tool_args, tool_out, tool_calls_info
141
+
142
+ def update_timing(self, context_id: str | None) -> None:
143
+ """Update timing information for the given context.
144
+
145
+ Args:
146
+ context_id: Context identifier
147
+ """
148
+ if context_id:
149
+ self.last_event_time_by_ctx[context_id] = monotonic()
150
+
151
+ def track_tools_and_agents(
152
+ self,
153
+ tool_name: str | None,
154
+ tool_calls_info: list[tuple[str, Any, Any]],
155
+ is_delegation_tool_func: Callable[[str], bool],
156
+ ) -> None:
157
+ """Track tools and sub-agents mentioned in the current event.
158
+
159
+ Args:
160
+ tool_name: Primary tool name
161
+ tool_calls_info: List of tool call information
162
+ is_delegation_tool_func: Function to check if tool is delegation
163
+ """
164
+ # Track all tools mentioned in this event
165
+ if tool_name:
166
+ self.current_event_tools.add(tool_name)
167
+ # If it's a delegation tool, add the sub-agent name
168
+ if is_delegation_tool_func(tool_name):
169
+ sub_agent_name = self._extract_sub_agent_name(tool_name)
170
+ self.current_event_sub_agents.add(sub_agent_name)
171
+
172
+ if tool_calls_info:
173
+ for tool_call_name, _, _ in tool_calls_info:
174
+ self.current_event_tools.add(tool_call_name)
175
+ # If it's a delegation tool, add the sub-agent name
176
+ if is_delegation_tool_func(tool_call_name):
177
+ sub_agent_name = self._extract_sub_agent_name(tool_call_name)
178
+ self.current_event_sub_agents.add(sub_agent_name)
179
+
180
+ def _extract_sub_agent_name(self, tool_name: str) -> str:
181
+ """Extract sub-agent name from delegation tool name.
182
+
183
+ Args:
184
+ tool_name: Delegation tool name
185
+
186
+ Returns:
187
+ Sub-agent name
188
+ """
189
+ if tool_name.startswith("delegate_to_"):
190
+ return tool_name.replace("delegate_to_", "")
191
+ elif tool_name.startswith("delegate_"):
192
+ return tool_name.replace("delegate_", "")
193
+ else:
194
+ return tool_name
195
+
196
+ def get_current_event_tools(self) -> set[str]:
197
+ """Get the set of tools mentioned in the current event."""
198
+ return self.current_event_tools.copy()
199
+
200
+ def get_current_event_sub_agents(self) -> set[str]:
201
+ """Get the set of sub-agents mentioned in the current event."""
202
+ return self.current_event_sub_agents.copy()
@@ -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