glaip-sdk 0.0.20__py3-none-any.whl → 0.7.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1250 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +271 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  12. glaip_sdk/cli/commands/agents/_common.py +561 -0
  13. glaip_sdk/cli/commands/agents/create.py +151 -0
  14. glaip_sdk/cli/commands/agents/delete.py +64 -0
  15. glaip_sdk/cli/commands/agents/get.py +89 -0
  16. glaip_sdk/cli/commands/agents/list.py +129 -0
  17. glaip_sdk/cli/commands/agents/run.py +264 -0
  18. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  19. glaip_sdk/cli/commands/agents/update.py +112 -0
  20. glaip_sdk/cli/commands/common_config.py +104 -0
  21. glaip_sdk/cli/commands/configure.py +734 -143
  22. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  23. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  24. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  25. glaip_sdk/cli/commands/mcps/create.py +152 -0
  26. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  27. glaip_sdk/cli/commands/mcps/get.py +212 -0
  28. glaip_sdk/cli/commands/mcps/list.py +69 -0
  29. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  30. glaip_sdk/cli/commands/mcps/update.py +190 -0
  31. glaip_sdk/cli/commands/models.py +14 -12
  32. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  33. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  34. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  35. glaip_sdk/cli/commands/tools/_common.py +80 -0
  36. glaip_sdk/cli/commands/tools/create.py +228 -0
  37. glaip_sdk/cli/commands/tools/delete.py +61 -0
  38. glaip_sdk/cli/commands/tools/get.py +103 -0
  39. glaip_sdk/cli/commands/tools/list.py +69 -0
  40. glaip_sdk/cli/commands/tools/script.py +49 -0
  41. glaip_sdk/cli/commands/tools/update.py +102 -0
  42. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  43. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  44. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  45. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  46. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  47. glaip_sdk/cli/commands/update.py +164 -23
  48. glaip_sdk/cli/config.py +49 -7
  49. glaip_sdk/cli/constants.py +38 -0
  50. glaip_sdk/cli/context.py +8 -0
  51. glaip_sdk/cli/core/__init__.py +79 -0
  52. glaip_sdk/cli/core/context.py +124 -0
  53. glaip_sdk/cli/core/output.py +851 -0
  54. glaip_sdk/cli/core/prompting.py +649 -0
  55. glaip_sdk/cli/core/rendering.py +187 -0
  56. glaip_sdk/cli/display.py +45 -32
  57. glaip_sdk/cli/entrypoint.py +20 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +14 -17
  60. glaip_sdk/cli/main.py +344 -167
  61. glaip_sdk/cli/masking.py +21 -33
  62. glaip_sdk/cli/mcp_validators.py +5 -15
  63. glaip_sdk/cli/pager.py +15 -22
  64. glaip_sdk/cli/parsers/__init__.py +1 -3
  65. glaip_sdk/cli/parsers/json_input.py +11 -22
  66. glaip_sdk/cli/resolution.py +5 -10
  67. glaip_sdk/cli/rich_helpers.py +1 -3
  68. glaip_sdk/cli/slash/__init__.py +0 -9
  69. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  70. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  71. glaip_sdk/cli/slash/agent_session.py +65 -29
  72. glaip_sdk/cli/slash/prompt.py +24 -10
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +827 -232
  75. glaip_sdk/cli/slash/tui/__init__.py +34 -0
  76. glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
  77. glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
  78. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  79. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  80. glaip_sdk/cli/slash/tui/context.py +59 -0
  81. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  82. glaip_sdk/cli/slash/tui/loading.py +58 -0
  83. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  84. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  85. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  86. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  87. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  88. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  89. glaip_sdk/cli/slash/tui/toast.py +123 -0
  90. glaip_sdk/cli/transcript/__init__.py +12 -52
  91. glaip_sdk/cli/transcript/cache.py +258 -60
  92. glaip_sdk/cli/transcript/capture.py +72 -21
  93. glaip_sdk/cli/transcript/history.py +815 -0
  94. glaip_sdk/cli/transcript/launcher.py +1 -3
  95. glaip_sdk/cli/transcript/viewer.py +79 -329
  96. glaip_sdk/cli/update_notifier.py +385 -24
  97. glaip_sdk/cli/validators.py +16 -18
  98. glaip_sdk/client/__init__.py +3 -1
  99. glaip_sdk/client/_schedule_payloads.py +89 -0
  100. glaip_sdk/client/agent_runs.py +147 -0
  101. glaip_sdk/client/agents.py +370 -100
  102. glaip_sdk/client/base.py +78 -35
  103. glaip_sdk/client/hitl.py +136 -0
  104. glaip_sdk/client/main.py +25 -10
  105. glaip_sdk/client/mcps.py +166 -27
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +583 -79
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +214 -56
  113. glaip_sdk/client/validators.py +20 -48
  114. glaip_sdk/config/constants.py +11 -0
  115. glaip_sdk/exceptions.py +1 -3
  116. glaip_sdk/hitl/__init__.py +48 -0
  117. glaip_sdk/hitl/base.py +64 -0
  118. glaip_sdk/hitl/callback.py +43 -0
  119. glaip_sdk/hitl/local.py +121 -0
  120. glaip_sdk/hitl/remote.py +523 -0
  121. glaip_sdk/icons.py +9 -3
  122. glaip_sdk/mcps/__init__.py +21 -0
  123. glaip_sdk/mcps/base.py +345 -0
  124. glaip_sdk/models/__init__.py +107 -0
  125. glaip_sdk/models/agent.py +47 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/mcp.py +33 -0
  129. glaip_sdk/models/schedule.py +224 -0
  130. glaip_sdk/models/tool.py +33 -0
  131. glaip_sdk/payload_schemas/__init__.py +1 -13
  132. glaip_sdk/payload_schemas/agent.py +1 -3
  133. glaip_sdk/registry/__init__.py +55 -0
  134. glaip_sdk/registry/agent.py +164 -0
  135. glaip_sdk/registry/base.py +139 -0
  136. glaip_sdk/registry/mcp.py +253 -0
  137. glaip_sdk/registry/tool.py +445 -0
  138. glaip_sdk/rich_components.py +58 -2
  139. glaip_sdk/runner/__init__.py +76 -0
  140. glaip_sdk/runner/base.py +84 -0
  141. glaip_sdk/runner/deps.py +112 -0
  142. glaip_sdk/runner/langgraph.py +872 -0
  143. glaip_sdk/runner/logging_config.py +77 -0
  144. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  145. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  146. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  147. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  148. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  149. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  150. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  151. glaip_sdk/schedules/__init__.py +22 -0
  152. glaip_sdk/schedules/base.py +291 -0
  153. glaip_sdk/tools/__init__.py +22 -0
  154. glaip_sdk/tools/base.py +468 -0
  155. glaip_sdk/utils/__init__.py +59 -12
  156. glaip_sdk/utils/a2a/__init__.py +34 -0
  157. glaip_sdk/utils/a2a/event_processor.py +188 -0
  158. glaip_sdk/utils/agent_config.py +4 -14
  159. glaip_sdk/utils/bundler.py +403 -0
  160. glaip_sdk/utils/client.py +111 -0
  161. glaip_sdk/utils/client_utils.py +46 -28
  162. glaip_sdk/utils/datetime_helpers.py +58 -0
  163. glaip_sdk/utils/discovery.py +78 -0
  164. glaip_sdk/utils/display.py +25 -21
  165. glaip_sdk/utils/export.py +143 -0
  166. glaip_sdk/utils/general.py +1 -36
  167. glaip_sdk/utils/import_export.py +15 -16
  168. glaip_sdk/utils/import_resolver.py +524 -0
  169. glaip_sdk/utils/instructions.py +101 -0
  170. glaip_sdk/utils/rendering/__init__.py +115 -1
  171. glaip_sdk/utils/rendering/formatting.py +38 -23
  172. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  173. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  174. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  175. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  176. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  177. glaip_sdk/utils/rendering/models.py +18 -8
  178. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  179. glaip_sdk/utils/rendering/renderer/base.py +534 -882
  180. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  181. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  182. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  183. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  184. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  185. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  186. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  187. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  188. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  189. glaip_sdk/utils/rendering/state.py +204 -0
  190. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  191. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  192. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  193. glaip_sdk/utils/rendering/steps/format.py +176 -0
  194. glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
  195. glaip_sdk/utils/rendering/timing.py +36 -0
  196. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  197. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  198. glaip_sdk/utils/resource_refs.py +29 -26
  199. glaip_sdk/utils/runtime_config.py +425 -0
  200. glaip_sdk/utils/serialization.py +32 -46
  201. glaip_sdk/utils/sync.py +162 -0
  202. glaip_sdk/utils/tool_detection.py +301 -0
  203. glaip_sdk/utils/tool_storage_provider.py +140 -0
  204. glaip_sdk/utils/validation.py +20 -28
  205. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
  206. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  207. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  208. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  209. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  210. glaip_sdk/cli/commands/agents.py +0 -1412
  211. glaip_sdk/cli/commands/mcps.py +0 -1225
  212. glaip_sdk/cli/commands/tools.py +0 -597
  213. glaip_sdk/cli/utils.py +0 -1330
  214. glaip_sdk/models.py +0 -259
  215. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  216. glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
