glaip-sdk 0.6.19__py3-none-any.whl → 0.7.27__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 (135) hide show
  1. glaip_sdk/agents/base.py +283 -30
  2. glaip_sdk/agents/component.py +233 -0
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  8. glaip_sdk/cli/commands/agents/_common.py +562 -0
  9. glaip_sdk/cli/commands/agents/create.py +155 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +1 -1
  17. glaip_sdk/cli/commands/configure.py +1 -2
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/entrypoint.py +20 -0
  46. glaip_sdk/cli/main.py +112 -35
  47. glaip_sdk/cli/pager.py +3 -3
  48. glaip_sdk/cli/resolution.py +2 -1
  49. glaip_sdk/cli/slash/accounts_controller.py +3 -1
  50. glaip_sdk/cli/slash/agent_session.py +1 -1
  51. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  52. glaip_sdk/cli/slash/session.py +343 -20
  53. glaip_sdk/cli/slash/tui/__init__.py +29 -1
  54. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  55. glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
  56. glaip_sdk/cli/slash/tui/clipboard.py +316 -0
  57. glaip_sdk/cli/slash/tui/context.py +92 -0
  58. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  59. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  60. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  61. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  62. glaip_sdk/cli/slash/tui/loading.py +43 -21
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
  64. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  65. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  66. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  67. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  68. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  69. glaip_sdk/cli/slash/tui/toast.py +388 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +1 -1
  72. glaip_sdk/cli/tui_settings.py +125 -0
  73. glaip_sdk/cli/update_notifier.py +215 -7
  74. glaip_sdk/cli/validators.py +1 -1
  75. glaip_sdk/client/__init__.py +2 -1
  76. glaip_sdk/client/_schedule_payloads.py +89 -0
  77. glaip_sdk/client/agents.py +293 -17
  78. glaip_sdk/client/base.py +25 -0
  79. glaip_sdk/client/hitl.py +136 -0
  80. glaip_sdk/client/main.py +7 -5
  81. glaip_sdk/client/mcps.py +44 -13
  82. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  83. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  84. glaip_sdk/client/payloads/agent/responses.py +43 -0
  85. glaip_sdk/client/run_rendering.py +109 -30
  86. glaip_sdk/client/schedules.py +439 -0
  87. glaip_sdk/client/tools.py +52 -23
  88. glaip_sdk/config/constants.py +22 -2
  89. glaip_sdk/guardrails/__init__.py +80 -0
  90. glaip_sdk/guardrails/serializer.py +91 -0
  91. glaip_sdk/hitl/__init__.py +35 -2
  92. glaip_sdk/hitl/base.py +64 -0
  93. glaip_sdk/hitl/callback.py +43 -0
  94. glaip_sdk/hitl/local.py +1 -31
  95. glaip_sdk/hitl/remote.py +523 -0
  96. glaip_sdk/models/__init__.py +47 -1
  97. glaip_sdk/models/_provider_mappings.py +101 -0
  98. glaip_sdk/models/_validation.py +97 -0
  99. glaip_sdk/models/agent.py +2 -1
  100. glaip_sdk/models/agent_runs.py +2 -1
  101. glaip_sdk/models/constants.py +141 -0
  102. glaip_sdk/models/model.py +170 -0
  103. glaip_sdk/models/schedule.py +224 -0
  104. glaip_sdk/payload_schemas/agent.py +1 -0
  105. glaip_sdk/payload_schemas/guardrails.py +34 -0
  106. glaip_sdk/ptc.py +145 -0
  107. glaip_sdk/registry/tool.py +270 -57
  108. glaip_sdk/runner/__init__.py +20 -3
  109. glaip_sdk/runner/deps.py +4 -1
  110. glaip_sdk/runner/langgraph.py +251 -27
  111. glaip_sdk/runner/logging_config.py +77 -0
  112. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
  113. glaip_sdk/runner/ptc_adapter.py +98 -0
  114. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
  115. glaip_sdk/schedules/__init__.py +22 -0
  116. glaip_sdk/schedules/base.py +291 -0
  117. glaip_sdk/tools/base.py +67 -14
  118. glaip_sdk/utils/__init__.py +1 -0
  119. glaip_sdk/utils/agent_config.py +8 -2
  120. glaip_sdk/utils/bundler.py +138 -2
  121. glaip_sdk/utils/import_resolver.py +427 -49
  122. glaip_sdk/utils/runtime_config.py +3 -2
  123. glaip_sdk/utils/sync.py +31 -11
  124. glaip_sdk/utils/tool_detection.py +274 -6
  125. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +22 -8
  126. glaip_sdk-0.7.27.dist-info/RECORD +227 -0
  127. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +1 -1
  128. glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -0
  129. glaip_sdk/cli/commands/agents.py +0 -1509
  130. glaip_sdk/cli/commands/mcps.py +0 -1356
  131. glaip_sdk/cli/commands/tools.py +0 -576
  132. glaip_sdk/cli/utils.py +0 -263
  133. glaip_sdk-0.6.19.dist-info/RECORD +0 -163
  134. glaip_sdk-0.6.19.dist-info/entry_points.txt +0 -2
  135. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/top_level.txt +0 -0
