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
@@ -1,41 +1,52 @@
1
- #!/usr/bin/env python3
1
+ # pylint: disable=duplicate-code
2
2
  """Agent client for AIP SDK.
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
12
+ import warnings
10
13
  from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
14
+ from contextlib import asynccontextmanager
11
15
  from os import PathLike
12
16
  from pathlib import Path
13
- from typing import Any, BinaryIO
17
+ from typing import TYPE_CHECKING, Any, BinaryIO
14
18
 
15
- import httpx
19
+ if TYPE_CHECKING:
20
+ from glaip_sdk.client.schedules import ScheduleClient
21
+ from glaip_sdk.hitl.remote import RemoteHITLHandler
16
22
 
17
- from glaip_sdk.client._agent_payloads import (
23
+ import httpx
24
+ from glaip_sdk.agents import Agent
25
+ from glaip_sdk.client.agent_runs import AgentRunsClient
26
+ from glaip_sdk.client.base import BaseClient
27
+ from glaip_sdk.client.mcps import MCPClient
28
+ from glaip_sdk.client.payloads.agent import (
18
29
  AgentCreateRequest,
19
30
  AgentListParams,
20
31
  AgentListResult,
21
32
  AgentUpdateRequest,
22
33
  )
23
- from glaip_sdk.client.base import BaseClient
24
- from glaip_sdk.client.mcps import MCPClient
25
34
  from glaip_sdk.client.run_rendering import (
26
35
  AgentRunRenderingManager,
27
36
  compute_timeout_seconds,
28
37
  )
38
+ from glaip_sdk.client.shared import build_shared_config
29
39
  from glaip_sdk.client.tools import ToolClient
30
40
  from glaip_sdk.config.constants import (
41
+ AGENT_CONFIG_FIELDS,
31
42
  DEFAULT_AGENT_FRAMEWORK,
32
43
  DEFAULT_AGENT_RUN_TIMEOUT,
33
44
  DEFAULT_AGENT_TYPE,
34
45
  DEFAULT_AGENT_VERSION,
35
- DEFAULT_MODEL,
36
46
  )
37
47
  from glaip_sdk.exceptions import NotFoundError, ValidationError
38
- from glaip_sdk.models import Agent
48
+ from glaip_sdk.models import AgentResponse
49
+ from glaip_sdk.models.constants import DEFAULT_MODEL
39
50
  from glaip_sdk.payload_schemas.agent import list_server_only_fields
40
51
  from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
41
52
  from glaip_sdk.utils.client_utils import (
@@ -67,6 +78,21 @@ _MERGED_SEQUENCE_FIELDS = ("tools", "agents", "mcps")
67
78
  _DEFAULT_METADATA_TYPE = "custom"
68
79
 
69
80
 
81
+ @asynccontextmanager
82
+ async def _async_timeout_guard(
83
+ timeout_seconds: float | None,
84
+ ) -> AsyncGenerator[None, None]:
85
+ """Apply an asyncio timeout when a custom timeout is provided."""
86
+ if timeout_seconds is None:
87
+ yield
88
+ return
89
+ try:
90
+ async with asyncio.timeout(timeout_seconds):
91
+ yield
92
+ except asyncio.TimeoutError as exc:
93
+ raise httpx.TimeoutException(f"Request timed out after {timeout_seconds}s") from exc
94
+
95
+
70
96
  def _normalise_sequence(value: Any) -> list[Any] | None:
71
97
  """Normalise optional sequence inputs to plain lists."""
72
98
  if value is None:
@@ -193,19 +219,7 @@ def _extract_original_refs(raw_definition: dict) -> dict[str, list]:
193
219
 
194
220
  def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
195
221
  """Build CLI args from overrides, filtering out None values."""
196
- cli_args = {
197
- key: overrides_dict.get(key)
198
- for key in (
199
- "name",
200
- "instruction",
201
- "model",
202
- "tools",
203
- "agents",
204
- "mcps",
205
- "timeout",
206
- )
207
- if overrides_dict.get(key) is not None
208
- }
222
+ cli_args = {key: overrides_dict.get(key) for key in AGENT_CONFIG_FIELDS if overrides_dict.get(key) is not None}
209
223
 
210
224
  # Normalize sequence fields
211
225
  for field in _MERGED_SEQUENCE_FIELDS:
@@ -242,18 +256,134 @@ class AgentClient(BaseClient):
242
256
  self,
243
257
  *,
244
258
  parent_client: BaseClient | None = None,
259
+ lm_cache_ttl: float = 3600.0,
245
260
  **kwargs: Any,
246
261
  ) -> None:
247
262
  """Initialize the agent client.
248
263
 
249
264
  Args:
