glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -7,11 +7,14 @@ Authors:
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import io
11
10
  import json
12
11
  import logging
12
+ from collections.abc import AsyncIterable, Callable
13
13
  from time import monotonic
14
- from typing import Any
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ if TYPE_CHECKING:
17
+ from glaip_sdk.hitl.remote import RemoteHITLHandler
15
18
 
16
19
  import httpx
17
20
  from rich.console import Console as _Console
@@ -19,8 +22,18 @@ from rich.console import Console as _Console
19
22
  from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
20
23
  from glaip_sdk.utils.client_utils import iter_sse_events
21
24
  from glaip_sdk.utils.rendering.models import RunStats
22
- from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
23
- from glaip_sdk.utils.rendering.renderer.config import RendererConfig
25
+ from glaip_sdk.utils.rendering.renderer import (
26
+ RendererFactoryOptions,
27
+ RichStreamRenderer,
28
+ make_default_renderer,
29
+ make_minimal_renderer,
30
+ make_silent_renderer,
31
+ make_verbose_renderer,
32
+ )
33
+ from glaip_sdk.utils.rendering.state import TranscriptBuffer
34
+
35
+ NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
36
+ _FINAL_EVENT_TYPES = {"final_response", "error", "step_limit_exceeded"}
24
37
 
25
38
 
26
39
  def _coerce_to_string(value: Any) -> str:
@@ -36,41 +49,6 @@ def _has_visible_text(value: Any) -> bool:
36
49
  return isinstance(value, str) and bool(value.strip())
37
50
 
38
51
 
39
- def _update_state_transcript(state: Any, text_value: str) -> bool:
40
- """Inject transcript text into renderer state if possible."""
41
- if state is None:
42
- return False
43
-
44
- updated = False
45
-
46
- if hasattr(state, "final_text") and not _has_visible_text(getattr(state, "final_text", "")):
47
- try:
48
- state.final_text = text_value
49
- updated = True
50
- except Exception:
51
- pass
52
-
53
- buffer = getattr(state, "buffer", None)
54
- if isinstance(buffer, list) and not any(_has_visible_text(item) for item in buffer):
55
- buffer.append(text_value)
56
- updated = True
57
-
58
- return updated
59
-
60
-
61
- def _update_renderer_transcript(renderer: Any, text_value: str) -> None:
62
- """Populate the renderer (or its state) with the supplied text."""
63
- state = getattr(renderer, "state", None)
64
- if _update_state_transcript(state, text_value):
65
- return
66
-
67
- if hasattr(renderer, "final_text") and not _has_visible_text(getattr(renderer, "final_text", "")):
68
- try:
69
- renderer.final_text = text_value
70
- except Exception:
71
- pass
72
-
73
-
74
52
  class AgentRunRenderingManager:
75
53
  """Coordinate renderer creation and streaming event handling."""
76
54
 
@@ -81,6 +59,7 @@ class AgentRunRenderingManager:
81
59
  logger: Optional logger instance, creates default if None