glaip_sdk/client/main.py CHANGED
@@ -12,13 +12,15 @@ from typing import TYPE_CHECKING, Any
12
12
 
13
13
  from glaip_sdk.client.agents import AgentClient
14
14
  from glaip_sdk.client.base import BaseClient
15
+ from glaip_sdk.client.hitl import HITLClient
15
16
  from glaip_sdk.client.mcps import MCPClient
17
+ from glaip_sdk.client.schedules import ScheduleClient
16
18
  from glaip_sdk.client.shared import build_shared_config
17
19
  from glaip_sdk.client.tools import ToolClient
18
20
 
19
21
  if TYPE_CHECKING: # pragma: no cover
20
22
  from glaip_sdk.agents import Agent
21
- from glaip_sdk.client._agent_payloads import AgentListResult
23
+ from glaip_sdk.client.payloads.agent import AgentListResult
22
24
  from glaip_sdk.mcps import MCP
23
25
  from glaip_sdk.tools import Tool
24
26
 
@@ -38,6 +40,8 @@ class Client(BaseClient):
38
40
  self.agents = AgentClient(**shared_config)
39
41
  self.tools = ToolClient(**shared_config)
40
42
  self.mcps = MCPClient(**shared_config)
43
+ self.schedules = ScheduleClient(**shared_config)
44
+ self.hitl = HITLClient(**shared_config)
41
45
 
42
46
  # ---- Core API Methods (Public Interface) ----
43
47
 
@@ -208,10 +212,6 @@ class Client(BaseClient):
208
212
  return self.mcps.get_mcp_tools_from_config(config)
209
213
 
210
214
  # Language Models
211
- def list_language_models(self) -> list[dict]:
212
- """List available language models."""
213
- data = self._request("GET", "/language-models")
214
- return data or []
215
215
 
216
216
  # ---- Timeout propagation ----
217
217
  @property
@@ -236,6 +236,8 @@ class Client(BaseClient):
236
236
  self.tools.http_client = self.http_client
237
237
  if hasattr(self, "mcps"):
238
238
  self.mcps.http_client = self.http_client
239
+ if hasattr(self, "schedules"):
240
+ self.schedules.http_client = self.http_client
239
241
  except Exception:
240
242
  pass
241
243
 
glaip_sdk/client/mcps.py CHANGED
@@ -85,26 +85,56 @@ class MCPClient(BaseClient):
85
85
  response = MCPResponse(**full_mcp_data)
86
86
  return MCP.from_response(response, client=self)
87
87
 
88
- def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
88
+ def update_mcp(self, mcp_id: str | MCP, **kwargs) -> MCP:
89
89
  """Update an existing MCP.
90
90
 
91
- Automatically chooses between PUT (full update) and PATCH (partial update)
92
- based on the provided fields:
93
- - Uses PUT if name, config, and transport are all provided (full update)
94
- - Uses PATCH otherwise (partial update)
91
+ Notes:
92
+ - Payload construction is centralized via ``_build_update_payload`` so required
93
+ defaults (e.g., ``type``) and value normalization stay consistent across SDK and CLI.
94
+ - For backward compatibility, still chooses PATCH vs PUT based on which fields the
95
+ caller provided, but uses the SDK payload builder for the final payload.
95
96
  """