250
- parent_client: Parent client to adopt session/config from
251
- **kwargs: Additional arguments for standalone initialization
265
+ parent_client: Parent client to adopt session/config from.
266
+ lm_cache_ttl: TTL for the language model list cache in seconds.
267
+ Defaults to 3600 (1 hour).
268
+ **kwargs: Additional arguments for standalone initialization.
252
269
  """
253
270
  super().__init__(parent_client=parent_client, **kwargs)
254
271
  self._renderer_manager = AgentRunRenderingManager(logger)
255
272
  self._tool_client: ToolClient | None = None
256
273
  self._mcp_client: MCPClient | None = None
274
+ self._runs_client: AgentRunsClient | None = None
275
+ self._schedule_client: ScheduleClient | None = None
276
+
277
+ self._lm_cache: list[dict[str, Any]] | None = None
278
+ self._lm_cache_time: float = 0.0
279
+ self._lm_cache_ttl: float = lm_cache_ttl
280
+
281
+ def clear_language_model_cache(self) -> None:
282
+ """Invalidate the language model list cache.
283
+
284
+ Forces the next call to list_language_models() to fetch a fresh list
285
+ from the server.
286
+ """
287
+ self._lm_cache = None
288
+ self._lm_cache_time = 0.0
289
+ logger.debug("Language model cache invalidated.")
290
+
291
+ def _resolve_language_model_id(self, model_str: str | None) -> str | None:
292
+ """Resolve a friendly model name to a server language model ID.
293
+
294
+ Handles provider name mapping (e.g., 'deepinfra/model' → 'openai-compatible/model')
295
+ by checking both the original provider name and its driver equivalent.
296
+
297
+ Args:
298
+ model_str: The model string to resolve (e.g., 'openai/gpt-4o', 'deepinfra/Qwen3-30B').
299
+
300
+ Returns:
301
+ The resolved server model ID (UUID), or None if not found.
302
+
303
+ Examples:
304
+ >>> _resolve_language_model_id("openai/gpt-4o")
305
+ "uuid-1234-..."
306
+ >>> _resolve_language_model_id("deepinfra/Qwen3-30B") # Maps to openai-compatible
307
+ "uuid-5678-..."
308
+ """
309
+ if not model_str:
310
+ return None
311
+
312
+ # If resolution is explicitly disabled (e.g. in unit tests to avoid extra API calls), skip it
313
+ if getattr(self, "_skip_model_resolution", False):
314
+ return None
315
+
316
+ try:
317
+ models = self.list_language_models()
318
+
319
+ # Try exact match first
320
+ model_id = self._find_exact_model_match(model_str, models)
321
+ if model_id:
322
+ return model_id
323
+
324
+ # Try with provider-to-driver mapping
325
+ return self._try_resolve_with_driver_mapping(model_str, models)
326
+ except Exception:
327
+ pass
328
+
329
+ return None
330
+
331
+ def _find_exact_model_match(self, model_str: str, models: list[dict[str, Any]]) -> str | None:
332
+ """Find exact model match in models list.
333
+
334
+ Args:
335
+ model_str: Model string to match.
336
+ models: List of language model dictionaries from server.
337
+
338
+ Returns:
339
+ Model ID (UUID) if found, None otherwise.
340
+ """
341
+ for model_info in models:
342
+ provider = model_info.get("provider")
343
+ name = model_info.get("name")
344
+ if provider and name:
345
+ full_name = f"{provider}/{name}"
346
+ if full_name == model_str:
347
+ return model_info.get("id")
348
+ if name == model_str:
349
+ return model_info.get("id")
350
+ return None
351
+
352
+ def _try_resolve_with_driver_mapping(self, model_str: str, models: list[dict[str, Any]]) -> str | None:
353
+ """Try to resolve model using provider-to-driver mapping.
354
+
355
+ Maps provider names to their driver implementations (e.g., deepinfra → openai-compatible)
356
+ and searches the models list with the driver name.
357
+
358
+ Args:
359
+ model_str: Model string in provider/model format (e.g., "deepinfra/Qwen3-30B").
360
+ models: List of language model dictionaries from server.
361
+
362
+ Returns:
363
+ Model ID (UUID) if found, None otherwise.
364
+ """
365
+ if "/" not in model_str:
366
+ return None
367
+
368
+ from glaip_sdk.models._provider_mappings import get_driver # noqa: PLC0415
369
+
370
+ provider, model_name = model_str.split("/", 1)
371
+ driver = get_driver(provider)
372
+
373
+ # Only try with driver if it's different from provider
374
+ if driver == provider:
375
+ return None
376
+
377
+ driver_model_str = f"{driver}/{model_name}"
378
+ for model_info in models:
379
+ provider_field = model_info.get("provider")
380
+ name_field = model_info.get("name")
381
+ if provider_field and name_field:
382
+ full_name = f"{provider_field}/{name_field}"
383
+ if full_name == driver_model_str:
384
+ return model_info.get("id")
385
+
386
+ return None
257
387
 
258
388
  def list_agents(
259
389
  self,
@@ -352,7 +482,8 @@ class AgentClient(BaseClient):
352
482
  status_code=404,
353
483
  )
354
484
 
355
- return Agent(**data)._set_client(self)
485
+ response = AgentResponse(**data)
486
+ return Agent.from_response(response, client=self)
356
487
 
357
488
  def find_agents(self, name: str | None = None) -> list[Agent]:
358
489
  """Find agents by name."""
@@ -366,6 +497,11 @@ class AgentClient(BaseClient):
366
497
  # Renderer delegation helpers
367
498
  # ------------------------------------------------------------------ #
368
499
  def _get_renderer_manager(self) -> AgentRunRenderingManager:
500
+ """Get or create the renderer manager instance.
501
+
502
+ Returns:
503
+ AgentRunRenderingManager instance.
504
+ """
369
505
  manager = getattr(self, "_renderer_manager", None)
370
506
  if manager is None:
371
507
  manager = AgentRunRenderingManager(logger)
@@ -373,6 +509,15 @@ class AgentClient(BaseClient):
373
509
  return manager
374
510
 
375
511
  def _create_renderer(self, renderer: RichStreamRenderer | str | None, **kwargs: Any) -> RichStreamRenderer:
512
+ """Create or return a renderer instance.
513
+
514
+ Args:
515
+ renderer: Renderer instance, string identifier, or None.
516
+ **kwargs: Additional keyword arguments (e.g., verbose).
517
+
518
+ Returns:
519
+ RichStreamRenderer instance.
520
+ """
376
521
  manager = self._get_renderer_manager()
377
522
  verbose = kwargs.get("verbose", False)
378
523
  if isinstance(renderer, RichStreamRenderer) or hasattr(renderer, "on_start"):
@@ -386,7 +531,21 @@ class AgentClient(BaseClient):
386
531
  timeout_seconds: float,
387
532
  agent_name: str | None,
388
533
  meta: dict[str, Any],
534
+ hitl_handler: "RemoteHITLHandler | None" = None,
389
535
  ) -> tuple[str, dict[str, Any], float | None, float | None]:
536
+ """Process stream events from an HTTP response.
537
+
538
+ Args:
539
+ stream_response: HTTP response stream.
540
+ renderer: Renderer to use for displaying events.
541
+ timeout_seconds: Timeout in seconds.
542
+ agent_name: Optional agent name.
543
+ meta: Metadata dictionary.
544
+ hitl_handler: Optional HITL handler for approval callbacks.
545
+
546
+ Returns:
547
+ Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
548
+ """
390
549
  manager = self._get_renderer_manager()
391
550
  return manager.process_stream_events(
392
551
  stream_response,
@@ -394,6 +553,7 @@ class AgentClient(BaseClient):
394
553
  timeout_seconds,
395
554
  agent_name,
396
555
  meta,
556
+ hitl_handler=hitl_handler,
397
557
  )
398
558
 
399
559
  def _finalize_renderer(
@@ -404,8 +564,25 @@ class AgentClient(BaseClient):
404
564
  started_monotonic: float | None,
405
565
  finished_monotonic: float | None,
406
566
  ) -> str:
567
+ """Finalize the renderer and return the final response text.
568
+
569
+ Args:
570
+ renderer: Renderer to finalize.
571
+ final_text: Final text content.
572
+ stats_usage: Usage statistics dictionary.
573
+ started_monotonic: Start time (monotonic).
574
+ finished_monotonic: Finish time (monotonic).
575
+
576
+ Returns:
577
+ Final text string.
578
+ """
579
+ from glaip_sdk.client.run_rendering import ( # noqa: PLC0415
580
+ finalize_render_manager,
581
+ )
582
+
407
583
  manager = self._get_renderer_manager()
408
- return manager.finalize_renderer(
584
+ return finalize_render_manager(
585
+ manager,
409
586
  renderer,
410
587
  final_text,
411
588
  stats_usage,
@@ -414,20 +591,53 @@ class AgentClient(BaseClient):
414
591
  )
415
592
 
416
593
  def _get_tool_client(self) -> ToolClient:
594
+ """Get or create the tool client instance.
595
+
596
+ Returns:
597
+ ToolClient instance.
598
+ """
417
599
  if self._tool_client is None:
418
600
  self._tool_client = ToolClient(parent_client=self)
419
601
  return self._tool_client
420
602
 
421
603
  def _get_mcp_client(self) -> MCPClient:
604
+ """Get or create the MCP client instance.
605
+
606
+ Returns:
607
+ MCPClient instance.
608
+ """
422
609
  if self._mcp_client is None:
423
610
  self._mcp_client = MCPClient(parent_client=self)
424
611
  return self._mcp_client
425
612
 
613
+ @property
614
+ def schedules(self) -> "ScheduleClient":
615
+ """Get or create the schedule client instance.
616
+
617
+ Returns:
618
+ ScheduleClient instance.
619
+ """
620
+ if self._schedule_client is None:
621
+ # Import here to avoid circular import
622
+ from glaip_sdk.client.schedules import ScheduleClient # noqa: PLC0415
623
+
624
+ self._schedule_client = ScheduleClient(parent_client=self)
625
+ return self._schedule_client
626
+
426
627
  def _normalise_reference_entry(
427
628
  self,
428
629
  entry: Any,
429
630
  fallback_iter: Iterator[Any] | None,
430
631
  ) -> tuple[str | None, str | None]:
632
+ """Normalize a reference entry to extract ID and name.
633
+
634
+ Args:
635
+ entry: Reference entry (string, dict, or other).
636
+ fallback_iter: Optional iterator for fallback values.
637
+
638
+ Returns:
639
+ Tuple of (entry_id, entry_name).
640
+ """
431
641
  entry_id: str | None = None
432
642
  entry_name: str | None = None
433
643
 
@@ -464,6 +674,19 @@ class AgentClient(BaseClient):
464
674
  label: str,
465
675
  plural_label: str | None = None,
466
676
  ) -> list[str] | None:
677
+ """Resolve a list of resource references to IDs.
678
+
679
+ Args:
680
+ items: List of resource references to resolve.
681
+ references: Optional list of reference objects for fallback.
682
+ fetch_by_id: Function to fetch resource by ID.
683
+ find_by_name: Function to find resources by name.
684
+ label: Singular label for error messages.
685
+ plural_label: Plural label for error messages.
686
+
687
+ Returns:
688
+ List of resolved resource IDs, or None if items is empty.
689
+ """
467
690
  if not items:
468
691
  return None
469
692
 
@@ -495,6 +718,22 @@ class AgentClient(BaseClient):
495
718
  singular: str,
496
719
  plural: str,
497
720
  ) -> str:
721
+ """Resolve a single resource reference to an ID.
722
+
723
+ Args:
724
+ entry: Resource reference to resolve.
725
+ fallback_iter: Optional iterator for fallback values.
726
+ fetch_by_id: Function to fetch resource by ID.
727
+ find_by_name: Function to find resources by name.
728
+ singular: Singular label for error messages.
729
+ plural: Plural label for error messages.
730
+
731
+ Returns:
732
+ Resolved resource ID string.
733
+
734
+ Raises:
735
+ ValueError: If the resource cannot be resolved.
736
+ """
498
737
  entry_id, entry_name = self._normalise_reference_entry(entry, fallback_iter)
499
738
 
500
739
  validated_id = self._validate_resource_id(fetch_by_id, entry_id)
@@ -511,6 +750,14 @@ class AgentClient(BaseClient):
511
750
 
512
751
  @staticmethod
513
752
  def _coerce_reference_value(entry: Any) -> str:
753
+ """Coerce a reference entry to a string value.
754
+
755
+ Args:
756
+ entry: Reference entry (dict, string, or other).
757
+
758
+ Returns:
759
+ String representation of the reference.
760
+ """
514
761
  if isinstance(entry, dict):
515
762
  if entry.get("id"):
516
763
  return str(entry["id"])
@@ -520,6 +767,15 @@ class AgentClient(BaseClient):
520
767
 
521
768
  @staticmethod
522
769
  def _validate_resource_id(fetch_by_id: Callable[[str], Any], candidate_id: str | None) -> str | None:
770
+ """Validate a resource ID by attempting to fetch it.
771
+
772
+ Args:
773
+ fetch_by_id: Function to fetch resource by ID.
774
+ candidate_id: Candidate ID to validate.
775
+
776
+ Returns:
777
+ Validated ID if found, None otherwise.
778
+ """
523
779
  if not candidate_id:
524
780
  return None
525
781
  try:
@@ -535,6 +791,20 @@ class AgentClient(BaseClient):
535
791
  singular: str,
536
792
  plural: str,
537
793
  ) -> tuple[str, bool]:
794
+ """Resolve a resource by name to an ID.
795
+
796
+ Args:
797
+ find_by_name: Function to find resources by name.
798
+ entry_name: Name of the resource to find.
799
+ singular: Singular label for error messages.
800
+ plural: Plural label for error messages.
801
+
802
+ Returns:
803
+ Tuple of (resolved_id, success).
804
+
805
+ Raises:
806
+ ValueError: If resource not found or multiple matches exist.
807
+ """
538
808
  try:
539
809
  matches = find_by_name(entry_name)
540
810
  except Exception:
@@ -555,6 +825,15 @@ class AgentClient(BaseClient):
555
825
  tools: list[Any] | None,
556
826
  references: list[Any] | None = None,
557
827
  ) -> list[str] | None:
828
+ """Resolve tool references to IDs.
829
+
830
+ Args:
831
+ tools: List of tool references to resolve.
832
+ references: Optional list of reference objects for fallback.
833
+
834
+ Returns:
835
+ List of resolved tool IDs, or None if tools is empty.
836
+ """
558
837
  tool_client = self._get_tool_client()
559
838
  return self._resolve_resource_ids(
560
839
  tools,
@@ -570,6 +849,15 @@ class AgentClient(BaseClient):
570
849
  agents: list[Any] | None,
571
850
  references: list[Any] | None = None,
572
851
  ) -> list[str] | None:
852
+ """Resolve agent references to IDs.
853
+
854
+ Args:
855
+ agents: List of agent references to resolve.
856
+ references: Optional list of reference objects for fallback.
857
+
858
+ Returns:
859
+ List of resolved agent IDs, or None if agents is empty.
860
+ """
573
861
  return self._resolve_resource_ids(
574
862
  agents,
575
863
  references,
@@ -584,6 +872,15 @@ class AgentClient(BaseClient):
584
872
  mcps: list[Any] | None,
585
873
  references: list[Any] | None = None,
586
874
  ) -> list[str] | None:
875
+ """Resolve MCP references to IDs.
876
+
877
+ Args:
878
+ mcps: List of MCP references to resolve.
879
+ references: Optional list of reference objects for fallback.
880
+
881
+ Returns:
882
+ List of resolved MCP IDs, or None if mcps is empty.
883
+ """
587
884
  mcp_client = self._get_mcp_client()
588
885
  return self._resolve_resource_ids(
589
886
  mcps,
@@ -594,10 +891,18 @@ class AgentClient(BaseClient):
594
891
  plural_label="MCPs",
595
892
  )
596
893
 
597
- def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
598
- """Create an agent using a fully prepared payload mapping."""
599
- known, extras = _split_known_and_extra(payload, AgentCreateRequest.__dataclass_fields__)
894
+ def _validate_agent_basics(self, known: dict[str, Any]) -> tuple[str, str]:
895
+ """Validate and extract basic agent fields.
600
896
 