82
60
  """
83
61
  self._logger = logger or logging.getLogger(__name__)
62
+ self._buffer_factory = TranscriptBuffer
84
63
 
85
64
  # --------------------------------------------------------------------- #
86
65
  # Renderer setup helpers
@@ -92,17 +71,38 @@ class AgentRunRenderingManager:
92
71
  verbose: bool = False,
93
72
  ) -> RichStreamRenderer:
94
73
  """Create an appropriate renderer based on the supplied spec."""
74
+ transcript_buffer = self._buffer_factory()
75
+ base_options = RendererFactoryOptions(console=_Console(), transcript_buffer=transcript_buffer)
95
76
  if isinstance(renderer_spec, RichStreamRenderer):
96
77
  return renderer_spec
97
78
 
98
79
  if isinstance(renderer_spec, str):
99
- if renderer_spec == "silent":
100
- return self._create_silent_renderer()
101
- if renderer_spec == "minimal":
102
- return self._create_minimal_renderer()
103
- return self._create_default_renderer(verbose)
80
+ lowered = renderer_spec.lower()
81
+ if lowered == "silent":
82
+ return self._attach_buffer(base_options.build(make_silent_renderer), transcript_buffer)
83
+ if lowered == "minimal":
84
+ return self._attach_buffer(base_options.build(make_minimal_renderer), transcript_buffer)
85
+ if lowered == "verbose":
86
+ return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
87
+
88
+ if verbose:
89
+ return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
90
+
91
+ default_options = RendererFactoryOptions(
92
+ console=_Console(),
93
+ transcript_buffer=transcript_buffer,
94
+ verbose=verbose,
95
+ )
96
+ return self._attach_buffer(default_options.build(make_default_renderer), transcript_buffer)
104
97
 
105
- return self._create_default_renderer(verbose)
98
+ @staticmethod
99
+ def _attach_buffer(renderer: RichStreamRenderer, buffer: TranscriptBuffer) -> RichStreamRenderer:
100
+ """Attach a captured transcript buffer to a renderer for later inspection."""
101
+ try:
102
+ renderer._captured_transcript_buffer = buffer # type: ignore[attr-defined]
103
+ except Exception:
104
+ pass
105
+ return renderer
106
106
 
107
107
  def build_initial_metadata(
108
108
  self,
@@ -123,49 +123,6 @@ class AgentRunRenderingManager:
123
123
  """Notify renderer that streaming is starting."""
124
124
  renderer.on_start(meta)
125
125
 
126
- def _create_silent_renderer(self) -> RichStreamRenderer:
127
- silent_config = RendererConfig(
128
- live=False,
129
- persist_live=False,
130
- render_thinking=False,
131
- )
132
- return RichStreamRenderer(
133
- console=_Console(file=io.StringIO(), force_terminal=False),
134
- cfg=silent_config,
135
- verbose=False,
136
- )
137
-
138
- def _create_minimal_renderer(self) -> RichStreamRenderer:
139
- minimal_config = RendererConfig(
140
- live=False,
141
- persist_live=False,
142
- render_thinking=False,
143
- )
144
- return RichStreamRenderer(
145
- console=_Console(),
146
- cfg=minimal_config,
147
- verbose=False,
148
- )
149
-
150
- def _create_verbose_renderer(self) -> RichStreamRenderer:
151
- verbose_config = RendererConfig(
152
- theme="dark",
153
- style="debug",
154
- live=False,
155
- append_finished_snapshots=False,
156
- )
157
- return RichStreamRenderer(
158
- console=_Console(),
159
- cfg=verbose_config,
160
- verbose=True,
161
- )
162
-
163
- def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
164
- if verbose:
165
- return self._create_verbose_renderer()
166
- default_config = RendererConfig()
167
- return RichStreamRenderer(console=_Console(), cfg=default_config)
168
-
169
126
  # --------------------------------------------------------------------- #
170
127
  # Streaming event handling
171
128
  # --------------------------------------------------------------------- #
@@ -176,6 +133,7 @@ class AgentRunRenderingManager:
176
133
  timeout_seconds: float,
177
134
  agent_name: str | None,
178
135
  meta: dict[str, Any],
136
+ hitl_handler: RemoteHITLHandler | None = None,
179
137
  ) -> tuple[str, dict[str, Any], float | None, float | None]:
180
138
  """Process streaming events and accumulate response."""
181
139
  final_text = ""
@@ -199,10 +157,14 @@ class AgentRunRenderingManager:
199
157
  final_text,
200
158
  stats_usage,
201
159
  meta,
160
+ hitl_handler=hitl_handler,
202
161
  )
203
162
 
204
163
  if controller and getattr(controller, "enabled", False):
205
164
  controller.poll(renderer)
165
+ parsed_event = self._parse_event(event)
166
+ if parsed_event and self._is_final_event(parsed_event):
167
+ break
206
168
  finally:
207
169
  if controller and getattr(controller, "enabled", False):
208
170
  controller.on_stream_complete()
@@ -210,18 +172,374 @@ class AgentRunRenderingManager:
210
172
  finished_monotonic = monotonic()
211
173
  return final_text, stats_usage, started_monotonic, finished_monotonic
212
174
 
175
+ async def _consume_event_stream(
176
+ self,
177
+ event_stream: AsyncIterable[dict[str, Any]],
178
+ renderer: RichStreamRenderer,
179
+ final_text: str,
180
+ stats_usage: dict[str, Any],
181
+ meta: dict[str, Any],
182
+ skip_final_render: bool,
183
+ last_rendered_content: str | None,
184
+ controller: Any | None,
185
+ ) -> tuple[str, dict[str, Any], float | None]:
186
+ """Consume event stream and update state.
187
+
188
+ Args:
189
+ event_stream: Async iterable yielding SSE-like event dicts.
190
+ renderer: Renderer to use for displaying events.
191
+ final_text: Current accumulated final text.
192
+ stats_usage: Usage statistics dictionary.
193
+ meta: Metadata dictionary.
194
+ skip_final_render: If True, skip rendering final_response events.
195
+ last_rendered_content: Last rendered content to avoid duplicates.
196
+ controller: Controller instance.
197
+
198
+ Returns:
199
+ Tuple of (final_text, stats_usage, started_monotonic).
200
+ """
201
+ started_monotonic: float | None = None
202
+
203
+ async for event in event_stream:
204
+ if started_monotonic is None:
205
+ started_monotonic = monotonic()
206
+
207
+ parsed_event = self._parse_event(event)
208
+ if parsed_event is None:
209
+ continue
210
+
211
+ final_text, stats_usage = self._handle_parsed_event(
212
+ parsed_event,
213
+ renderer,
214
+ final_text,
215
+ stats_usage,
216
+ meta,
217
+ skip_final_render=skip_final_render,
218
+ last_rendered_content=last_rendered_content,
219
+ )
220
+
221
+ content_str = self._extract_content_string(parsed_event)
222
+ if content_str:
223
+ last_rendered_content = content_str
224
+
225
+ if controller and getattr(controller, "enabled", False):
226
+ controller.poll(renderer)
227
+ if parsed_event and self._is_final_event(parsed_event):
228
+ break
229
+
230
+ return final_text, stats_usage, started_monotonic
231
+
232
+ async def async_process_stream_events(
233
+ self,
234
+ event_stream: AsyncIterable[dict[str, Any]],
235
+ renderer: RichStreamRenderer,
236
+ meta: dict[str, Any],
237
+ *,
238
+ skip_final_render: bool = True,
239
+ ) -> tuple[str, dict[str, Any], float | None, float | None]:
240
+ """Process streaming events from an async event source.
241
+
242
+ This method provides unified stream processing for both remote (HTTP)
243
+ and local (LangGraph) agent execution, ensuring consistent behavior.
244
+
245
+ Args:
246
+ event_stream: Async iterable yielding SSE-like event dicts.
247
+ Each event should have a "data" key with JSON string, or be
248
+ a pre-parsed dict with "content", "metadata", etc.
249
+ renderer: Renderer to use for displaying events.
250
+ meta: Metadata dictionary for renderer context.
251
+ skip_final_render: If True, skip rendering final_response events
252
+ (they are rendered separately via finalize_renderer).
253
+
254
+ Returns:
255
+ Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
256
+ """
257
+ final_text = ""
258
+ stats_usage: dict[str, Any] = {}
259
+ started_monotonic: float | None = None
260
+ last_rendered_content: str | None = None
261
+
262
+ controller = getattr(renderer, "transcript_controller", None)
263
+ if controller and getattr(controller, "enabled", False):
264
+ controller.on_stream_start(renderer)
265
+
266
+ try:
267
+ final_text, stats_usage, started_monotonic = await self._consume_event_stream(
268
+ event_stream,
269
+ renderer,
270
+ final_text,
271
+ stats_usage,
272
+ meta,
273
+ skip_final_render,
274
+ last_rendered_content,
275
+ controller,
276
+ )
277
+ except Exception as e:
278
+ err_msg = str(e)
279
+ reason = getattr(getattr(e, "result", None), "reason", None)
280
+ if reason:
281
+ final_text = f"⚠️ Guardrail violation: {reason}"
282
+ elif "⚠️ Guardrail violation" in err_msg or "Content blocked by guardrails" in err_msg:
283
+ final_text = err_msg
284
+ else:
285
+ raise e
286
+ finally:
287
+ if controller and getattr(controller, "enabled", False):
288
+ controller.on_stream_complete()
289
+
290
+ finished_monotonic = monotonic()
291
+ return final_text, stats_usage, started_monotonic, finished_monotonic
292
+
293
+ def _parse_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
294
+ """Parse an SSE event dict into a usable format.
295
+
296
+ Args:
297
+ event: Raw event dict, either with "data" key (SSE format) or
298
+ pre-parsed with "content", "metadata", etc.
299
+
300
+ Returns:
301
+ Parsed event dict, or None if parsing fails.
302
+ """
303
+ if "data" in event:
304
+ try:
305
+ return json.loads(event["data"])
306
+ except json.JSONDecodeError:
307
+ self._logger.debug("Non-JSON SSE fragment skipped")
308
+ return None
309
+ # Already parsed (e.g., from local runner)
310
+ return event if event else None
311
+
312
+ def _handle_parsed_event(
313
+ self,
314
+ ev: dict[str, Any],
315
+ renderer: RichStreamRenderer,
316
+ final_text: str,
317
+ stats_usage: dict[str, Any],
318
+ meta: dict[str, Any],
319
+ *,
320
+ skip_final_render: bool = True,
321
+ last_rendered_content: str | None = None,
322
+ ) -> tuple[str, dict[str, Any]]:
323
+ """Handle a parsed event and update accumulators.
324
+
325
+ Args:
326
+ ev: Parsed event dictionary.
327
+ renderer: Renderer instance.
328
+ final_text: Current accumulated final text.
329
+ stats_usage: Usage statistics dictionary.
330
+ meta: Metadata dictionary.
331
+ skip_final_render: If True, skip rendering final_response events.
332
+ last_rendered_content: Last rendered content to avoid duplicates.
333
+
334
+ Returns:
335
+ Tuple of (updated_final_text, updated_stats_usage).
336
+ """
337
+ kind = self._get_event_kind(ev)
338
+
339
+ # Dispatch to specialized handlers based on event kind
340
+ handler = self._get_event_handler(kind, ev)
341
+ if handler:
342
+ return handler(ev, renderer, final_text, stats_usage, meta, skip_final_render)
343
+
344
+ # Default: handle content events
345
+ return self._handle_content_event_async(ev, renderer, final_text, stats_usage, last_rendered_content)
346
+
347
+ def _get_event_handler(
348
+ self,
349
+ kind: str | None,
350
+ ev: dict[str, Any],
351
+ ) -> Callable[..., tuple[str, dict[str, Any]]] | None:
352
+ """Get the appropriate handler for an event kind.
353
+
354
+ Args:
355
+ kind: Event kind string.
356
+ ev: Event dictionary (for checking is_final flag).
357
+
358
+ Returns:
359
+ Handler function or None for default content handling.
360
+ """
361
+ if kind == "usage":
362
+ return self._handle_usage_event
363
+ if kind == "final_response" or ev.get("is_final"):
364
+ return self._handle_final_response_event
365
+ if kind == "run_info":
366
+ return self._handle_run_info_event_wrapper
367
+ if kind in ("artifact", "status_update"):
368
+ return self._handle_render_only_event
369
+ return None
370
+
371
+ def _handle_usage_event(
372
+ self,
373
+ ev: dict[str, Any],
374
+ _renderer: RichStreamRenderer,
375
+ final_text: str,
376
+ stats_usage: dict[str, Any],
377
+ _meta: dict[str, Any],
378
+ _skip_final_render: bool,
379
+ ) -> tuple[str, dict[str, Any]]:
380
+ """Handle usage events."""
381
+ stats_usage.update(ev.get("usage") or {})
382
+ return final_text, stats_usage
383
+
384
+ def _handle_final_response_event(
385
+ self,
386
+ ev: dict[str, Any],
387
+ renderer: RichStreamRenderer,
388
+ final_text: str,
389
+ stats_usage: dict[str, Any],
390
+ _meta: dict[str, Any],
391
+ skip_final_render: bool,
392
+ ) -> tuple[str, dict[str, Any]]:
393
+ """Handle final_response events."""
394
+ content = ev.get("content")
395
+ if content:
396
+ final_text = str(content)
397
+ if not skip_final_render:
398
+ renderer.on_event(ev)
399
+ return final_text, stats_usage
400
+
401
+ def _handle_run_info_event_wrapper(
402
+ self,
403
+ ev: dict[str, Any],
404
+ renderer: RichStreamRenderer,
405
+ final_text: str,
406
+ stats_usage: dict[str, Any],
407
+ meta: dict[str, Any],
408
+ _skip_final_render: bool,
409
+ ) -> tuple[str, dict[str, Any]]:
410
+ """Handle run_info events."""
411
+ self._handle_run_info_event(ev, meta, renderer)
412
+ return final_text, stats_usage
413
+
414
+ def _handle_render_only_event(
415
+ self,
416
+ ev: dict[str, Any],
417
+ renderer: RichStreamRenderer,
418
+ final_text: str,
419
+ stats_usage: dict[str, Any],
420
+ _meta: dict[str, Any],
421
+ _skip_final_render: bool,
422
+ ) -> tuple[str, dict[str, Any]]:
423
+ """Handle events that only need rendering (artifact, status_update)."""
424
+ renderer.on_event(ev)
425
+ return final_text, stats_usage
426
+
427
+ def _handle_content_event_async(
428
+ self,
429
+ ev: dict[str, Any],
430
+ renderer: RichStreamRenderer,
431
+ final_text: str,
432
+ stats_usage: dict[str, Any],
433
+ last_rendered_content: str | None,
434
+ ) -> tuple[str, dict[str, Any]]:
435
+ """Handle content events with deduplication."""
436
+ content = ev.get("content")
437
+ if content:
438
+ content_str = str(content)
439
+ if not content_str.startswith("Artifact received:"):
440
+ kind = self._get_event_kind(ev)
441
+ # Skip accumulating content for status updates and agent steps
442
+ if kind in ("agent_step", "status_update"):
443
+ renderer.on_event(ev)
444
+ return final_text, stats_usage
445
+
446
+ if self._is_token_event(ev):
447
+ renderer.on_event(ev)
448
+ final_text = f"{final_text}{content_str}"
449
+ else:
450
+ if content_str != last_rendered_content:
451
+ renderer.on_event(ev)
452
+ final_text = content_str
453
+ else:
454
+ renderer.on_event(ev)
455
+ return final_text, stats_usage
456
+
457
+ def _get_event_kind(self, ev: dict[str, Any]) -> str | None:
458
+ """Extract normalized event kind from parsed event.
459
+
460
+ Args:
461
+ ev: Parsed event dictionary.
462
+
463
+ Returns:
464
+ Event kind string or None.
465
+ """
466
+ metadata = ev.get("metadata") or {}
467
+ kind = metadata.get("kind")
468
+ if kind:
469
+ return str(kind)
470
+ event_type = ev.get("event_type")
471
+ return str(event_type) if event_type else None
472
+
473
+ def _is_token_event(self, ev: dict[str, Any]) -> bool:
474
+ """Return True when the event represents token streaming output.
475
+
476
+ Args:
477
+ ev: Parsed event dictionary.
478
+
479
+ Returns:
480
+ True when the event is a token chunk, otherwise False.
481
+ """
482
+ metadata = ev.get("metadata") or {}
483
+ kind = metadata.get("kind")
484
+ return str(kind).lower() == "token"
485
+
486
+ def _is_final_event(self, ev: dict[str, Any]) -> bool:
487
+ """Return True when the event marks stream termination.
488
+
489
+ Args:
490
+ ev: Parsed event dictionary.
491
+
492
+ Returns:
493
+ True when the event is terminal, otherwise False.
494
+ """
495
+ if ev.get("is_final") is True or ev.get("final") is True:
496
+ return True
497
+ kind = self._get_event_kind(ev)
498
+ return kind in _FINAL_EVENT_TYPES
499
+
500
+ def _extract_content_string(self, event: dict[str, Any]) -> str | None:
501
+ """Extract textual content from a parsed event.
502
+
503
+ Args:
504
+ event: Parsed event dictionary.
505
+
506
+ Returns:
507
+ Content string or None.
508
+ """
509
+ if not event:
510
+ return None
511
+ content = event.get("content")
512
+ if content:
513
+ return str(content)
514
+ return None
515
+
213
516
  def _capture_request_id(
214
517
  self,
215
518
  stream_response: httpx.Response,
216
519
  meta: dict[str, Any],
217
520
  renderer: RichStreamRenderer,
218
521
  ) -> None:
522
+ """Capture request ID from response headers and update metadata.
523
+
524
+ Args:
525
+ stream_response: HTTP response stream.
526
+ meta: Metadata dictionary to update.
527
+ renderer: Renderer instance.
528
+ """
219
529
  req_id = stream_response.headers.get("x-request-id") or stream_response.headers.get("x-run-id")
220
530
  if req_id:
221
531
  meta["run_id"] = req_id
222
532
  renderer.on_start(meta)
223
533
 
224
534
  def _maybe_start_timer(self, event: dict[str, Any]) -> float | None:
535
+ """Start timing if this is a content-bearing event.
536
+
537
+ Args:
538
+ event: Event dictionary.
539
+
540
+ Returns:
541
+ Monotonic time if timer should start, None otherwise.
542
+ """
225
543
  try:
226
544
  ev = json.loads(event["data"])
227
545
  except json.JSONDecodeError:
@@ -238,13 +556,38 @@ class AgentRunRenderingManager:
238
556
  final_text: str,
239
557
  stats_usage: dict[str, Any],
240
558
  meta: dict[str, Any],
559
+ hitl_handler: RemoteHITLHandler | None = None,
241
560
  ) -> tuple[str, dict[str, Any]]:
561
+ """Process a single streaming event.
562
+
563
+ Args:
564
+ event: Event dictionary.
565
+ renderer: Renderer instance.
566
+ final_text: Accumulated text so far.
567
+ stats_usage: Usage statistics dictionary.
568
+ meta: Metadata dictionary.
569
+ hitl_handler: Optional HITL handler for approval callbacks.
570
+
571
+ Returns:
572
+ Tuple of (updated_final_text, updated_stats_usage).
573
+ """
242
574
  try:
243
575
  ev = json.loads(event["data"])
244
576
  except json.JSONDecodeError:
245
577
  self._logger.debug("Non-JSON SSE fragment skipped")
246
578
  return final_text, stats_usage
247
579
 
580
+ # Handle HITL event (non-blocking via thread)
581
+ if hitl_handler and self._is_hitl_pending_event(ev):
582
+ try:
583
+ hitl_handler.handle_hitl_event(ev)
584
+ except Exception as e:
585
+ # Log but don't crash stream
586
+ self._logger.error(
587
+ f"HITL handler error: {e}",
588
+ exc_info=True,
589
+ )
590
+
248
591
  kind = (ev.get("metadata") or {}).get("kind")
249
592
  renderer.on_event(ev)
250
593
 
@@ -259,7 +602,9 @@ class AgentRunRenderingManager:
259
602
  if handled is not None:
260
603
  return handled
261
604
 
262
- if ev.get("content"):
605
+ # Only accumulate content for actual content events, not status updates or agent steps
606
+ # Status updates (agent_step) should be rendered but not accumulated in final_text
607
+ if ev.get("content") and kind not in ("agent_step", "status_update"):
263
608
  final_text = self._handle_content_event(ev, final_text)
264
609
 
265
610
  return final_text, stats_usage
@@ -294,17 +639,49 @@ class AgentRunRenderingManager:
294
639
  return None
295
640
 
296
641
  def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
642
+ """Handle a content event and update final text.
643
+
644
+ Args:
645
+ ev: Event dictionary.
646
+ final_text: Current accumulated text.
647
+
648
+ Returns:
649
+ Updated final text.
650
+ """
297
651
  content = ev.get("content", "")
298
652
  if not content.startswith("Artifact received:"):
653
+ if self._is_token_event(ev):
654
+ return f"{final_text}{content}"
299
655
  return content
300
656
  return final_text
301
657
 
658
+ @staticmethod
659
+ def _is_hitl_pending_event(event: dict[str, Any]) -> bool:
660
+ """Check if event is a pending HITL approval request.
661
+
662
+ Args:
663
+ event: Parsed event dictionary.
664
+
665
+ Returns:
666
+ True if event is a pending HITL request.
667
+ """
668
+ metadata = event.get("metadata", {})
669
+ hitl_meta = metadata.get("hitl", {})
670
+ return hitl_meta.get("required") is True and hitl_meta.get("decision") == "pending"
671
+
302
672
  def _handle_run_info_event(
303
673
  self,
304
674
  ev: dict[str, Any],
305
675
  meta: dict[str, Any],
306
676
  renderer: RichStreamRenderer,
307
677
  ) -> None:
678
+ """Handle a run_info event and update metadata.
679
+
680
+ Args:
681
+ ev: Event dictionary.
682
+ meta: Metadata dictionary to update.
683
+ renderer: Renderer instance.
684
+ """
308
685
  if ev.get("model"):
309
686
  meta["model"] = ev["model"]
310
687
  renderer.on_start(meta)
@@ -318,7 +695,52 @@ class AgentRunRenderingManager:
318
695
  return
319
696
 
320
697
  text_value = _coerce_to_string(text)
321
- _update_renderer_transcript(renderer, text_value)
698
+ state = getattr(renderer, "state", None)
699
+ if state is None:
700
+ self._ensure_renderer_text(renderer, text_value)
701
+ return
702
+
703
+ self._ensure_state_final_text(state, text_value)
704
+ self._ensure_state_buffer(state, text_value)
705
+
706
+ def _ensure_renderer_text(self, renderer: RichStreamRenderer, text_value: str) -> None:
707
+ """Best-effort assignment for renderer.final_text."""
708
+ if not hasattr(renderer, "final_text"):
709
+ return
710
+ current_text = getattr(renderer, "final_text", "")
711
+ if _has_visible_text(current_text):
712
+ return
713
+ self._safe_set_attr(renderer, "final_text", text_value)
714
+
715
+ def _ensure_state_final_text(self, state: Any, text_value: str) -> None:
716
+ """Best-effort assignment for renderer.state.final_text."""
717
+ current_text = getattr(state, "final_text", "")
718
+ if _has_visible_text(current_text):
719
+ return
720
+ self._safe_set_attr(state, "final_text", text_value)
721
+
722
+ def _ensure_state_buffer(self, state: Any, text_value: str) -> None:
723
+ """Append fallback text to the state buffer when available."""
724
+ buffer = getattr(state, "buffer", None)
725
+ if not hasattr(buffer, "append"):
726
+ return
727
+ self._safe_append(buffer.append, text_value)
728
+
729
+ @staticmethod
730
+ def _safe_set_attr(target: Any, attr: str, value: str) -> None:
731
+ """Assign attribute while masking renderer-specific failures."""
732
+ try:
733
+ setattr(target, attr, value)
734
+ except Exception:
735
+ pass
736
+
737
+ @staticmethod
738
+ def _safe_append(appender: Callable[[str], Any], value: str) -> None:
739
+ """Invoke append-like functions without leaking renderer errors."""
740
+ try:
741
+ appender(value)
742
+ except Exception:
743
+ pass
322
744
 
323
745
  # --------------------------------------------------------------------- #
324
746
  # Finalisation helpers
@@ -345,7 +767,9 @@ class AgentRunRenderingManager:
345
767
  elif hasattr(renderer, "buffer"):
346
768
  buffer_values = renderer.buffer
347
769
 
348
- if buffer_values is not None:
770
+ if isinstance(buffer_values, TranscriptBuffer):
771
+ rendered_text = buffer_values.render()
772
+ elif buffer_values is not None:
349
773
  try:
350
774
  rendered_text = "".join(buffer_values)
351
775
  except TypeError:
@@ -356,7 +780,7 @@ class AgentRunRenderingManager:
356
780
  self._ensure_renderer_final_content(renderer, fallback_text)
357
781
 
358
782
  renderer.on_complete(st)
359
- return final_text or rendered_text or "No response content received."
783
+ return final_text or rendered_text or NO_AGENT_RESPONSE_FALLBACK
360
784
 
361
785
 
362
786
  def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
@@ -370,3 +794,33 @@ def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
370
794
  if not specified in kwargs.
371
795
  """
372
796
  return kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
797
+
798
+
799
+ def finalize_render_manager(
800
+ manager: AgentRunRenderingManager,
801
+ renderer: RichStreamRenderer,
802
+ final_text: str,
803
+ stats_usage: dict[str, Any],
804
+ started_monotonic: float | None,
805
+ finished_monotonic: float | None,
806
+ ) -> str:
807
+ """Helper to finalize renderer via manager and return final text.
808
+
809
+ Args:
810
+ manager: The rendering manager instance.
811
+ renderer: Renderer to finalize.
812
+ final_text: Final text content.
813
+ stats_usage: Usage statistics dictionary.
814
+ started_monotonic: Start time (monotonic).
815
+ finished_monotonic: Finish time (monotonic).
816
+
817
+ Returns:
818
+ Final text string.
819
+ """
820
+ return manager.finalize_renderer(
821
+ renderer,
822
+ final_text,
823
+ stats_usage,
824
+ started_monotonic,
825
+ finished_monotonic,
826
+ )