glaip-sdk 0.1.3__py3-none-any.whl → 0.6.19__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 (146) 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 +1196 -0
  5. glaip_sdk/branding.py +13 -0
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/auth.py +254 -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.py +213 -73
  11. glaip_sdk/cli/commands/common_config.py +104 -0
  12. glaip_sdk/cli/commands/configure.py +729 -113
  13. glaip_sdk/cli/commands/mcps.py +241 -72
  14. glaip_sdk/cli/commands/models.py +11 -5
  15. glaip_sdk/cli/commands/tools.py +49 -57
  16. glaip_sdk/cli/commands/transcripts.py +755 -0
  17. glaip_sdk/cli/config.py +48 -4
  18. glaip_sdk/cli/constants.py +38 -0
  19. glaip_sdk/cli/context.py +8 -0
  20. glaip_sdk/cli/core/__init__.py +79 -0
  21. glaip_sdk/cli/core/context.py +124 -0
  22. glaip_sdk/cli/core/output.py +851 -0
  23. glaip_sdk/cli/core/prompting.py +649 -0
  24. glaip_sdk/cli/core/rendering.py +187 -0
  25. glaip_sdk/cli/display.py +35 -19
  26. glaip_sdk/cli/hints.py +57 -0
  27. glaip_sdk/cli/io.py +6 -3
  28. glaip_sdk/cli/main.py +241 -121
  29. glaip_sdk/cli/masking.py +21 -33
  30. glaip_sdk/cli/pager.py +9 -10
  31. glaip_sdk/cli/parsers/__init__.py +1 -3
  32. glaip_sdk/cli/slash/__init__.py +0 -9
  33. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  34. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  35. glaip_sdk/cli/slash/agent_session.py +62 -21
  36. glaip_sdk/cli/slash/prompt.py +21 -0
  37. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  38. glaip_sdk/cli/slash/session.py +771 -140
  39. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  40. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  41. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  42. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  43. glaip_sdk/cli/slash/tui/loading.py +58 -0
  44. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  45. glaip_sdk/cli/transcript/__init__.py +12 -52
  46. glaip_sdk/cli/transcript/cache.py +255 -44
  47. glaip_sdk/cli/transcript/capture.py +27 -1
  48. glaip_sdk/cli/transcript/history.py +815 -0
  49. glaip_sdk/cli/transcript/viewer.py +72 -499
  50. glaip_sdk/cli/update_notifier.py +14 -5
  51. glaip_sdk/cli/utils.py +243 -1252
  52. glaip_sdk/cli/validators.py +5 -6
  53. glaip_sdk/client/__init__.py +2 -1
  54. glaip_sdk/client/_agent_payloads.py +45 -9
  55. glaip_sdk/client/agent_runs.py +147 -0
  56. glaip_sdk/client/agents.py +291 -35
  57. glaip_sdk/client/base.py +1 -0
  58. glaip_sdk/client/main.py +19 -10
  59. glaip_sdk/client/mcps.py +122 -12
  60. glaip_sdk/client/run_rendering.py +466 -89
  61. glaip_sdk/client/shared.py +21 -0
  62. glaip_sdk/client/tools.py +155 -10
  63. glaip_sdk/config/constants.py +11 -0
  64. glaip_sdk/hitl/__init__.py +15 -0
  65. glaip_sdk/hitl/local.py +151 -0
  66. glaip_sdk/mcps/__init__.py +21 -0
  67. glaip_sdk/mcps/base.py +345 -0
  68. glaip_sdk/models/__init__.py +90 -0
  69. glaip_sdk/models/agent.py +47 -0
  70. glaip_sdk/models/agent_runs.py +116 -0
  71. glaip_sdk/models/common.py +42 -0
  72. glaip_sdk/models/mcp.py +33 -0
  73. glaip_sdk/models/tool.py +33 -0
  74. glaip_sdk/payload_schemas/__init__.py +1 -13
  75. glaip_sdk/registry/__init__.py +55 -0
  76. glaip_sdk/registry/agent.py +164 -0
  77. glaip_sdk/registry/base.py +139 -0
  78. glaip_sdk/registry/mcp.py +253 -0
  79. glaip_sdk/registry/tool.py +232 -0
  80. glaip_sdk/rich_components.py +58 -2
  81. glaip_sdk/runner/__init__.py +59 -0
  82. glaip_sdk/runner/base.py +84 -0
  83. glaip_sdk/runner/deps.py +112 -0
  84. glaip_sdk/runner/langgraph.py +870 -0
  85. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  86. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  87. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  88. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  89. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  90. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  91. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  92. glaip_sdk/tools/__init__.py +22 -0
  93. glaip_sdk/tools/base.py +435 -0
  94. glaip_sdk/utils/__init__.py +58 -12
  95. glaip_sdk/utils/a2a/__init__.py +34 -0
  96. glaip_sdk/utils/a2a/event_processor.py +188 -0
  97. glaip_sdk/utils/bundler.py +267 -0
  98. glaip_sdk/utils/client.py +111 -0
  99. glaip_sdk/utils/client_utils.py +39 -7
  100. glaip_sdk/utils/datetime_helpers.py +58 -0
  101. glaip_sdk/utils/discovery.py +78 -0
  102. glaip_sdk/utils/display.py +23 -15
  103. glaip_sdk/utils/export.py +143 -0
  104. glaip_sdk/utils/general.py +0 -33
  105. glaip_sdk/utils/import_export.py +12 -7
  106. glaip_sdk/utils/import_resolver.py +492 -0
  107. glaip_sdk/utils/instructions.py +101 -0
  108. glaip_sdk/utils/rendering/__init__.py +115 -1
  109. glaip_sdk/utils/rendering/formatting.py +5 -30
  110. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  111. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  112. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  113. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  114. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  115. glaip_sdk/utils/rendering/models.py +1 -0
  116. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  117. glaip_sdk/utils/rendering/renderer/base.py +275 -1476
  118. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  119. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  120. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  121. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  122. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  123. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  124. glaip_sdk/utils/rendering/state.py +204 -0
  125. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  126. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  127. glaip_sdk/utils/rendering/steps/format.py +176 -0
  128. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  129. glaip_sdk/utils/rendering/timing.py +36 -0
  130. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  131. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  132. glaip_sdk/utils/resource_refs.py +25 -13
  133. glaip_sdk/utils/runtime_config.py +425 -0
  134. glaip_sdk/utils/serialization.py +18 -0
  135. glaip_sdk/utils/sync.py +142 -0
  136. glaip_sdk/utils/tool_detection.py +33 -0
  137. glaip_sdk/utils/tool_storage_provider.py +140 -0
  138. glaip_sdk/utils/validation.py +16 -24
  139. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/METADATA +56 -21
  140. glaip_sdk-0.6.19.dist-info/RECORD +163 -0
  141. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.19.dist-info}/WHEEL +2 -1
  142. glaip_sdk-0.6.19.dist-info/entry_points.txt +2 -0
  143. glaip_sdk-0.6.19.dist-info/top_level.txt +1 -0
  144. glaip_sdk/models.py +0 -240
  145. glaip_sdk-0.1.3.dist-info/RECORD +0 -83
  146. glaip_sdk-0.1.3.dist-info/entry_points.txt +0 -3