897
+ Args:
898
+ known: Known fields dictionary.
899
+
900
+ Returns:
901
+ Tuple of (name, validated_instruction).
902
+
903
+ Raises:
904
+ ValueError: If name or instruction is empty/whitespace.
905
+ """
601
906
  name = known.pop("name", None)
602
907
  instruction = known.pop("instruction", None)
603
908
  if not name or not str(name).strip():
@@ -606,9 +911,20 @@ class AgentClient(BaseClient):
606
911
  raise ValueError("Agent instruction cannot be empty or whitespace")
607
912
 
608
913
  validated_instruction = validate_agent_instruction(str(instruction))
609
- _normalise_sequence_fields(known)
914
+ return str(name).strip(), validated_instruction
610
915
 
611
- resolved_model = known.pop("model", None) or DEFAULT_MODEL
916
+ def _resolve_all_resources(
917
+ self, known: dict[str, Any], extras: dict[str, Any]
918
+ ) -> tuple[list[str] | None, list[str] | None, list[str] | None]:
919
+ """Resolve all resource IDs (tools, agents, mcps).
920
+
921
+ Args:
922
+ known: Known fields dictionary.
923
+ extras: Extra fields dictionary.
924
+
925
+ Returns:
926
+ Tuple of (resolved_tools, resolved_agents, resolved_mcps).
927
+ """
612
928
  tool_refs = extras.pop("_tool_refs", None)
613
929
  agent_refs = extras.pop("_agent_refs", None)
614
930
  mcp_refs = extras.pop("_mcp_refs", None)
@@ -621,10 +937,56 @@ class AgentClient(BaseClient):
621
937
  resolved_agents = self._resolve_agent_ids(agents_raw, agent_refs)
622
938
  resolved_mcps = self._resolve_mcp_ids(mcps_raw, mcp_refs)
623
939
 
940
+ return resolved_tools, resolved_agents, resolved_mcps
941
+
942
+ def _process_model_fields(
943
+ self, resolved_model: Any, known: dict[str, Any]
944
+ ) -> tuple[str, str | None, str | None, str | None]:
945
+ """Process model fields and extract language model ID.
946
+
947
+ Args:
948
+ resolved_model: Resolved model (string or Model object).
949
+ known: Known fields dictionary.
950
+
951
+ Returns:
952
+ Tuple of (resolved_model_str, language_model_id, provider, model_name).
953
+ """
954
+ from glaip_sdk.models import Model # noqa: PLC0415
955
+
956
+ if isinstance(resolved_model, Model):
957
+ if resolved_model.credentials or resolved_model.hyperparameters or resolved_model.base_url:
958
+ warnings.warn(
959
+ "Model object contains local configuration (credentials, hyperparameters, or base_url) "
960
+ "which is ignored for remote deployment. These fields are only used for local execution.",
961
+ UserWarning,
962
+ stacklevel=2,
963
+ )
964
+ resolved_model = resolved_model.id
965
+
966
+ # Validate and normalize string models (handles bare name deprecation)
967
+ if isinstance(resolved_model, str):
968
+ from glaip_sdk.models._validation import _validate_model # noqa: PLC0415
969
+
970
+ resolved_model = _validate_model(resolved_model)
971
+
624
972
  language_model_id = known.pop("language_model_id", None)
973
+ if not language_model_id and isinstance(resolved_model, str):
974
+ language_model_id = self._resolve_language_model_id(resolved_model)
975
+
625
976
  provider = known.pop("provider", None)
626
977
  model_name = known.pop("model_name", None)
627
978
 
979
+ return resolved_model, language_model_id, provider, model_name
980
+
981
+ def _extract_agent_metadata(self, known: dict[str, Any]) -> tuple[str, str, str]:
982
+ """Extract agent type, framework, and version.
983
+
984
+ Args:
985
+ known: Known fields dictionary.
986
+
987
+ Returns:
988
+ Tuple of (agent_type, framework, version).
989
+ """
628
990
  agent_type_value = known.pop("agent_type", None)
629
991
  fallback_type_value = known.pop("type", None)
630
992
  if agent_type_value is None:
@@ -632,6 +994,26 @@ class AgentClient(BaseClient):
632
994
 
633
995
  framework_value = known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK
634
996
  version_value = known.pop("version", None) or DEFAULT_AGENT_VERSION
997
+
998
+ return agent_type_value, framework_value, version_value
999
+
1000
+ def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
1001
+ """Create an agent using a fully prepared payload mapping."""
1002
+ known, extras = _split_known_and_extra(payload, AgentCreateRequest.__dataclass_fields__)
1003
+
1004
+ # Validate and extract basic fields
1005
+ name, validated_instruction = self._validate_agent_basics(known)
1006
+ _normalise_sequence_fields(known)
1007
+
1008
+ # Resolve model and resources
1009
+ resolved_model = known.pop("model", None) or DEFAULT_MODEL
1010
+ resolved_tools, resolved_agents, resolved_mcps = self._resolve_all_resources(known, extras)
1011
+
1012
+ # Process model and language model ID
1013
+ resolved_model, language_model_id, provider, model_name = self._process_model_fields(resolved_model, known)
1014
+
1015
+ # Extract agent type, framework, version
1016
+ agent_type_value, framework_value, version_value = self._extract_agent_metadata(known)
635
1017
  account_id = known.pop("account_id", None)
636
1018
  description = known.pop("description", None)
637
1019
  metadata = _prepare_agent_metadata(known.pop("metadata", None))
@@ -644,7 +1026,7 @@ class AgentClient(BaseClient):
644
1026
  final_extras.setdefault("model", resolved_model)
645
1027
 
646
1028
  request = AgentCreateRequest(
647
- name=str(name).strip(),
1029
+ name=name,
648
1030
  instruction=validated_instruction,
649
1031
  model=resolved_model,
650
1032
  language_model_id=language_model_id,
@@ -675,7 +1057,8 @@ class AgentClient(BaseClient):
675
1057
  get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
676
1058
  json=payload_dict,
677
1059
  )
678
- return Agent(**full_agent_data)._set_client(self)
1060
+ response = AgentResponse(**full_agent_data)
1061
+ return Agent.from_response(response, client=self)
679
1062
 
680
1063
  def create_agent(
681
1064
  self,
@@ -721,6 +1104,29 @@ class AgentClient(BaseClient):
721
1104
  """Backward-compatible helper to create an agent from a configuration file."""
722
1105
  return self.create_agent(file=file_path, **overrides)
723
1106
 
1107
+ def _resolve_update_model_fields(self, known: dict[str, Any]) -> None:
1108
+ """Resolve model fields in-place for update payload if present.
1109
+
1110
+ If 'model' or 'language_model_id' keys exist in the known fields dict,
1111
+ this method resolves them into 'language_model_id', 'provider', and 'model_name'
1112
+ using the standard resolution logic, ensuring consistency with create_agent.
1113
+
1114
+ Args:
1115
+ known: The dictionary of known fields to check and update in-place.
1116
+ """
1117
+ if "model" in known or "language_model_id" in known:
1118
+ model_val = known.pop("model", None)
1119
+ r_model, r_id, r_prov, r_name = self._process_model_fields(model_val, known)
1120
+
1121
+ if r_model is not None:
1122
+ known["model"] = r_model
1123
+ if r_id is not None:
1124
+ known["language_model_id"] = r_id
1125
+ if r_prov is not None:
1126
+ known["provider"] = r_prov
1127
+ if r_name is not None:
1128
+ known["model_name"] = r_name
1129
+
724
1130
  def _update_agent_from_payload(
725
1131
  self,
726
1132
  agent_id: str,
@@ -746,6 +1152,8 @@ class AgentClient(BaseClient):
746
1152
  if mcps_value is not None:
747
1153
  mcps_value = self._resolve_mcp_ids(mcps_value, mcp_refs) # pragma: no cover
748
1154
 
1155
+ self._resolve_update_model_fields(known)
1156
+
749
1157
  request = AgentUpdateRequest(
750
1158
  name=known.pop("name", None),
751
1159
  instruction=known.pop("instruction", None),
@@ -770,8 +1178,9 @@ class AgentClient(BaseClient):
770
1178
 
771
1179
  payload_dict = request.to_payload(current_agent)
772
1180
 
773
- response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
774
- return Agent(**response)._set_client(self)
1181
+ api_response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
1182
+ response = AgentResponse(**api_response)
1183
+ return Agent.from_response(response, client=self)
775
1184
 
776
1185
  def update_agent(
777
1186
  self,
@@ -818,6 +1227,62 @@ class AgentClient(BaseClient):
818
1227
  """Delete an agent."""
819
1228
  self._request("DELETE", f"/agents/{agent_id}")
820
1229
 
1230
+ def upsert_agent(self, identifier: str | Agent, **kwargs) -> Agent:
1231
+ """Create or update an agent by instance, ID, or name.
1232
+
1233
+ Args:
1234
+ identifier: Agent instance, ID (UUID string), or name
1235
+ **kwargs: Agent configuration (instruction, description, tools, etc.)
1236
+
1237
+ Returns:
1238
+ The created or updated agent.
1239
+
1240
+ Example:
1241
+ >>> # By name (creates if not exists)
1242
+ >>> agent = client.agents.upsert_agent(
1243
+ ... "hello_agent",
1244
+ ... instruction="You are a helpful assistant.",
1245
+ ... description="A friendly agent",
1246
+ ... )
1247
+ >>> # By instance
1248
+ >>> agent = client.agents.upsert_agent(existing_agent, description="Updated")
1249
+ >>> # By ID
1250
+ >>> agent = client.agents.upsert_agent("uuid-here", description="Updated")
1251
+ """
1252
+ # Handle Agent instance
1253
+ if isinstance(identifier, Agent):
1254
+ if identifier.id:
1255
+ logger.info("Updating agent by instance: %s", identifier.name)
1256
+ return self.update_agent(identifier.id, name=identifier.name, **kwargs)
1257
+ identifier = identifier.name
1258
+
1259
+ # Handle string (ID or name)
1260
+ if isinstance(identifier, str):
1261
+ # Check if it's a UUID
1262
+ if is_uuid(identifier):
1263
+ logger.info("Updating agent by ID: %s", identifier)
1264
+ return self.update_agent(identifier, **kwargs)
1265
+
1266
+ # It's a name - find or create
1267
+ return self._upsert_agent_by_name(identifier, **kwargs)
1268
+
1269
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
1270
+
1271
+ def _upsert_agent_by_name(self, name: str, **kwargs) -> Agent:
1272
+ """Find agent by name and update, or create if not found."""
1273
+ existing = self.find_agents(name)
1274
+
1275
+ if len(existing) == 1:
1276
+ logger.info("Updating existing agent: %s", name)
1277
+ return self.update_agent(existing[0].id, name=name, **kwargs)
1278
+
1279
+ if len(existing) > 1:
1280
+ raise ValueError(f"Multiple agents found with name '{name}'")
1281
+
1282
+ # Create new agent
1283
+ logger.info("Creating new agent: %s", name)
1284
+ return self.create_agent(name=name, **kwargs)
1285
+
821
1286
  def _prepare_sync_request_data(
822
1287
  self,
823
1288
  message: str,
@@ -882,9 +1347,35 @@ class AgentClient(BaseClient):
882
1347
  tty: bool = False,
883
1348
  *,
884
1349
  renderer: RichStreamRenderer | str | None = "auto",
1350
+ runtime_config: dict[str, Any] | None = None,
1351
+ hitl_handler: "RemoteHITLHandler | None" = None,
885
1352
  **kwargs,
886
1353
  ) -> str:
887
- """Run an agent with a message, streaming via a renderer."""
1354
+ """Run an agent with a message, streaming via a renderer.
1355
+
1356
+ Args:
1357
+ agent_id: The ID of the agent to run.
1358
+ message: The message to send to the agent.
1359
+ files: Optional list of files to include with the request.
1360
+ tty: Whether to enable TTY mode.
1361
+ renderer: Renderer for streaming output.
1362
+ runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1363
+ Keys should be platform IDs. Example:
1364
+ {
1365
+ "tool_configs": {"tool-id": {"param": "value"}},
1366
+ "mcp_configs": {"mcp-id": {"setting": "on"}},
1367
+ "agent_config": {"planning": True},
1368
+ }
1369
+ hitl_handler: Optional RemoteHITLHandler for approval callbacks.
1370
+ Set GLAIP_HITL_AUTO_APPROVE=true for auto-approval without handler.
1371
+ **kwargs: Additional arguments to pass to the run API.
1372
+
1373
+ Returns:
1374
+ The agent's response as a string.
1375
+ """
1376
+ # Include runtime_config in kwargs only when caller hasn't already provided it
1377
+ if runtime_config is not None and "runtime_config" not in kwargs:
1378
+ kwargs["runtime_config"] = runtime_config
888
1379
  (
889
1380
  payload,
890
1381
  data_payload,
@@ -933,6 +1424,7 @@ class AgentClient(BaseClient):
933
1424
  timeout_seconds,
934
1425
  agent_name,
935
1426
  meta,
1427
+ hitl_handler=hitl_handler,
936
1428
  )
937
1429
 
938
1430
  except KeyboardInterrupt:
@@ -949,6 +1441,13 @@ class AgentClient(BaseClient):
949
1441
  if multipart_data:
950
1442
  multipart_data.close()
951
1443
 
1444
+ # Wait for pending HITL decisions before returning
1445
+ if hitl_handler and hasattr(hitl_handler, "wait_for_pending_decisions"):
1446
+ try:
1447
+ hitl_handler.wait_for_pending_decisions(timeout=30)
1448
+ except Exception as e:
1449
+ logger.warning(f"Error waiting for HITL decisions: {e}")
1450
+
952
1451
  return self._finalize_renderer(
953
1452
  r,
954
1453
  final_text,
@@ -1028,7 +1527,9 @@ class AgentClient(BaseClient):
1028
1527
  message: str,
1029
1528
  files: list[str | BinaryIO] | None = None,
1030
1529
  *,
1031
- timeout: float | None = None,
1530
+ request_timeout: float | None = None,
1531
+ runtime_config: dict[str, Any] | None = None,
1532
+ hitl_handler: "RemoteHITLHandler | None" = None,
1032
1533
  **kwargs,
1033
1534
  ) -> AsyncGenerator[dict, None]:
1034
1535
  """Async run an agent with a message, yielding streaming JSON chunks.