@@ -3,39 +3,49 @@
3
3
 
4
4
  Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
6
7
  """
7
8
 
9
+ import asyncio
8
10
  import json
9
11
  import logging
10
12
  from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
13
+ from contextlib import asynccontextmanager
11
14
  from os import PathLike
12
15
  from pathlib import Path
13
- from typing import Any, BinaryIO
16
+ from typing import TYPE_CHECKING, Any, BinaryIO
14
17
 
15
- import httpx
18
+ if TYPE_CHECKING:
19
+ from glaip_sdk.client.schedules import ScheduleClient
20
+ from glaip_sdk.hitl.remote import RemoteHITLHandler
16
21
 
17
- from glaip_sdk.client._agent_payloads import (
22
+ import httpx
23
+ from glaip_sdk.agents import Agent
24
+ from glaip_sdk.client.payloads.agent import (
18
25
  AgentCreateRequest,
19
26
  AgentListParams,
20
27
  AgentListResult,
21
28
  AgentUpdateRequest,
22
29
  )
30
+ from glaip_sdk.client.agent_runs import AgentRunsClient
23
31
  from glaip_sdk.client.base import BaseClient
24
32
  from glaip_sdk.client.mcps import MCPClient
25
33
  from glaip_sdk.client.run_rendering import (
26
34
  AgentRunRenderingManager,
27
35
  compute_timeout_seconds,
28
36
  )
37
+ from glaip_sdk.client.shared import build_shared_config
29
38
  from glaip_sdk.client.tools import ToolClient
30
39
  from glaip_sdk.config.constants import (
40
+ AGENT_CONFIG_FIELDS,
31
41
  DEFAULT_AGENT_FRAMEWORK,
32
42
  DEFAULT_AGENT_RUN_TIMEOUT,
33
43
  DEFAULT_AGENT_TYPE,
34
44
  DEFAULT_AGENT_VERSION,
35
45
  DEFAULT_MODEL,
36
46
  )
37
- from glaip_sdk.exceptions import NotFoundError
38
- from glaip_sdk.models import Agent
47
+ from glaip_sdk.exceptions import NotFoundError, ValidationError
48
+ from glaip_sdk.models import AgentResponse
39
49
  from glaip_sdk.payload_schemas.agent import list_server_only_fields
40
50
  from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
41
51
  from glaip_sdk.utils.client_utils import (
@@ -67,6 +77,21 @@ _MERGED_SEQUENCE_FIELDS = ("tools", "agents", "mcps")
67
77
  _DEFAULT_METADATA_TYPE = "custom"
68
78
 
69
79
 
80
+ @asynccontextmanager
81
+ async def _async_timeout_guard(
82
+ timeout_seconds: float | None,
83
+ ) -> AsyncGenerator[None, None]:
84
+ """Apply an asyncio timeout when a custom timeout is provided."""
85
+ if timeout_seconds is None:
86
+ yield
87
+ return
88
+ try:
89
+ async with asyncio.timeout(timeout_seconds):
90
+ yield
91
+ except asyncio.TimeoutError as exc:
92
+ raise httpx.TimeoutException(f"Request timed out after {timeout_seconds}s") from exc
93
+
94
+
70
95
  def _normalise_sequence(value: Any) -> list[Any] | None:
71
96
  """Normalise optional sequence inputs to plain lists."""
72
97
  if value is None:
@@ -97,9 +122,7 @@ def _merge_override_maps(
97
122
  for key, value in source.items():
98
123
  if value is None:
99
124
  continue
100
- merged[key] = (
101
- _normalise_sequence(value) if key in _MERGED_SEQUENCE_FIELDS else value
102
- )
125
+ merged[key] = _normalise_sequence(value) if key in _MERGED_SEQUENCE_FIELDS else value
103
126
  return merged
104
127
 
105
128
 
@@ -134,9 +157,7 @@ def _prepare_agent_metadata(value: Any) -> dict[str, Any]:
134
157
  return prepared
135
158
 
136
159
 
137
- def _load_agent_file_payload(
138
- file_path: Path, *, model_override: str | None
139
- ) -> dict[str, Any]:
160
+ def _load_agent_file_payload(file_path: Path, *, model_override: str | None) -> dict[str, Any]:
140
161
  """Load agent configuration from disk and normalise legacy fields."""
141
162
  if not file_path.exists():
142
163
  raise FileNotFoundError(f"Agent configuration file not found: {file_path}")
@@ -168,9 +189,7 @@ def _prepare_import_payload(
168
189
  raw_definition = load_resource_from_file(file_path)
169
190
  original_refs = _extract_original_refs(raw_definition)
170
191
 
171
- base_payload = _load_agent_file_payload(
172
- file_path, model_override=overrides_dict.get("model")
173
- )
192
+ base_payload = _load_agent_file_payload(file_path, model_override=overrides_dict.get("model"))
174
193
 
175
194
  cli_args = _build_cli_args(overrides_dict)
176
195
 
@@ -199,19 +218,7 @@ def _extract_original_refs(raw_definition: dict) -> dict[str, list]:
199
218
 
200
219
  def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
201
220
  """Build CLI args from overrides, filtering out None values."""
202
- cli_args = {
203
- key: overrides_dict.get(key)
204
- for key in (
205
- "name",
206
- "instruction",
207
- "model",
208
- "tools",
209
- "agents",
210
- "mcps",
211
- "timeout",
212
- )
213
- if overrides_dict.get(key) is not None
214
- }
221
+ cli_args = {key: overrides_dict.get(key) for key in AGENT_CONFIG_FIELDS if overrides_dict.get(key) is not None}
215
222
 
216
223
  # Normalize sequence fields
217
224
  for field in _MERGED_SEQUENCE_FIELDS:
@@ -223,11 +230,7 @@ def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
223
230
 
224
231
  def _build_additional_args(overrides_dict: dict, cli_args: dict) -> dict[str, Any]:
225
232
  """Build additional args not already in CLI args."""
226
- return {
227
- key: value
228
- for key, value in overrides_dict.items()
229
- if value is not None and key not in cli_args
230
- }
233
+ return {key: value for key, value in overrides_dict.items() if value is not None and key not in cli_args}
231
234
 
232
235
 
233
236
  def _remove_model_fields_if_needed(merged: dict, overrides_dict: dict) -> None:
@@ -264,6 +267,8 @@ class AgentClient(BaseClient):
264
267
  self._renderer_manager = AgentRunRenderingManager(logger)
265
268
  self._tool_client: ToolClient | None = None
266
269
  self._mcp_client: MCPClient | None = None
270
+ self._runs_client: AgentRunsClient | None = None
271
+ self._schedule_client: ScheduleClient | None = None
267
272
 
268
273
  def list_agents(
269
274
  self,
@@ -278,9 +283,7 @@ class AgentClient(BaseClient):
278
283
  """