@@ -3,31 +3,37 @@
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
16
  from typing import Any, BinaryIO
14
17
 
15
18
  import httpx
16
-
19
+ from glaip_sdk.agents import Agent
17
20
  from glaip_sdk.client._agent_payloads import (
18
21
  AgentCreateRequest,
19
22
  AgentListParams,
20
23
  AgentListResult,
21
24
  AgentUpdateRequest,
22
25
  )
26
+ from glaip_sdk.client.agent_runs import AgentRunsClient
23
27
  from glaip_sdk.client.base import BaseClient
24
28
  from glaip_sdk.client.mcps import MCPClient
25
29
  from glaip_sdk.client.run_rendering import (
26
30
  AgentRunRenderingManager,
27
31
  compute_timeout_seconds,
28
32
  )
33
+ from glaip_sdk.client.shared import build_shared_config
29
34
  from glaip_sdk.client.tools import ToolClient
30
35
  from glaip_sdk.config.constants import (
36
+ AGENT_CONFIG_FIELDS,
31
37
  DEFAULT_AGENT_FRAMEWORK,
32
38
  DEFAULT_AGENT_RUN_TIMEOUT,
33
39
  DEFAULT_AGENT_TYPE,
@@ -35,7 +41,7 @@ from glaip_sdk.config.constants import (
35
41
  DEFAULT_MODEL,
36
42
  )
37
43
  from glaip_sdk.exceptions import NotFoundError, ValidationError
38
- from glaip_sdk.models import Agent
44
+ from glaip_sdk.models import AgentResponse
39
45
  from glaip_sdk.payload_schemas.agent import list_server_only_fields
40
46
  from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
41
47
  from glaip_sdk.utils.client_utils import (
@@ -67,6 +73,21 @@ _MERGED_SEQUENCE_FIELDS = ("tools", "agents", "mcps")
67
73
  _DEFAULT_METADATA_TYPE = "custom"
68
74
 
69
75
 
76
+ @asynccontextmanager
77
+ async def _async_timeout_guard(
78
+ timeout_seconds: float | None,
79
+ ) -> AsyncGenerator[None, None]:
80
+ """Apply an asyncio timeout when a custom timeout is provided."""
81
+ if timeout_seconds is None:
82
+ yield
83
+ return
84
+ try:
85
+ async with asyncio.timeout(timeout_seconds):
86
+ yield
87
+ except asyncio.TimeoutError as exc:
88
+ raise httpx.TimeoutException(f"Request timed out after {timeout_seconds}s") from exc
89
+
90
+
70
91
  def _normalise_sequence(value: Any) -> list[Any] | None:
71
92
  """Normalise optional sequence inputs to plain lists."""
72
93
  if value is None:
@@ -193,19 +214,7 @@ def _extract_original_refs(raw_definition: dict) -> dict[str, list]:
193
214
 
194
215
  def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
195
216
  """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
- }
217
+ cli_args = {key: overrides_dict.get(key) for key in AGENT_CONFIG_FIELDS if overrides_dict.get(key) is not None}
209
218
 
210
219
  # Normalize sequence fields
211
220
  for field in _MERGED_SEQUENCE_FIELDS:
@@ -254,6 +263,7 @@ class AgentClient(BaseClient):
254
263
  self._renderer_manager = AgentRunRenderingManager(logger)
255
264
  self._tool_client: ToolClient | None = None
256
265
  self._mcp_client: MCPClient | None = None
266
+ self._runs_client: AgentRunsClient | None = None
257
267
 
258
268
  def list_agents(
259
269
  self,
@@ -352,7 +362,8 @@ class AgentClient(BaseClient):
352
362
  status_code=404,
353
363
  )
354
364
 
355
- return Agent(**data)._set_client(self)
365
+ response = AgentResponse(**data)
366
+ return Agent.from_response(response, client=self)
356
367
 
357
368
  def find_agents(self, name: str | None = None) -> list[Agent]:
358
369
  """Find agents by name."""
@@ -366,6 +377,11 @@ class AgentClient(BaseClient):
366
377
  # Renderer delegation helpers
367
378
  # ------------------------------------------------------------------ #
368
379
  def _get_renderer_manager(self) -> AgentRunRenderingManager:
380
+ """Get or create the renderer manager instance.
381
+
382
+ Returns:
383
+ AgentRunRenderingManager instance.
384
+ """
369
385
  manager = getattr(self, "_renderer_manager", None)
370
386
  if manager is None:
371
387
  manager = AgentRunRenderingManager(logger)
@@ -373,6 +389,15 @@ class AgentClient(BaseClient):
373
389
  return manager
374
390
 
375
391
  def _create_renderer(self, renderer: RichStreamRenderer | str | None, **kwargs: Any) -> RichStreamRenderer:
392
+ """Create or return a renderer instance.
393
+
394
+ Args:
395
+ renderer: Renderer instance, string identifier, or None.
396
+ **kwargs: Additional keyword arguments (e.g., verbose).
397
+
398
+ Returns:
399
+ RichStreamRenderer instance.
400
+ """
376
401
  manager = self._get_renderer_manager()
377
402
  verbose = kwargs.get("verbose", False)
378
403
  if isinstance(renderer, RichStreamRenderer) or hasattr(renderer, "on_start"):
@@ -387,6 +412,18 @@ class AgentClient(BaseClient):
387
412
  agent_name: str | None,
388
413
  meta: dict[str, Any],
389
414
  ) -> tuple[str, dict[str, Any], float | None, float | None]:
415
+ """Process stream events from an HTTP response.
416
+
417
+ Args:
418
+ stream_response: HTTP response stream.
419
+ renderer: Renderer to use for displaying events.
420
+ timeout_seconds: Timeout in seconds.
421
+ agent_name: Optional agent name.
422
+ meta: Metadata dictionary.
423
+
424
+ Returns:
425
+ Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
426
+ """
390
427
  manager = self._get_renderer_manager()
391
428
  return manager.process_stream_events(
392
429
  stream_response,
@@ -404,21 +441,41 @@ class AgentClient(BaseClient):
404
441
  started_monotonic: float | None,
405
442
  finished_monotonic: float | None,
406
443
  ) -> str:
444
+ """Finalize the renderer and return the final response text.
445
+
446
+ Args:
447
+ renderer: Renderer to finalize.
448
+ final_text: Final text content.
449
+ stats_usage: Usage statistics dictionary.
450
+ started_monotonic: Start time (monotonic).
451
+ finished_monotonic: Finish time (monotonic).
452
+
453
+ Returns:
454
+ Final text string.
455
+ """
456
+ from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
457
+
407
458
  manager = self._get_renderer_manager()
408
- return manager.finalize_renderer(
409
- renderer,
410
- final_text,
411
- stats_usage,
412
- started_monotonic,
413
- finished_monotonic,
459
+ return finalize_render_manager(
460
+ manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
414
461
  )
415
462
 
416
463
  def _get_tool_client(self) -> ToolClient:
464
+ """Get or create the tool client instance.
465
+
466
+ Returns:
467
+ ToolClient instance.
468
+ """
417
469
  if self._tool_client is None:
418
470
  self._tool_client = ToolClient(parent_client=self)
419
471
  return self._tool_client
420
472
 
421
473
  def _get_mcp_client(self) -> MCPClient:
474
+ """Get or create the MCP client instance.
475
+
476
+ Returns:
477
+ MCPClient instance.
478
+ """
422
479
  if self._mcp_client is None:
423
480
  self._mcp_client = MCPClient(parent_client=self)
424
481
  return self._mcp_client
@@ -428,6 +485,15 @@ class AgentClient(BaseClient):
428
485
  entry: Any,
429
486
  fallback_iter: Iterator[Any] | None,
430
487
  ) -> tuple[str | None, str | None]:
488
+ """Normalize a reference entry to extract ID and name.
489
+
490
+ Args:
491
+ entry: Reference entry (string, dict, or other).
492
+ fallback_iter: Optional iterator for fallback values.
493
+
494
+ Returns:
495
+ Tuple of (entry_id, entry_name).
496
+ """
431
497
  entry_id: str | None = None
432
498
  entry_name: str | None = None
433
499
 
@@ -464,6 +530,19 @@ class AgentClient(BaseClient):
464
530
  label: str,
465
531
  plural_label: str | None = None,
466
532
  ) -> list[str] | None:
533
+ """Resolve a list of resource references to IDs.
534
+
535
+ Args:
536
+ items: List of resource references to resolve.
537
+ references: Optional list of reference objects for fallback.
538
+ fetch_by_id: Function to fetch resource by ID.
539
+ find_by_name: Function to find resources by name.
540
+ label: Singular label for error messages.
541
+ plural_label: Plural label for error messages.
542
+
543
+ Returns:
544
+ List of resolved resource IDs, or None if items is empty.
545
+ """
467
546
  if not items:
468
547
  return None
469
548
 
@@ -495,6 +574,22 @@ class AgentClient(BaseClient):
495
574
  singular: str,
496
575
  plural: str,
497
576
  ) -> str:
577
+ """Resolve a single resource reference to an ID.
578
+
579
+ Args:
580
+ entry: Resource reference to resolve.
581
+ fallback_iter: Optional iterator for fallback values.
582
+ fetch_by_id: Function to fetch resource by ID.
583
+ find_by_name: Function to find resources by name.
584
+ singular: Singular label for error messages.
585
+ plural: Plural label for error messages.
586
+
587
+ Returns:
588
+ Resolved resource ID string.
589
+
590
+ Raises:
591
+ ValueError: If the resource cannot be resolved.
592
+ """
498
593
  entry_id, entry_name = self._normalise_reference_entry(entry, fallback_iter)
499
594
 
500
595
  validated_id = self._validate_resource_id(fetch_by_id, entry_id)
@@ -511,6 +606,14 @@ class AgentClient(BaseClient):
511
606
 
512
607
  @staticmethod
513
608
  def _coerce_reference_value(entry: Any) -> str:
609
+ """Coerce a reference entry to a string value.
610
+
611
+ Args:
612
+ entry: Reference entry (dict, string, or other).
613
+
614
+ Returns:
615
+ String representation of the reference.
616
+ """
514
617
  if isinstance(entry, dict):
515
618
  if entry.get("id"):
516
619
  return str(entry["id"])
@@ -520,6 +623,15 @@ class AgentClient(BaseClient):
520
623
 
521
624
  @staticmethod
522
625
  def _validate_resource_id(fetch_by_id: Callable[[str], Any], candidate_id: str | None) -> str | None:
626
+ """Validate a resource ID by attempting to fetch it.
627
+
628
+ Args:
629
+ fetch_by_id: Function to fetch resource by ID.
630
+ candidate_id: Candidate ID to validate.
631
+
632
+ Returns:
633
+ Validated ID if found, None otherwise.
634
+ """
523
635
  if not candidate_id:
524
636
  return None
525
637
  try:
@@ -535,6 +647,20 @@ class AgentClient(BaseClient):
535
647
  singular: str,
536
648
  plural: str,
537
649
  ) -> tuple[str, bool]:
650
+ """Resolve a resource by name to an ID.
651
+
652
+ Args:
653
+ find_by_name: Function to find resources by name.
654
+ entry_name: Name of the resource to find.
655
+ singular: Singular label for error messages.
656
+ plural: Plural label for error messages.
657
+
658
+ Returns:
659
+ Tuple of (resolved_id, success).
660
+
661
+ Raises:
662
+ ValueError: If resource not found or multiple matches exist.
663
+ """
538
664
  try:
539
665
  matches = find_by_name(entry_name)
540
666
  except Exception:
@@ -555,6 +681,15 @@ class AgentClient(BaseClient):
555
681
  tools: list[Any] | None,
556
682
  references: list[Any] | None = None,
557
683
  ) -> list[str] | None:
684
+ """Resolve tool references to IDs.
685
+
686
+ Args:
687
+ tools: List of tool references to resolve.
688
+ references: Optional list of reference objects for fallback.
689
+
690
+ Returns:
691
+ List of resolved tool IDs, or None if tools is empty.
692
+ """
558
693
  tool_client = self._get_tool_client()
559
694
  return self._resolve_resource_ids(
560
695
  tools,
@@ -570,6 +705,15 @@ class AgentClient(BaseClient):
570
705
  agents: list[Any] | None,
571
706
  references: list[Any] | None = None,
572
707
  ) -> list[str] | None:
708
+ """Resolve agent references to IDs.
709
+
710
+ Args:
711
+ agents: List of agent references to resolve.
712
+ references: Optional list of reference objects for fallback.
713
+
714
+ Returns:
715
+ List of resolved agent IDs, or None if agents is empty.
716
+ """
573
717
  return self._resolve_resource_ids(
574
718
  agents,
575
719
  references,
@@ -584,6 +728,15 @@ class AgentClient(BaseClient):
584
728
  mcps: list[Any] | None,
585
729
  references: list[Any] | None = None,
586
730
  ) -> list[str] | None:
731
+ """Resolve MCP references to IDs.
732
+
733
+ Args:
734
+ mcps: List of MCP references to resolve.
735
+ references: Optional list of reference objects for fallback.
736
+
737
+ Returns:
738
+ List of resolved MCP IDs, or None if mcps is empty.
739
+ """
587
740
  mcp_client = self._get_mcp_client()
588
741
  return self._resolve_resource_ids(
589
742
  mcps,
@@ -675,7 +828,8 @@ class AgentClient(BaseClient):
675
828
  get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
676
829
  json=payload_dict,
677
830
  )
678
- return Agent(**full_agent_data)._set_client(self)
831
+ response = AgentResponse(**full_agent_data)
832
+ return Agent.from_response(response, client=self)
679
833
 
680
834
  def create_agent(
681
835
  self,
@@ -770,8 +924,9 @@ class AgentClient(BaseClient):
770
924
 
771
925
  payload_dict = request.to_payload(current_agent)
772
926
 
773
- response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
774
- return Agent(**response)._set_client(self)
927
+ api_response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
928
+ response = AgentResponse(**api_response)
929
+ return Agent.from_response(response, client=self)
775
930
 
776
931
  def update_agent(
777
932
  self,
@@ -818,6 +973,62 @@ class AgentClient(BaseClient):
818
973
  """Delete an agent."""
819
974
  self._request("DELETE", f"/agents/{agent_id}")
820
975
 
976
+ def upsert_agent(self, identifier: str | Agent, **kwargs) -> Agent:
977
+ """Create or update an agent by instance, ID, or name.
978
+
979
+ Args:
980
+ identifier: Agent instance, ID (UUID string), or name
981
+ **kwargs: Agent configuration (instruction, description, tools, etc.)
982
+
983
+ Returns:
984
+ The created or updated agent.
985
+
986
+ Example:
987
+ >>> # By name (creates if not exists)
988
+ >>> agent = client.agents.upsert_agent(
989
+ ... "hello_agent",
990
+ ... instruction="You are a helpful assistant.",
991
+ ... description="A friendly agent",
992
+ ... )
993
+ >>> # By instance
994
+ >>> agent = client.agents.upsert_agent(existing_agent, description="Updated")
995
+ >>> # By ID
996
+ >>> agent = client.agents.upsert_agent("uuid-here", description="Updated")
997
+ """
998
+ # Handle Agent instance
999
+ if isinstance(identifier, Agent):
1000
+ if identifier.id:
1001
+ logger.info("Updating agent by instance: %s", identifier.name)
1002
+ return self.update_agent(identifier.id, name=identifier.name, **kwargs)
1003
+ identifier = identifier.name
1004
+
1005
+ # Handle string (ID or name)
1006
+ if isinstance(identifier, str):
1007
+ # Check if it's a UUID
1008
+ if is_uuid(identifier):
1009
+ logger.info("Updating agent by ID: %s", identifier)
1010
+ return self.update_agent(identifier, **kwargs)
1011
+
1012
+ # It's a name - find or create
1013
+ return self._upsert_agent_by_name(identifier, **kwargs)
1014
+
1015
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
1016
+
1017
+ def _upsert_agent_by_name(self, name: str, **kwargs) -> Agent:
1018
+ """Find agent by name and update, or create if not found."""
1019
+ existing = self.find_agents(name)
1020
+
1021
+ if len(existing) == 1:
1022
+ logger.info("Updating existing agent: %s", name)
1023
+ return self.update_agent(existing[0].id, name=name, **kwargs)
1024
+
1025
+ if len(existing) > 1:
1026
+ raise ValueError(f"Multiple agents found with name '{name}'")
1027
+
1028
+ # Create new agent
1029
+ logger.info("Creating new agent: %s", name)
1030
+ return self.create_agent(name=name, **kwargs)
1031
+
821
1032
  def _prepare_sync_request_data(
822
1033
  self,
823
1034
  message: str,
@@ -882,9 +1093,32 @@ class AgentClient(BaseClient):
882
1093
  tty: bool = False,
883
1094
  *,
884
1095
  renderer: RichStreamRenderer | str | None = "auto",
1096
+ runtime_config: dict[str, Any] | None = None,
885
1097
  **kwargs,
886
1098
  ) -> str:
887
- """Run an agent with a message, streaming via a renderer."""
1099
+ """Run an agent with a message, streaming via a renderer.
1100
+
1101
+ Args:
1102
+ agent_id: The ID of the agent to run.
1103
+ message: The message to send to the agent.
1104
+ files: Optional list of files to include with the request.
1105
+ tty: Whether to enable TTY mode.
1106
+ renderer: Renderer for streaming output.
1107
+ runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1108
+ Keys should be platform IDs. Example:
1109
+ {
1110
+ "tool_configs": {"tool-id": {"param": "value"}},
1111
+ "mcp_configs": {"mcp-id": {"setting": "on"}},
1112
+ "agent_config": {"planning": True},
1113
+ }
1114
+ **kwargs: Additional arguments to pass to the run API.
1115
+
1116
+ Returns:
1117
+ The agent's response as a string.
1118
+ """
1119
+ # Include runtime_config in kwargs only when caller hasn't already provided it
1120
+ if runtime_config is not None and "runtime_config" not in kwargs:
1121
+ kwargs["runtime_config"] = runtime_config
888
1122
  (
889
1123
  payload,
890
1124
  data_payload,
@@ -1028,7 +1262,8 @@ class AgentClient(BaseClient):
1028
1262
  message: str,
1029
1263
  files: list[str | BinaryIO] | None = None,
1030
1264
  *,
1031
- timeout: float | None = None,
1265
+ request_timeout: float | None = None,
1266
+ runtime_config: dict[str, Any] | None = None,
1032
1267
  **kwargs,
1033
1268
  ) -> AsyncGenerator[dict, None]:
1034
1269
  """Async run an agent with a message, yielding streaming JSON chunks.
@@ -1037,7 +1272,14 @@ class AgentClient(BaseClient):
1037
1272
  agent_id: ID of the agent to run
1038
1273
  message: Message to send to the agent
1039
1274
  files: Optional list of files to include
1040
- timeout: Request timeout in seconds
1275
+ request_timeout: Optional request timeout in seconds (defaults to client timeout)
1276
+ runtime_config: Optional runtime configuration for tools, MCPs, and agents.
1277
+ Keys should be platform IDs. Example:
1278
+ {
1279
+ "tool_configs": {"tool-id": {"param": "value"}},
1280
+ "mcp_configs": {"mcp-id": {"setting": "on"}},
1281
+ "agent_config": {"planning": True},
1282
+ }
1041
1283
  **kwargs: Additional arguments (chat_history, pii_mapping, etc.)
1042
1284
 
1043
1285
  Yields:
@@ -1048,18 +1290,25 @@ class AgentClient(BaseClient):
1048
1290
  httpx.TimeoutException: When general timeout occurs
1049
1291
  Exception: For other unexpected errors
1050
1292
  """
1293
+ # Include runtime_config in kwargs only when caller hasn't already provided it
1294
+ if runtime_config is not None and "runtime_config" not in kwargs:
1295
+ kwargs["runtime_config"] = runtime_config
1296
+ # Derive timeout values for request/control flow
1297
+ legacy_timeout = kwargs.get("timeout")
1298
+ http_timeout_override = request_timeout if request_timeout is not None else legacy_timeout
1299
+ http_timeout = http_timeout_override or self.timeout
1300
+
1051
1301
  # Prepare request data
1052
1302
  payload, data_payload, files_payload, headers = self._prepare_request_data(message, files, **kwargs)
1053
1303
 
1054
1304
  # Create async client configuration
1055
- async_client_config = self._create_async_client_config(timeout, headers)
1305
+ async_client_config = self._create_async_client_config(http_timeout_override, headers)
1056
1306
 
1057
1307
  # Get execution timeout for streaming control
1058
1308
  timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
1059
1309
  agent_name = kwargs.get("agent_name")
1060
1310
 
1061
- try:
1062
- # Create async client and stream response
1311
+ async def _chunk_stream() -> AsyncGenerator[dict, None]:
1063
1312
  async with httpx.AsyncClient(**async_client_config) as async_client:
1064
1313
  async for chunk in self._stream_agent_response(
1065
1314
  async_client,
@@ -1073,7 +1322,14 @@ class AgentClient(BaseClient):
1073
1322
  ):
1074
1323
  yield chunk
1075
1324
 
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
1325
+ async with _async_timeout_guard(http_timeout):
1326
+ async for chunk in _chunk_stream():
1327
+ yield chunk
1328
+
1329
+ @property
1330
+ def runs(self) -> "AgentRunsClient":
1331
+ """Get the agent runs client."""
1332
+ if self._runs_client is None:
1333
+ shared_config = build_shared_config(self)
1334
+ self._runs_client = AgentRunsClient(**shared_config)
1335
+ return self._runs_client
glaip_sdk/client/base.py CHANGED
@@ -3,6 +3,7 @@
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
 
8
9
  import logging
glaip_sdk/client/main.py CHANGED
@@ -3,15 +3,24 @@
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
 
8
- from typing import Any
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any
9
12
 
10
13
  from glaip_sdk.client.agents import AgentClient
11
14
  from glaip_sdk.client.base import BaseClient
12
15
  from glaip_sdk.client.mcps import MCPClient
16
+ from glaip_sdk.client.shared import build_shared_config
13
17
  from glaip_sdk.client.tools import ToolClient
14
- from glaip_sdk.models import MCP, Agent, Tool
18
+
19
+ if TYPE_CHECKING: # pragma: no cover
20
+ from glaip_sdk.agents import Agent
21
+ from glaip_sdk.client._agent_payloads import AgentListResult
22
+ from glaip_sdk.mcps import MCP
23
+ from glaip_sdk.tools import Tool
15
24
 
16
25
 
17
26
  class Client(BaseClient):
@@ -25,12 +34,7 @@ class Client(BaseClient):
25
34
  """
26
35
  super().__init__(**kwargs)
27
36
  # Share the single httpx.Client + config with sub-clients
28
- shared_config = {
29
- "parent_client": self,
30
- "api_url": self.api_url,
31
- "api_key": self.api_key,
32
- "timeout": self._timeout,
33
- }
37
+ shared_config = build_shared_config(self)
34
38
  self.agents = AgentClient(**shared_config)
35
39
  self.tools = ToolClient(**shared_config)
36
40
  self.mcps = MCPClient(**shared_config)
@@ -53,7 +57,7 @@ class Client(BaseClient):
53
57
  name: str | None = None,
54
58
  version: str | None = None,
55
59
  sync_langflow_agents: bool = False,
56
- ) -> list[Agent]:
60
+ ) -> AgentListResult:
57
61
  """List agents with optional filtering.
58
62
 
59
63
  Args:
@@ -64,7 +68,7 @@ class Client(BaseClient):
64
68
  sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
65
69
 
66
70
  Returns:
67
- List of agents matching the filters
71
+ AgentListResult with agents and pagination metadata. Supports iteration and indexing.
68
72
  """
69
73
  return self.agents.list_agents(
70
74
  agent_type=agent_type,
@@ -217,6 +221,11 @@ class Client(BaseClient):
217
221
 
218
222
  @timeout.setter
219
223
  def timeout(self, value: float) -> None: # type: ignore[override]
224
+ """Set the client timeout and propagate to sub-clients.
225
+
226
+ Args:
227
+ value: Timeout value in seconds.
228
+ """
220
229
  # Rebuild the root http client
221
230
  BaseClient.timeout.fset(self, value) # call parent setter
222
231
  # Propagate the new session to sub-clients so they don't hold a closed client