@@ -1037,29 +1538,53 @@ class AgentClient(BaseClient):
1037
1538
  agent_id: ID of the agent to run
1038
1539
  message: Message to send to the agent
1039
1540
  files: Optional list of files to include
1040
- timeout: Request timeout in seconds
1541
+ request_timeout: Optional request timeout in seconds (defaults to client timeout)
1542
+ runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1543
+ Keys should be platform IDs. Example:
1544
+ {
1545
+ "tool_configs": {"tool-id": {"param": "value"}},
1546
+ "mcp_configs": {"mcp-id": {"setting": "on"}},
1547
+ "agent_config": {"planning": True},
1548
+ }
1549
+ hitl_handler: Optional HITL handler for remote approval requests.
1550
+ Note: Async HITL support is currently deferred. This parameter
1551
+ is accepted for API consistency but will raise NotImplementedError
1552
+ if provided.
1041
1553
  **kwargs: Additional arguments (chat_history, pii_mapping, etc.)
1042
1554
 
1043
1555
  Yields:
1044
1556
  Dictionary containing parsed JSON chunks from the streaming response
1045
1557
 
1046
1558
  Raises:
1559
+ NotImplementedError: If hitl_handler is provided (async HITL not yet supported)
1047
1560
  AgentTimeoutError: When agent execution times out