279
284
  if query is not None and kwargs:
280
285
  # Both query object and individual parameters provided
281
- raise ValueError(
282
- "Provide either `query` or individual filter arguments, not both."
283
- )
286
+ raise ValueError("Provide either `query` or individual filter arguments, not both.")
284
287
 
285
288
  if query is None:
286
289
  # Create query from individual parameters for backward compatibility
@@ -339,7 +342,19 @@ class AgentClient(BaseClient):
339
342
 
340
343
  def get_agent_by_id(self, agent_id: str) -> Agent:
341
344
  """Get agent by ID."""
342
- data = self._request("GET", f"/agents/{agent_id}")
345
+ try:
346
+ data = self._request("GET", f"/agents/{agent_id}")
347
+ except ValidationError as exc:
348
+ if exc.status_code == 422:
349
+ message = f"Agent '{agent_id}' not found"
350
+ raise NotFoundError(
351
+ message,
352
+ status_code=404,
353
+ error_type=exc.error_type,
354
+ payload=exc.payload,
355
+ request_id=exc.request_id,
356
+ ) from exc
357
+ raise
343
358
 
344
359
  if isinstance(data, str):
345
360
  # Some backends may respond with plain text for missing agents.
@@ -352,7 +367,8 @@ class AgentClient(BaseClient):
352
367
  status_code=404,