96
- # Check if all required fields for full update are provided
97
+ # Backward-compatible: allow passing an MCP instance to avoid an extra fetch.
98
+ if isinstance(mcp_id, MCP):
99
+ current_mcp = mcp_id
100
+ if not current_mcp.id:
101
+ raise ValueError("MCP instance has no id; cannot update.")
102
+ mcp_id_value = str(current_mcp.id)
103
+ else:
104
+ current_mcp = None
105
+ mcp_id_value = mcp_id
106
+
97
107
  required_fields = {"name", "config", "transport"}
98
108
  provided_fields = set(kwargs.keys())
109
+ method = "PUT" if required_fields.issubset(provided_fields) else "PATCH"
110
+
111
+ if not kwargs:
112
+ data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id_value}", json={})
113
+ response = MCPResponse(**data)
114
+ return MCP.from_response(response, client=self)
115
+
116
+ if current_mcp is None:
117
+ current_mcp = self.get_mcp_by_id(mcp_id_value)
118
+
119
+ payload_kwargs = kwargs.copy()
120
+ name = payload_kwargs.pop("name", None)
121
+ description = payload_kwargs.pop("description", None)
122
+ full_payload = self._build_update_payload(
123
+ current_mcp=current_mcp,
124
+ name=name,
125
+ description=description,
126
+ **payload_kwargs,
127
+ )
99
128
 
100
- if required_fields.issubset(provided_fields):
101
- # All required fields provided - use full update (PUT)
102
- method = "PUT"
129
+ if method == "PUT":
130
+ json_payload = full_payload
103
131
  else:
104
- # Partial update - use PATCH
105
- method = "PATCH"
132
+ json_payload = {key: full_payload[key] for key in provided_fields if key in full_payload}
133
+ json_payload["type"] = full_payload["type"]
134
+ if "config" in provided_fields and "transport" not in provided_fields and "transport" in full_payload:
135
+ json_payload["transport"] = full_payload["transport"]
106
136
 
107
- data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id}", json=kwargs)
137
+ data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id_value}", json=json_payload)
108
138
  response = MCPResponse(**data)
109
139
  return MCP.from_response(response, client=self)
110
140
 
@@ -188,7 +218,8 @@ class MCPClient(BaseClient):
188
218
  **kwargs,
189
219
  ) -> MCP:
190
220
  """Find by name and update, or create if not found."""
191
- existing = self.find_mcps(name)
221
+ all_mcps = self.list_mcps()
222
+ existing = [mcp for mcp in all_mcps if mcp.name.lower() == name.lower()]
192
223
 
193
224
  if len(existing) == 1:
194
225
  logger.info("Updating existing MCP: %s", name)
@@ -0,0 +1,23 @@
1
+ """Agent payload types for requests and responses.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from glaip_sdk.client.payloads.agent.requests import (
8
+ AgentCreateRequest,
9
+ AgentListParams,
10
+ AgentUpdateRequest,
11
+ merge_payload_fields,
12
+ resolve_language_model_fields,
13
+ )
14
+ from glaip_sdk.client.payloads.agent.responses import AgentListResult
15
+
16
+ __all__ = [
17
+ "AgentCreateRequest",
18
+ "AgentListParams",
19
+ "AgentListResult",
20
+ "AgentUpdateRequest",
21
+ "merge_payload_fields",
22
+ "resolve_language_model_fields",
23
+ ]
@@ -1,11 +1,15 @@
1
- #!/usr/bin/env python3
2
- """Shared helpers for Agent client payload construction and query handling."""
1
+ """Agent request payload types and helpers.
3
2
 
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ # pylint: disable=duplicate-code
4
8
  from __future__ import annotations
5
9
 
6
10
  from collections.abc import Callable, Mapping, MutableMapping, Sequence
7
11
  from copy import deepcopy
8
- from dataclasses import dataclass, field
12
+ from dataclasses import dataclass
9
13
  from typing import Any
10
14
 