1048
1561
  httpx.TimeoutException: When general timeout occurs
1049
1562
  Exception: For other unexpected errors
1050
1563
  """
1564
+ if hitl_handler is not None:
1565
+ raise NotImplementedError(
1566
+ "Async HITL support is currently deferred. "
1567
+ "Please use the synchronous run_agent() method with hitl_handler."
1568
+ )
1569
+ # Include runtime_config in kwargs only when caller hasn't already provided it
1570
+ if runtime_config is not None and "runtime_config" not in kwargs:
1571
+ kwargs["runtime_config"] = runtime_config
1572
+ # Derive timeout values for request/control flow
1573
+ legacy_timeout = kwargs.get("timeout")
1574
+ http_timeout_override = request_timeout if request_timeout is not None else legacy_timeout
1575
+ http_timeout = http_timeout_override or self.timeout
1576
+
1051
1577
  # Prepare request data
1052
1578
  payload, data_payload, files_payload, headers = self._prepare_request_data(message, files, **kwargs)
1053
1579
 
1054
1580
  # Create async client configuration
1055
- async_client_config = self._create_async_client_config(timeout, headers)
1581
+ async_client_config = self._create_async_client_config(http_timeout_override, headers)
1056
1582
 
1057
1583
  # Get execution timeout for streaming control
1058
1584
  timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
1059
1585
  agent_name = kwargs.get("agent_name")
1060
1586
 
1061
- try:
1062
- # Create async client and stream response
1587
+ async def _chunk_stream() -> AsyncGenerator[dict, None]:
1063
1588
  async with httpx.AsyncClient(**async_client_config) as async_client:
1064
1589
  async for chunk in self._stream_agent_response(
1065
1590
  async_client,
@@ -1073,7 +1598,14 @@ class AgentClient(BaseClient):
1073
1598
  ):
1074
1599
  yield chunk
1075
1600
 
1076
- finally:
1077
- # Ensure cleanup - this is handled by the calling context
1078
- # but we keep this for safety in case of future changes
1079
- pass
1601
+ async with _async_timeout_guard(http_timeout):
1602
+ async for chunk in _chunk_stream():
1603
+ yield chunk
1604
+
1605
+ @property
1606
+ def runs(self) -> "AgentRunsClient":
1607
+ """Get the agent runs client."""
1608
+ if self._runs_client is None:
1609
+ shared_config = build_shared_config(self)
1610
+ self._runs_client = AgentRunsClient(**shared_config)
1611
+ return self._runs_client