353
368
  )
354
369
 
355
- return Agent(**data)._set_client(self)
370
+ response = AgentResponse(**data)
371
+ return Agent.from_response(response, client=self)
356
372
 
357
373
  def find_agents(self, name: str | None = None) -> list[Agent]:
358
374
  """Find agents by name."""
@@ -366,15 +382,27 @@ class AgentClient(BaseClient):
366
382
  # Renderer delegation helpers
367
383
  # ------------------------------------------------------------------ #
368
384
  def _get_renderer_manager(self) -> AgentRunRenderingManager:
385
+ """Get or create the renderer manager instance.
386
+
387
+ Returns:
388
+ AgentRunRenderingManager instance.
389
+ """
369
390
  manager = getattr(self, "_renderer_manager", None)
370
391
  if manager is None:
371
392
  manager = AgentRunRenderingManager(logger)
372
393
  self._renderer_manager = manager
373
394
  return manager
374
395
 
375
- def _create_renderer(
376
- self, renderer: RichStreamRenderer | str | None, **kwargs: Any
377
- ) -> RichStreamRenderer:
396
+ def _create_renderer(self, renderer: RichStreamRenderer | str | None, **kwargs: Any) -> RichStreamRenderer:
397
+ """Create or return a renderer instance.
398
+
399
+ Args:
400
+ renderer: Renderer instance, string identifier, or None.
401
+ **kwargs: Additional keyword arguments (e.g., verbose).
402
+
403
+ Returns:
404
+ RichStreamRenderer instance.
405
+ """
378
406
  manager = self._get_renderer_manager()
379
407
  verbose = kwargs.get("verbose", False)
380
408
  if isinstance(renderer, RichStreamRenderer) or hasattr(renderer, "on_start"):
@@ -388,7 +416,21 @@ class AgentClient(BaseClient):
388
416
  timeout_seconds: float,
389
417
  agent_name: str | None,
390
418
  meta: dict[str, Any],
419
+ hitl_handler: "RemoteHITLHandler | None" = None,
391
420
  ) -> tuple[str, dict[str, Any], float | None, float | None]:
421
+ """Process stream events from an HTTP response.
422
+
423
+ Args:
424
+ stream_response: HTTP response stream.
425
+ renderer: Renderer to use for displaying events.
426
+ timeout_seconds: Timeout in seconds.
427
+ agent_name: Optional agent name.
428
+ meta: Metadata dictionary.
429
+ hitl_handler: Optional HITL handler for approval callbacks.
430
+
431
+ Returns:
432
+ Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
433
+ """
392
434
  manager = self._get_renderer_manager()
393
435
  return manager.process_stream_events(
394
436
  stream_response,
@@ -396,6 +438,7 @@ class AgentClient(BaseClient):
396
438
  timeout_seconds,
397
439
  agent_name,
398
440
  meta,
441
+ hitl_handler=hitl_handler,
399
442
  )
400
443
 
401
444
  def _finalize_renderer(
@@ -406,30 +449,73 @@ class AgentClient(BaseClient):
406
449
  started_monotonic: float | None,
407
450
  finished_monotonic: float | None,
408
451
  ) -> str:
452
+ """Finalize the renderer and return the final response text.
453
+
454
+ Args:
455
+ renderer: Renderer to finalize.
456
+ final_text: Final text content.
457
+ stats_usage: Usage statistics dictionary.
458
+ started_monotonic: Start time (monotonic).
459
+ finished_monotonic: Finish time (monotonic).
460
+
461
+ Returns:
462
+ Final text string.
463
+ """
464
+ from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
465
+
409
466
  manager = self._get_renderer_manager()
410
- return manager.finalize_renderer(
411
- renderer,
412
- final_text,
413
- stats_usage,
414
- started_monotonic,
415
- finished_monotonic,
467
+ return finalize_render_manager(
468
+ manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
416
469
  )
417
470
 
418
471
  def _get_tool_client(self) -> ToolClient:
472
+ """Get or create the tool client instance.
473
+
474
+ Returns:
475
+ ToolClient instance.
476
+ """
419
477
  if self._tool_client is None:
420
478
  self._tool_client = ToolClient(parent_client=self)
421
479
  return self._tool_client
422
480
 
423
481
  def _get_mcp_client(self) -> MCPClient:
482
+ """Get or create the MCP client instance.
483
+
484
+ Returns:
485
+ MCPClient instance.
486
+ """
424
487
  if self._mcp_client is None:
425
488
  self._mcp_client = MCPClient(parent_client=self)
426
489
  return self._mcp_client
427
490
 
491
+ @property
492
+ def schedules(self) -> "ScheduleClient":
493
+ """Get or create the schedule client instance.
494
+
495
+ Returns:
496
+ ScheduleClient instance.
497
+ """
498
+ if self._schedule_client is None:
499
+ # Import here to avoid circular import
500
+ from glaip_sdk.client.schedules import ScheduleClient # noqa: PLC0415
501
+
502
+ self._schedule_client = ScheduleClient(parent_client=self)
503
+ return self._schedule_client
504
+
428
505
  def _normalise_reference_entry(
429
506
  self,
430
507
  entry: Any,
431
508
  fallback_iter: Iterator[Any] | None,
432
509
  ) -> tuple[str | None, str | None]:
510
+ """Normalize a reference entry to extract ID and name.
511
+
512
+ Args:
513
+ entry: Reference entry (string, dict, or other).
514
+ fallback_iter: Optional iterator for fallback values.
515
+
516
+ Returns:
517
+ Tuple of (entry_id, entry_name).
518
+ """
433
519
  entry_id: str | None = None
434
520
  entry_name: str | None = None
435
521
 
@@ -466,6 +552,19 @@ class AgentClient(BaseClient):
466
552
  label: str,
467
553
  plural_label: str | None = None,
468
554
  ) -> list[str] | None:
555
+ """Resolve a list of resource references to IDs.
556
+
557
+ Args:
558
+ items: List of resource references to resolve.
559
+ references: Optional list of reference objects for fallback.
560
+ fetch_by_id: Function to fetch resource by ID.
561
+ find_by_name: Function to find resources by name.
562
+ label: Singular label for error messages.
563
+ plural_label: Plural label for error messages.
564
+
565
+ Returns:
566
+ List of resolved resource IDs, or None if items is empty.
567
+ """
469
568
  if not items:
470
569
  return None
471
570
 
@@ -497,6 +596,22 @@ class AgentClient(BaseClient):
497
596
  singular: str,
498
597
  plural: str,
499
598
  ) -> str:
599
+ """Resolve a single resource reference to an ID.
600
+
601
+ Args:
602
+ entry: Resource reference to resolve.
603
+ fallback_iter: Optional iterator for fallback values.
604
+ fetch_by_id: Function to fetch resource by ID.
605
+ find_by_name: Function to find resources by name.
606
+ singular: Singular label for error messages.
607
+ plural: Plural label for error messages.
608
+
609
+ Returns:
610
+ Resolved resource ID string.
611
+
612
+ Raises:
613
+ ValueError: If the resource cannot be resolved.
614
+ """
500
615
  entry_id, entry_name = self._normalise_reference_entry(entry, fallback_iter)
501
616
 
502
617
  validated_id = self._validate_resource_id(fetch_by_id, entry_id)
@@ -506,15 +621,21 @@ class AgentClient(BaseClient):
506
621
  return entry_id
507
622
 
508
623
  if entry_name:
509
- resolved, success = self._resolve_resource_by_name(
510
- find_by_name, entry_name, singular, plural
511
- )
624
+ resolved, success = self._resolve_resource_by_name(find_by_name, entry_name, singular, plural)
512
625
  return resolved if success else entry_name
513
626
 
514
627
  raise ValueError(f"{singular} references must include a valid ID or name.")
515
628
 
516
629
  @staticmethod
517
630
  def _coerce_reference_value(entry: Any) -> str:
631
+ """Coerce a reference entry to a string value.
632
+
633
+ Args:
634
+ entry: Reference entry (dict, string, or other).
635
+
636
+ Returns:
637
+ String representation of the reference.
638
+ """
518
639
  if isinstance(entry, dict):
519
640
  if entry.get("id"):
520
641
  return str(entry["id"])
@@ -523,9 +644,16 @@ class AgentClient(BaseClient):
523
644
  return str(entry)
524
645
 
525
646
  @staticmethod
526
- def _validate_resource_id(
527
- fetch_by_id: Callable[[str], Any], candidate_id: str | None
528
- ) -> str | None:
647
+ def _validate_resource_id(fetch_by_id: Callable[[str], Any], candidate_id: str | None) -> str | None:
648
+ """Validate a resource ID by attempting to fetch it.
649
+
650
+ Args:
651
+ fetch_by_id: Function to fetch resource by ID.
652
+ candidate_id: Candidate ID to validate.
653
+
654
+ Returns:
655
+ Validated ID if found, None otherwise.
656
+ """
529
657
  if not candidate_id:
530
658
  return None
531
659
  try:
@@ -541,27 +669,33 @@ class AgentClient(BaseClient):
541
669
  singular: str,
542
670
  plural: str,
543
671
  ) -> tuple[str, bool]:
672
+ """Resolve a resource by name to an ID.
673
+
674
+ Args:
675
+ find_by_name: Function to find resources by name.
676
+ entry_name: Name of the resource to find.
677
+ singular: Singular label for error messages.
678
+ plural: Plural label for error messages.
679
+
680
+ Returns:
681
+ Tuple of (resolved_id, success).
682
+
683
+ Raises:
684
+ ValueError: If resource not found or multiple matches exist.
685
+ """
544
686
  try:
545
687
  matches = find_by_name(entry_name)
546
688
  except Exception:
547
689
  return entry_name, False
548
690
 
549
691
  if not matches:
550
- raise ValueError(
551
- f"{singular} '{entry_name}' not found in current workspace."
552
- )
692
+ raise ValueError(f"{singular} '{entry_name}' not found in current workspace.")
553
693
  if len(matches) > 1:
554
- exact = [
555
- m
556
- for m in matches
557
- if getattr(m, "name", "").lower() == entry_name.lower()
558
- ]
694
+ exact = [m for m in matches if getattr(m, "name", "").lower() == entry_name.lower()]
559
695
  if len(exact) == 1:
560
696
  matches = exact
561
697
  else:
562
- raise ValueError(
563
- f"Multiple {plural} named '{entry_name}'. Please disambiguate."
564
- )
698
+ raise ValueError(f"Multiple {plural} named '{entry_name}'. Please disambiguate.")
565
699
  return str(matches[0].id), True
566
700
 
567
701
  def _resolve_tool_ids(
@@ -569,6 +703,15 @@ class AgentClient(BaseClient):
569
703
  tools: list[Any] | None,
570
704
  references: list[Any] | None = None,
571
705
  ) -> list[str] | None:
706
+ """Resolve tool references to IDs.
707
+
708
+ Args:
709
+ tools: List of tool references to resolve.
710
+ references: Optional list of reference objects for fallback.
711
+
712
+ Returns:
713
+ List of resolved tool IDs, or None if tools is empty.
714
+ """
572
715
  tool_client = self._get_tool_client()
573
716
  return self._resolve_resource_ids(
574
717
  tools,
@@ -584,6 +727,15 @@ class AgentClient(BaseClient):
584
727
  agents: list[Any] | None,
585
728
  references: list[Any] | None = None,
586
729
  ) -> list[str] | None:
730
+ """Resolve agent references to IDs.
731
+
732
+ Args:
733
+ agents: List of agent references to resolve.
734
+ references: Optional list of reference objects for fallback.
735
+
736
+ Returns:
737
+ List of resolved agent IDs, or None if agents is empty.
738
+ """
587
739
  return self._resolve_resource_ids(
588
740
  agents,
589
741
  references,
@@ -598,6 +750,15 @@ class AgentClient(BaseClient):
598
750
  mcps: list[Any] | None,
599
751
  references: list[Any] | None = None,
600
752
  ) -> list[str] | None:
753
+ """Resolve MCP references to IDs.
754
+
755
+ Args:
756
+ mcps: List of MCP references to resolve.
757
+ references: Optional list of reference objects for fallback.
758
+
759
+ Returns:
760
+ List of resolved MCP IDs, or None if mcps is empty.
761
+ """
601
762
  mcp_client = self._get_mcp_client()
602
763
  return self._resolve_resource_ids(
603
764
  mcps,
@@ -610,9 +771,7 @@ class AgentClient(BaseClient):
610
771
 
611
772
  def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
612
773
  """Create an agent using a fully prepared payload mapping."""
613
- known, extras = _split_known_and_extra(
614
- payload, AgentCreateRequest.__dataclass_fields__
615
- )
774
+ known, extras = _split_known_and_extra(payload, AgentCreateRequest.__dataclass_fields__)
616
775
 
617
776
  name = known.pop("name", None)
618
777
  instruction = known.pop("instruction", None)
@@ -691,7 +850,8 @@ class AgentClient(BaseClient):
691
850
  get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
692
851
  json=payload_dict,
693
852
  )
694
- return Agent(**full_agent_data)._set_client(self)
853
+ response = AgentResponse(**full_agent_data)
854
+ return Agent.from_response(response, client=self)
695
855
 
696
856
  def create_agent(
697
857
  self,
@@ -721,9 +881,7 @@ class AgentClient(BaseClient):
721
881
  overrides = _merge_override_maps(base_overrides, kwargs)
722
882
 
723
883
  if file is not None:
724
- payload = _prepare_import_payload(
725
- Path(file).expanduser(), overrides, drop_model_fields=True
726
- )
884
+ payload = _prepare_import_payload(Path(file).expanduser(), overrides, drop_model_fields=True)
727
885
  if overrides.get("model") is None:
728
886
  payload.pop("model", None)
729
887
  else:
@@ -746,9 +904,7 @@ class AgentClient(BaseClient):
746
904
  payload: Mapping[str, Any],
747
905
  ) -> "Agent":
748
906
  """Update an agent using a prepared payload mapping."""
749
- known, extras = _split_known_and_extra(
750
- payload, AgentUpdateRequest.__dataclass_fields__
751
- )
907
+ known, extras = _split_known_and_extra(payload, AgentUpdateRequest.__dataclass_fields__)
752
908
  _normalise_sequence_fields(known)
753
909
 
754
910
  tool_refs = extras.pop("_tool_refs", None)
@@ -790,8 +946,9 @@ class AgentClient(BaseClient):
790
946
 
791
947
  payload_dict = request.to_payload(current_agent)
792
948
 
793
- response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
794
- return Agent(**response)._set_client(self)
949
+ api_response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
950
+ response = AgentResponse(**api_response)
951
+ return Agent.from_response(response, client=self)
795
952
 