11
15
  from glaip_sdk.config.constants import (
@@ -13,8 +17,8 @@ from glaip_sdk.config.constants import (
13
17
  DEFAULT_AGENT_PROVIDER,
14
18
  DEFAULT_AGENT_TYPE,
15
19
  DEFAULT_AGENT_VERSION,
16
- DEFAULT_MODEL,
17
20
  )
21
+ from glaip_sdk.models.constants import DEFAULT_MODEL
18
22
  from glaip_sdk.payload_schemas.agent import AgentImportOperation, get_import_field_plan
19
23
  from glaip_sdk.utils.client_utils import extract_ids
20
24
 
@@ -96,6 +100,11 @@ def resolve_language_model_fields(
96
100
  resolved_model = model_name or model or default_model
97
101
  resolved_provider = provider if provider is not None else default_provider
98
102
 
103
+ if resolved_model and isinstance(resolved_model, str) and "/" in resolved_model:
104
+ parts = resolved_model.split("/", 1)
105
+ resolved_provider = parts[0]
106
+ resolved_model = parts[1]
107
+
99
108
  result: dict[str, Any] = {}
100
109
  if resolved_model is not None:
101
110
  result["model_name"] = resolved_model
@@ -273,38 +282,6 @@ class AgentListParams:
273
282
  params[f"metadata.{key}"] = value
274
283
 
275
284
 
276
- @dataclass(slots=True)
277
- class AgentListResult:
278
- """Structured response for list_agents that retains pagination metadata."""
279
-
280
- items: list[Any] = field(default_factory=list)
281
- total: int | None = None
282
- page: int | None = None
283
- limit: int | None = None
284
- has_next: bool | None = None
285
- has_prev: bool | None = None
286
- message: str | None = None
287
-
288
- def __len__(self) -> int: # pragma: no cover - simple delegation
289
- """Return the number of items in the result list."""
290
- return len(self.items)
291
-
292
- def __iter__(self): # pragma: no cover - simple delegation
293
- """Return an iterator over the items in the result list."""
294
- return iter(self.items)
295
-
296
- def __getitem__(self, index: int) -> Any: # pragma: no cover - simple delegation
297
- """Get an item from the result list by index.
298
-
299
- Args:
300
- index: Index of the item to retrieve.
301
-
302
- Returns:
303
- The item at the specified index.
304
- """
305
- return self.items[index]
306
-
307
-
308
285
  @dataclass(slots=True)
309
286
  class AgentCreateRequest:
310
287
  """Declarative representation of an agent creation payload."""
@@ -422,16 +399,6 @@ class AgentUpdateRequest:
422
399
  return payload
423
400
 
424
401
 
425
- __all__ = [
426
- "AgentCreateRequest",
427
- "AgentListParams",
428
- "AgentListResult",
429
- "AgentUpdateRequest",
430
- "merge_payload_fields",
431
- "resolve_language_model_fields",
432
- ]
433
-
434
-
435
402
  def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
436
403
  """Populate immutable agent update fields using request data or existing agent defaults."""
437
404
  # Support both "agent_type" (runtime class) and "type" (API response) attributes
@@ -451,14 +418,27 @@ def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any)
451
418
 
452
419
  def _resolve_update_language_model_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
453
420
  """Resolve the language-model portion of an update request with sensible fallbacks."""
421
+ # Check if any LM inputs were provided
422
+ has_lm_inputs = any(
423
+ [
424
+ request.model is not None,
425
+ request.language_model_id is not None,
426
+ request.provider is not None,
427
+ request.model_name is not None,
428
+ ]
429
+ )
430
+
431
+ if not has_lm_inputs:
432
+ # No LM inputs provided - preserve existing fields
433
+ return _existing_language_model_fields(current_agent)
434
+
435
+ # LM inputs provided - resolve them (may return defaults if only partial info)
454
436
  fields = resolve_language_model_fields(
455
437
  model=request.model,
456
438
  language_model_id=request.language_model_id,
457
439
  provider=request.provider,
458
440
  model_name=request.model_name,
459
441
  )
460
- if not fields:
461
- fields = _existing_language_model_fields(current_agent)
462
442
  return fields
463
443
 
464
444
 
@@ -0,0 +1,43 @@
1
+ """Agent response payload types.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ # pylint: disable=duplicate-code
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class AgentListResult:
16
+ """Structured response for list_agents that retains pagination metadata."""
17
+
18
+ items: list[Any] = field(default_factory=list)
19
+ total: int | None = None
20
+ page: int | None = None
21
+ limit: int | None = None
22
+ has_next: bool | None = None
23
+ has_prev: bool | None = None
24
+ message: str | None = None
25
+
26
+ def __len__(self) -> int: # pragma: no cover - simple delegation
27
+ """Return the number of items in the result list."""
28
+ return len(self.items)
29
+
30
+ def __iter__(self): # pragma: no cover - simple delegation
31
+ """Return an iterator over the items in the result list."""
32
+ return iter(self.items)
33
+
34
+ def __getitem__(self, index: int) -> Any: # pragma: no cover - simple delegation
35
+ """Get an item from the result list by index.
36
+
37
+ Args:
38
+ index: Index of the item to retrieve.
39
+
40
+ Returns:
41
+ The item at the specified index.
42
+ """
43
+ return self.items[index]
@@ -11,7 +11,10 @@ import json
11
11
  import logging
12
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
@@ -130,6 +133,7 @@ class AgentRunRenderingManager:
130
133
  timeout_seconds: float,
131
134
  agent_name: str | None,
132
135
  meta: dict[str, Any],
136
+ hitl_handler: RemoteHITLHandler | None = None,
133
137
  ) -> tuple[str, dict[str, Any], float | None, float | None]:
134
138
  """Process streaming events and accumulate response."""
135
139
  final_text = ""
@@ -153,6 +157,7 @@ class AgentRunRenderingManager:
153
157
  final_text,
154
158
  stats_usage,
155
159
  meta,
160
+ hitl_handler=hitl_handler,
156
161
  )
157
162
 
158
163
  if controller and getattr(controller, "enabled", False):
@@ -167,6 +172,63 @@ class AgentRunRenderingManager:
167
172
  finished_monotonic = monotonic()
168
173
  return final_text, stats_usage, started_monotonic, finished_monotonic
169
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
+
170
232
  async def async_process_stream_events(
171
233
  self,
172
234
  event_stream: AsyncIterable[dict[str, Any]],
@@ -202,35 +264,25 @@ class AgentRunRenderingManager:
202
264
  controller.on_stream_start(renderer)
203
265
 
204
266
  try:
205
- async for event in event_stream:
206
- if started_monotonic is None:
207
- started_monotonic = monotonic()
208
-
209
- # Parse event if needed (handles both raw SSE and pre-parsed dicts)
210
- parsed_event = self._parse_event(event)
211
- if parsed_event is None:
212
- continue
213
-
214
- # Process the event and update accumulators
215
- final_text, stats_usage = self._handle_parsed_event(
216
- parsed_event,
217
- renderer,
218
- final_text,
219
- stats_usage,
220
- meta,
221
- skip_final_render=skip_final_render,
222
- last_rendered_content=last_rendered_content,
223
- )
224
-
225
- # Track last rendered content to avoid duplicates
226
- content_str = self._extract_content_string(parsed_event)
227
- if content_str:
228
- last_rendered_content = content_str
229
-
230
- if controller and getattr(controller, "enabled", False):
231
- controller.poll(renderer)
232
- if parsed_event and self._is_final_event(parsed_event):
233
- break
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
234
286
  finally:
235
287
  if controller and getattr(controller, "enabled", False):
236
288
  controller.on_stream_complete()
@@ -504,6 +556,7 @@ class AgentRunRenderingManager:
504
556
  final_text: str,
505
557
  stats_usage: dict[str, Any],
506
558
  meta: dict[str, Any],
559
+ hitl_handler: RemoteHITLHandler | None = None,
507
560
  ) -> tuple[str, dict[str, Any]]:
508
561
  """Process a single streaming event.
509
562
 
@@ -513,6 +566,7 @@ class AgentRunRenderingManager:
513
566
  final_text: Accumulated text so far.
514
567
  stats_usage: Usage statistics dictionary.
515
568
  meta: Metadata dictionary.
569
+ hitl_handler: Optional HITL handler for approval callbacks.
516
570
 
517
571
  Returns:
518
572
  Tuple of (updated_final_text, updated_stats_usage).
@@ -523,6 +577,17 @@ class AgentRunRenderingManager:
523
577
  self._logger.debug("Non-JSON SSE fragment skipped")
524
578
  return final_text, stats_usage
525
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
+
526
591
  kind = (ev.get("metadata") or {}).get("kind")
527
592
  renderer.on_event(ev)
528
593
 
@@ -590,6 +655,20 @@ class AgentRunRenderingManager:
590
655
  return content
591
656
  return final_text
592
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
+
593
672
  def _handle_run_info_event(
594
673
  self,
595
674
  ev: dict[str, Any],