796
953
  def update_agent(
797
954
  self,
@@ -818,9 +975,7 @@ class AgentClient(BaseClient):
818
975
  overrides = _merge_override_maps(base_overrides, kwargs)
819
976
 
820
977
  if file is not None:
821
- payload = _prepare_import_payload(
822
- Path(file).expanduser(), overrides, drop_model_fields=True
823
- )
978
+ payload = _prepare_import_payload(Path(file).expanduser(), overrides, drop_model_fields=True)
824
979
  else:
825
980
  payload = overrides
826
981
 
@@ -840,6 +995,62 @@ class AgentClient(BaseClient):
840
995
  """Delete an agent."""
841
996
  self._request("DELETE", f"/agents/{agent_id}")
842
997
 
998
+ def upsert_agent(self, identifier: str | Agent, **kwargs) -> Agent:
999
+ """Create or update an agent by instance, ID, or name.
1000
+
1001
+ Args:
1002
+ identifier: Agent instance, ID (UUID string), or name
1003
+ **kwargs: Agent configuration (instruction, description, tools, etc.)
1004
+
1005
+ Returns:
1006
+ The created or updated agent.
1007
+
1008
+ Example:
1009
+ >>> # By name (creates if not exists)
1010
+ >>> agent = client.agents.upsert_agent(
1011
+ ... "hello_agent",
1012
+ ... instruction="You are a helpful assistant.",
1013
+ ... description="A friendly agent",
1014
+ ... )
1015
+ >>> # By instance
1016
+ >>> agent = client.agents.upsert_agent(existing_agent, description="Updated")
1017
+ >>> # By ID
1018
+ >>> agent = client.agents.upsert_agent("uuid-here", description="Updated")
1019
+ """
1020
+ # Handle Agent instance
1021
+ if isinstance(identifier, Agent):
1022
+ if identifier.id:
1023
+ logger.info("Updating agent by instance: %s", identifier.name)
1024
+ return self.update_agent(identifier.id, name=identifier.name, **kwargs)
1025
+ identifier = identifier.name
1026
+
1027
+ # Handle string (ID or name)
1028
+ if isinstance(identifier, str):
1029
+ # Check if it's a UUID
1030
+ if is_uuid(identifier):
1031
+ logger.info("Updating agent by ID: %s", identifier)
1032
+ return self.update_agent(identifier, **kwargs)
1033
+
1034
+ # It's a name - find or create
1035
+ return self._upsert_agent_by_name(identifier, **kwargs)
1036
+
1037
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
1038
+
1039
+ def _upsert_agent_by_name(self, name: str, **kwargs) -> Agent:
1040
+ """Find agent by name and update, or create if not found."""
1041
+ existing = self.find_agents(name)
1042
+
1043
+ if len(existing) == 1:
1044
+ logger.info("Updating existing agent: %s", name)
1045
+ return self.update_agent(existing[0].id, name=name, **kwargs)
1046
+
1047
+ if len(existing) > 1:
1048
+ raise ValueError(f"Multiple agents found with name '{name}'")
1049
+
1050
+ # Create new agent
1051
+ logger.info("Creating new agent: %s", name)
1052
+ return self.create_agent(name=name, **kwargs)
1053
+
843
1054
  def _prepare_sync_request_data(
844
1055
  self,
845
1056
  message: str,
@@ -882,9 +1093,7 @@ class AgentClient(BaseClient):
882
1093
  payload["tty"] = True
883
1094
  return payload, None, None, headers, None
884
1095
 
885
- def _get_timeout_values(
886
- self, timeout: float | None, **kwargs: Any
887
- ) -> tuple[float, float]:
1096
+ def _get_timeout_values(self, timeout: float | None, **kwargs: Any) -> tuple[float, float]:
888
1097
  """Get request timeout and execution timeout values.
889
1098
 
890
1099
  Args:
@@ -906,9 +1115,35 @@ class AgentClient(BaseClient):
906
1115
  tty: bool = False,
907
1116
  *,
908
1117
  renderer: RichStreamRenderer | str | None = "auto",
1118
+ runtime_config: dict[str, Any] | None = None,
1119
+ hitl_handler: "RemoteHITLHandler | None" = None,
909
1120
  **kwargs,
910
1121
  ) -> str:
911
- """Run an agent with a message, streaming via a renderer."""
1122
+ """Run an agent with a message, streaming via a renderer.
1123
+
1124
+ Args:
1125
+ agent_id: The ID of the agent to run.
1126
+ message: The message to send to the agent.
1127
+ files: Optional list of files to include with the request.
1128
+ tty: Whether to enable TTY mode.
1129
+ renderer: Renderer for streaming output.
1130
+ runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1131
+ Keys should be platform IDs. Example:
1132
+ {
1133
+ "tool_configs": {"tool-id": {"param": "value"}},
1134
+ "mcp_configs": {"mcp-id": {"setting": "on"}},
1135
+ "agent_config": {"planning": True},
1136
+ }
1137
+ hitl_handler: Optional RemoteHITLHandler for approval callbacks.
1138
+ Set GLAIP_HITL_AUTO_APPROVE=true for auto-approval without handler.
1139
+ **kwargs: Additional arguments to pass to the run API.
1140
+
1141
+ Returns:
1142
+ The agent's response as a string.
1143
+ """
1144
+ # Include runtime_config in kwargs only when caller hasn't already provided it
1145
+ if runtime_config is not None and "runtime_config" not in kwargs:
1146
+ kwargs["runtime_config"] = runtime_config
912
1147
  (
913
1148
  payload,
914
1149
  data_payload,
@@ -957,6 +1192,7 @@ class AgentClient(BaseClient):
957
1192
  timeout_seconds,
958
1193
  agent_name,
959
1194
  meta,
1195
+ hitl_handler=hitl_handler,
960
1196
  )
961
1197
 
962
1198
  except KeyboardInterrupt:
@@ -973,6 +1209,13 @@ class AgentClient(BaseClient):
973
1209
  if multipart_data:
974
1210
  multipart_data.close()
975
1211
 
1212
+ # Wait for pending HITL decisions before returning
1213
+ if hitl_handler and hasattr(hitl_handler, "wait_for_pending_decisions"):
1214
+ try:
1215
+ hitl_handler.wait_for_pending_decisions(timeout=30)
1216
+ except Exception as e:
1217
+ logger.warning(f"Error waiting for HITL decisions: {e}")
1218
+
976
1219
  return self._finalize_renderer(
977
1220
  r,
978
1221
  final_text,
@@ -1009,9 +1252,7 @@ class AgentClient(BaseClient):
1009
1252
  headers = {"Accept": SSE_CONTENT_TYPE}
1010
1253
  return payload, None, None, headers
1011
1254
 
1012
- def _create_async_client_config(
1013
- self, timeout: float | None, headers: dict | None
1014
- ) -> dict:
1255
+ def _create_async_client_config(self, timeout: float | None, headers: dict | None) -> dict:
1015
1256
  """Create async client configuration with proper headers and timeout."""
1016
1257
  config = self._build_async_client(timeout or self.timeout)
1017
1258
  if headers:
@@ -1040,9 +1281,7 @@ class AgentClient(BaseClient):
1040
1281
  ) as stream_response:
1041
1282
  stream_response.raise_for_status()
1042
1283
 
1043
- async for event in aiter_sse_events(
1044
- stream_response, timeout_seconds, agent_name
1045
- ):
1284
+ async for event in aiter_sse_events(stream_response, timeout_seconds, agent_name):
1046
1285
  try:
1047
1286
  chunk = json.loads(event["data"])
1048
1287
  yield chunk
@@ -1056,7 +1295,9 @@ class AgentClient(BaseClient):
1056
1295
  message: str,
1057
1296
  files: list[str | BinaryIO] | None = None,
1058
1297
  *,
1059
- timeout: float | None = None,
1298
+ request_timeout: float | None = None,
1299
+ runtime_config: dict[str, Any] | None = None,
1300
+ hitl_handler: "RemoteHITLHandler | None" = None,
1060
1301
  **kwargs,
1061
1302
  ) -> AsyncGenerator[dict, None]:
1062
1303
  """Async run an agent with a message, yielding streaming JSON chunks.
@@ -1065,31 +1306,53 @@ class AgentClient(BaseClient):
1065
1306
  agent_id: ID of the agent to run
1066
1307
  message: Message to send to the agent
1067
1308
  files: Optional list of files to include
1068
- timeout: Request timeout in seconds
1309
+ request_timeout: Optional request timeout in seconds (defaults to client timeout)
1310
+ runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1311
+ Keys should be platform IDs. Example:
1312
+ {
1313
+ "tool_configs": {"tool-id": {"param": "value"}},
1314
+ "mcp_configs": {"mcp-id": {"setting": "on"}},
1315
+ "agent_config": {"planning": True},
1316
+ }
1317
+ hitl_handler: Optional HITL handler for remote approval requests.
1318
+ Note: Async HITL support is currently deferred. This parameter
1319
+ is accepted for API consistency but will raise NotImplementedError
1320
+ if provided.
1069
1321
  **kwargs: Additional arguments (chat_history, pii_mapping, etc.)
1070
1322
 
1071
1323
  Yields:
1072
1324
  Dictionary containing parsed JSON chunks from the streaming response
1073
1325
 
1074
1326
  Raises:
1327
+ NotImplementedError: If hitl_handler is provided (async HITL not yet supported)
1075
1328
  AgentTimeoutError: When agent execution times out
1076
1329
  httpx.TimeoutException: When general timeout occurs
1077
1330
  Exception: For other unexpected errors
1078
1331
  """
1332
+ if hitl_handler is not None:
1333
+ raise NotImplementedError(
1334
+ "Async HITL support is currently deferred. "
1335
+ "Please use the synchronous run_agent() method with hitl_handler."
1336
+ )
1337
+ # Include runtime_config in kwargs only when caller hasn't already provided it
1338
+ if runtime_config is not None and "runtime_config" not in kwargs:
1339
+ kwargs["runtime_config"] = runtime_config
1340
+ # Derive timeout values for request/control flow
1341
+ legacy_timeout = kwargs.get("timeout")
1342
+ http_timeout_override = request_timeout if request_timeout is not None else legacy_timeout
1343
+ http_timeout = http_timeout_override or self.timeout
1344
+
1079
1345
  # Prepare request data
1080
- payload, data_payload, files_payload, headers = self._prepare_request_data(
1081
- message, files, **kwargs
1082
- )
1346
+ payload, data_payload, files_payload, headers = self._prepare_request_data(message, files, **kwargs)
1083
1347
 
1084
1348
  # Create async client configuration
1085
- async_client_config = self._create_async_client_config(timeout, headers)
1349
+ async_client_config = self._create_async_client_config(http_timeout_override, headers)
1086
1350
 
1087
1351
  # Get execution timeout for streaming control
1088
1352
  timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
1089
1353
  agent_name = kwargs.get("agent_name")
1090
1354
 
1091
- try:
1092
- # Create async client and stream response
1355
+ async def _chunk_stream() -> AsyncGenerator[dict, None]:
1093
1356
  async with httpx.AsyncClient(**async_client_config) as async_client:
1094
1357
  async for chunk in self._stream_agent_response(
1095
1358
  async_client,
@@ -1103,7 +1366,14 @@ class AgentClient(BaseClient):
1103
1366
  ):
1104
1367
  yield chunk
1105
1368
 
1106
- finally:
1107
- # Ensure cleanup - this is handled by the calling context
1108
- # but we keep this for safety in case of future changes
1109
- pass
1369
+ async with _async_timeout_guard(http_timeout):
1370
+ async for chunk in _chunk_stream():
1371
+ yield chunk
1372
+
1373
+ @property
1374
+ def runs(self) -> "AgentRunsClient":
1375
+ """Get the agent runs client."""
1376
+ if self._runs_client is None:
1377
+ shared_config = build_shared_config(self)
1378
+ self._runs_client = AgentRunsClient(**shared_config)
1379
+ return self._runs_client