glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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 (156) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1191 -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 +265 -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.py +251 -173
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +735 -143
  14. glaip_sdk/cli/commands/mcps.py +266 -134
  15. glaip_sdk/cli/commands/models.py +13 -9
  16. glaip_sdk/cli/commands/tools.py +67 -88
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +3 -8
  19. glaip_sdk/cli/config.py +49 -7
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +8 -0
  22. glaip_sdk/cli/core/__init__.py +79 -0
  23. glaip_sdk/cli/core/context.py +124 -0
  24. glaip_sdk/cli/core/output.py +846 -0
  25. glaip_sdk/cli/core/prompting.py +649 -0
  26. glaip_sdk/cli/core/rendering.py +187 -0
  27. glaip_sdk/cli/display.py +45 -32
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +14 -17
  30. glaip_sdk/cli/main.py +232 -143
  31. glaip_sdk/cli/masking.py +21 -33
  32. glaip_sdk/cli/mcp_validators.py +5 -15
  33. glaip_sdk/cli/pager.py +12 -19
  34. glaip_sdk/cli/parsers/__init__.py +1 -3
  35. glaip_sdk/cli/parsers/json_input.py +11 -22
  36. glaip_sdk/cli/resolution.py +3 -9
  37. glaip_sdk/cli/rich_helpers.py +1 -3
  38. glaip_sdk/cli/slash/__init__.py +0 -9
  39. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +65 -29
  42. glaip_sdk/cli/slash/prompt.py +24 -10
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +807 -225
  45. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  46. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  47. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  48. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  49. glaip_sdk/cli/slash/tui/loading.py +58 -0
  50. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  51. glaip_sdk/cli/transcript/__init__.py +12 -52
  52. glaip_sdk/cli/transcript/cache.py +258 -60
  53. glaip_sdk/cli/transcript/capture.py +72 -21
  54. glaip_sdk/cli/transcript/history.py +815 -0
  55. glaip_sdk/cli/transcript/launcher.py +1 -3
  56. glaip_sdk/cli/transcript/viewer.py +79 -499
  57. glaip_sdk/cli/update_notifier.py +177 -24
  58. glaip_sdk/cli/utils.py +242 -1308
  59. glaip_sdk/cli/validators.py +16 -18
  60. glaip_sdk/client/__init__.py +2 -1
  61. glaip_sdk/client/_agent_payloads.py +53 -37
  62. glaip_sdk/client/agent_runs.py +147 -0
  63. glaip_sdk/client/agents.py +320 -92
  64. glaip_sdk/client/base.py +78 -35
  65. glaip_sdk/client/main.py +19 -10
  66. glaip_sdk/client/mcps.py +123 -15
  67. glaip_sdk/client/run_rendering.py +136 -101
  68. glaip_sdk/client/shared.py +21 -0
  69. glaip_sdk/client/tools.py +163 -34
  70. glaip_sdk/client/validators.py +20 -48
  71. glaip_sdk/config/constants.py +11 -0
  72. glaip_sdk/exceptions.py +1 -3
  73. glaip_sdk/mcps/__init__.py +21 -0
  74. glaip_sdk/mcps/base.py +345 -0
  75. glaip_sdk/models/__init__.py +90 -0
  76. glaip_sdk/models/agent.py +47 -0
  77. glaip_sdk/models/agent_runs.py +116 -0
  78. glaip_sdk/models/common.py +42 -0
  79. glaip_sdk/models/mcp.py +33 -0
  80. glaip_sdk/models/tool.py +33 -0
  81. glaip_sdk/payload_schemas/__init__.py +1 -13
  82. glaip_sdk/payload_schemas/agent.py +1 -3
  83. glaip_sdk/registry/__init__.py +55 -0
  84. glaip_sdk/registry/agent.py +164 -0
  85. glaip_sdk/registry/base.py +139 -0
  86. glaip_sdk/registry/mcp.py +253 -0
  87. glaip_sdk/registry/tool.py +232 -0
  88. glaip_sdk/rich_components.py +58 -2
  89. glaip_sdk/runner/__init__.py +59 -0
  90. glaip_sdk/runner/base.py +84 -0
  91. glaip_sdk/runner/deps.py +115 -0
  92. glaip_sdk/runner/langgraph.py +706 -0
  93. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  94. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  95. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  96. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  97. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  98. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  99. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  100. glaip_sdk/tools/__init__.py +22 -0
  101. glaip_sdk/tools/base.py +435 -0
  102. glaip_sdk/utils/__init__.py +58 -12
  103. glaip_sdk/utils/a2a/__init__.py +34 -0
  104. glaip_sdk/utils/a2a/event_processor.py +188 -0
  105. glaip_sdk/utils/agent_config.py +4 -14
  106. glaip_sdk/utils/bundler.py +267 -0
  107. glaip_sdk/utils/client.py +111 -0
  108. glaip_sdk/utils/client_utils.py +46 -28
  109. glaip_sdk/utils/datetime_helpers.py +58 -0
  110. glaip_sdk/utils/discovery.py +78 -0
  111. glaip_sdk/utils/display.py +25 -21
  112. glaip_sdk/utils/export.py +143 -0
  113. glaip_sdk/utils/general.py +1 -36
  114. glaip_sdk/utils/import_export.py +15 -16
  115. glaip_sdk/utils/import_resolver.py +492 -0
  116. glaip_sdk/utils/instructions.py +101 -0
  117. glaip_sdk/utils/rendering/__init__.py +115 -1
  118. glaip_sdk/utils/rendering/formatting.py +7 -35
  119. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  120. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  121. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  122. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  123. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  124. glaip_sdk/utils/rendering/models.py +3 -6
  125. glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
  126. glaip_sdk/utils/rendering/renderer/base.py +258 -1577
  127. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  128. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  129. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  130. glaip_sdk/utils/rendering/renderer/stream.py +10 -51
  131. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  132. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  133. glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
  134. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  135. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  136. glaip_sdk/utils/rendering/state.py +204 -0
  137. glaip_sdk/utils/rendering/step_tree_state.py +1 -3
  138. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  139. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
  140. glaip_sdk/utils/rendering/steps/format.py +176 -0
  141. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  142. glaip_sdk/utils/rendering/timing.py +36 -0
  143. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  144. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  145. glaip_sdk/utils/resource_refs.py +29 -26
  146. glaip_sdk/utils/runtime_config.py +425 -0
  147. glaip_sdk/utils/serialization.py +32 -46
  148. glaip_sdk/utils/sync.py +142 -0
  149. glaip_sdk/utils/tool_detection.py +33 -0
  150. glaip_sdk/utils/validation.py +20 -28
  151. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
  152. glaip_sdk-0.6.10.dist-info/RECORD +159 -0
  153. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
  154. glaip_sdk/models.py +0 -259
  155. glaip_sdk-0.1.0.dist-info/RECORD +0 -82
  156. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
@@ -13,6 +13,7 @@ from typing import Any
13
13
 
14
14
  import click
15
15
 
16
+ from glaip_sdk.cli.utils import handle_best_effort_check
16
17
  from glaip_sdk.utils.validation import (
17
18
  coerce_timeout,
18
19
  validate_agent_instruction,
@@ -42,7 +43,7 @@ def validate_agent_name_cli(name: str) -> str:
42
43
  try:
43
44
  return validate_agent_name(name)
44
45
  except ValueError as e:
45
- raise click.ClickException(str(e))
46
+ raise click.ClickException(str(e)) from e
46
47
 
47
48
 
48
49
  def validate_agent_instruction_cli(instruction: str) -> str:
@@ -60,7 +61,7 @@ def validate_agent_instruction_cli(instruction: str) -> str:
60
61
  try:
61
62
  return validate_agent_instruction(instruction)
62
63
  except ValueError as e:
63
- raise click.ClickException(str(e))
64
+ raise click.ClickException(str(e)) from e
64
65
 
65
66
 
66
67
  def validate_timeout_cli(timeout: int) -> int:
@@ -78,7 +79,7 @@ def validate_timeout_cli(timeout: int) -> int:
78
79
  try:
79
80
  return validate_timeout(timeout)
80
81
  except ValueError as e:
81
- raise click.ClickException(str(e))
82
+ raise click.ClickException(str(e)) from e
82
83
 
83
84
 
84
85
  def validate_tool_name_cli(name: str) -> str:
@@ -96,7 +97,7 @@ def validate_tool_name_cli(name: str) -> str:
96
97
  try:
97
98
  return validate_tool_name(name)
98
99
  except ValueError as e:
99
- raise click.ClickException(str(e))
100
+ raise click.ClickException(str(e)) from e
100
101
 
101
102
 
102
103
  def validate_mcp_name_cli(name: str) -> str:
@@ -114,7 +115,7 @@ def validate_mcp_name_cli(name: str) -> str:
114
115
  try:
115
116
  return validate_mcp_name(name)
116
117
  except ValueError as e:
117
- raise click.ClickException(str(e))
118
+ raise click.ClickException(str(e)) from e
118
119
 
119
120
 
120
121
  def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Path:
@@ -133,7 +134,7 @@ def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Pa
133
134
  try:
134
135
  return validate_file_path(file_path, must_exist)
135
136
  except ValueError as e:
136
- raise click.ClickException(str(e))
137
+ raise click.ClickException(str(e)) from e
137
138
 
138
139
 
139
140
  def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -> Path:
@@ -152,7 +153,7 @@ def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -
152
153
  try:
153
154
  return validate_directory_path(dir_path, must_exist)
154
155
  except ValueError as e:
155
- raise click.ClickException(str(e))
156
+ raise click.ClickException(str(e)) from e
156
157
 
157
158
 
158
159
  def validate_url_cli(url: str) -> str:
@@ -170,7 +171,7 @@ def validate_url_cli(url: str) -> str:
170
171
  try:
171
172
  return validate_url(url)
172
173
  except ValueError as e:
173
- raise click.ClickException(str(e))
174
+ raise click.ClickException(str(e)) from e
174
175
 
175
176
 
176
177
  def validate_api_key_cli(api_key: str) -> str:
@@ -188,7 +189,7 @@ def validate_api_key_cli(api_key: str) -> str:
188
189
  try:
189
190
  return validate_api_key(api_key)
190
191
  except ValueError as e:
191
- raise click.ClickException(str(e))
192
+ raise click.ClickException(str(e)) from e
192
193
 
193
194
 
194
195
  def coerce_timeout_cli(value: int | float | str) -> int:
@@ -206,7 +207,7 @@ def coerce_timeout_cli(value: int | float | str) -> int:
206
207
  try:
207
208
  return coerce_timeout(value)
208
209
  except ValueError as e:
209
- raise click.ClickException(str(e))
210
+ raise click.ClickException(str(e)) from e
210
211
 
211
212
 
212
213
  def validate_name_uniqueness_cli(
@@ -226,15 +227,12 @@ def validate_name_uniqueness_cli(
226
227
  Raises:
227
228
  click.ClickException: If name is not unique
228
229
  """
229
- try:
230
+
231
+ def _check_duplicate() -> None:
230
232
  existing = finder_func(name=name)
231
233
  if existing:
232
234
  raise click.ClickException(
233
- f"A {resource_type.lower()} named '{name}' already exists. "
234
- "Please choose a unique name."
235
+ f"A {resource_type.lower()} named '{name}' already exists. Please choose a unique name."
235
236
  )
236
- except click.ClickException:
237
- raise
238
- except Exception:
239
- # Non-fatal: best-effort duplicate check
240
- pass
237
+
238
+ handle_best_effort_check(_check_duplicate)
@@ -5,6 +5,7 @@ Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
6
  """
7
7
 
8
+ from glaip_sdk.client.agent_runs import AgentRunsClient
8
9
  from glaip_sdk.client.main import Client
9
10
 
10
- __all__ = ["Client"]
11
+ __all__ = ["AgentRunsClient", "Client"]
@@ -15,10 +15,7 @@ from glaip_sdk.config.constants import (
15
15
  DEFAULT_AGENT_VERSION,
16
16
  DEFAULT_MODEL,
17
17
  )
18
- from glaip_sdk.payload_schemas.agent import (
19
- AgentImportOperation,
20
- get_import_field_plan,
21
- )
18
+ from glaip_sdk.payload_schemas.agent import AgentImportOperation, get_import_field_plan
22
19
  from glaip_sdk.utils.client_utils import extract_ids
23
20
 
24
21
  _LM_CONFLICT_KEYS = {
@@ -209,6 +206,11 @@ class AgentListParams:
209
206
  return params
210
207
 
211
208
  def _base_filter_params(self) -> dict[str, Any]:
209
+ """Build base filter parameters from non-None fields.
210
+
211
+ Returns:
212
+ Dictionary of filter parameters with non-None values.
213
+ """
212
214
  return {
213
215
  key: value
214
216
  for key, value in (
@@ -221,6 +223,11 @@ class AgentListParams:
221
223
  }
222
224
 
223
225
  def _apply_pagination_params(self, params: dict[str, Any]) -> None:
226
+ """Apply pagination parameters to the params dictionary.
227
+
228
+ Args:
229
+ params: Dictionary to update with pagination parameters.
230
+ """
224
231
  if self.limit is not None:
225
232
  if not 1 <= self.limit <= 100:
226
233
  raise ValueError("limit must be between 1 and 100 inclusive")
@@ -238,6 +245,11 @@ class AgentListParams:
238
245
  params["sync_langflow_agents"] = str(self.sync_langflow_agents).lower()
239
246
 
240
247
  def _apply_timestamp_filters(self, params: dict[str, Any]) -> None:
248
+ """Apply timestamp filter parameters to the params dictionary.
249
+
250
+ Args:
251
+ params: Dictionary to update with timestamp filter parameters.
252
+ """
241
253
  timestamp_filters = {
242
254
  "created_at_start": self.created_at_start,
243
255
  "created_at_end": self.created_at_end,
@@ -249,6 +261,11 @@ class AgentListParams:
249
261
  params[key] = value
250
262
 
251
263
  def _apply_metadata_filters(self, params: dict[str, Any]) -> None:
264
+ """Apply metadata filter parameters to the params dictionary.
265
+
266
+ Args:
267
+ params: Dictionary to update with metadata filter parameters.
268
+ """
252
269
  if not self.metadata:
253
270
  return
254
271
  for key, value in self.metadata.items():
@@ -269,12 +286,22 @@ class AgentListResult:
269
286
  message: str | None = None
270
287
 
271
288
  def __len__(self) -> int: # pragma: no cover - simple delegation
289
+ """Return the number of items in the result list."""
272
290
  return len(self.items)
273
291
 
274
292
  def __iter__(self): # pragma: no cover - simple delegation
293
+ """Return an iterator over the items in the result list."""
275
294
  return iter(self.items)
276
295
 
277
296
  def __getitem__(self, index: int) -> Any: # pragma: no cover - simple delegation
297
+ """Get an item from the result list by index.
298
+
299
+ Args:
300
+ index: Index of the item to retrieve.
301
+
302
+ Returns:
303
+ The item at the specified index.
304
+ """
278
305
  return self.items[index]
279
306
 
280
307
 
@@ -348,9 +375,7 @@ class AgentCreateRequest:
348
375
  payload["tool_configs"] = _copy_structure(self.tool_configs)
349
376
 
350
377
  effective_agent_config = _sanitize_agent_config(self.agent_config)
351
- effective_agent_config = _merge_execution_timeout(
352
- effective_agent_config, self.timeout
353
- )
378
+ effective_agent_config = _merge_execution_timeout(effective_agent_config, self.timeout)
354
379
  if effective_agent_config:
355
380
  payload["agent_config"] = effective_agent_config
356
381
 
@@ -407,31 +432,25 @@ __all__ = [
407
432
  ]
408
433
 
409
434
 
410
- def _build_base_update_payload(
411
- request: AgentUpdateRequest, current_agent: Any
412
- ) -> dict[str, Any]:
435
+ def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
436
+ """Populate immutable agent update fields using request data or existing agent defaults."""
437
+ # Support both "agent_type" (runtime class) and "type" (API response) attributes
438
+ current_type = getattr(current_agent, "agent_type", None) or getattr(current_agent, "type", None)
413
439
  return {
414
- "name": request.name.strip()
415
- if request.name is not None
416
- else getattr(current_agent, "name", None),
417
- "instruction": request.instruction.strip()
418
- if request.instruction is not None
419
- else getattr(current_agent, "instruction", None),
420
- "type": request.agent_type
421
- or getattr(current_agent, "type", None)
422
- or DEFAULT_AGENT_TYPE,
423
- "framework": request.framework
424
- or getattr(current_agent, "framework", None)
425
- or DEFAULT_AGENT_FRAMEWORK,
426
- "version": request.version
427
- or getattr(current_agent, "version", None)
428
- or DEFAULT_AGENT_VERSION,
440
+ "name": (request.name.strip() if request.name is not None else getattr(current_agent, "name", None)),
441
+ "instruction": (
442
+ request.instruction.strip()
443
+ if request.instruction is not None
444
+ else getattr(current_agent, "instruction", None)
445
+ ),
446
+ "type": request.agent_type or current_type or DEFAULT_AGENT_TYPE,
447
+ "framework": request.framework or getattr(current_agent, "framework", None) or DEFAULT_AGENT_FRAMEWORK,
448
+ "version": request.version or getattr(current_agent, "version", None) or DEFAULT_AGENT_VERSION,
429
449
  }
430
450
 
431
451
 
432
- def _resolve_update_language_model_fields(
433
- request: AgentUpdateRequest, current_agent: Any
434
- ) -> dict[str, Any]:
452
+ def _resolve_update_language_model_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
453
+ """Resolve the language-model portion of an update request with sensible fallbacks."""
435
454
  fields = resolve_language_model_fields(
436
455
  model=request.model,
437
456
  language_model_id=request.language_model_id,
@@ -443,9 +462,8 @@ def _resolve_update_language_model_fields(
443
462
  return fields
444
463
 
445
464
 
446
- def _collect_optional_update_fields(
447
- request: AgentUpdateRequest, current_agent: Any
448
- ) -> dict[str, Any]:
465
+ def _collect_optional_update_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
466
+ """Collect optional agent fields, preserving current values when updates are absent."""
449
467
  result: dict[str, Any] = {}
450
468
 
451
469
  for field_name, value in (
@@ -483,9 +501,8 @@ def _collect_optional_update_fields(
483
501
  return result
484
502
 
485
503
 
486
- def _collect_relationship_fields(
487
- request: AgentUpdateRequest, current_agent: Any
488
- ) -> dict[str, Any]:
504
+ def _collect_relationship_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
505
+ """Return relationship identifiers (tools/agents/mcps) for an update request."""
489
506
  return {
490
507
  "tools": _resolve_relation_ids(request.tools, current_agent, "tools"),
491
508
  "agents": _resolve_relation_ids(request.agents, current_agent, "agents"),
@@ -493,9 +510,8 @@ def _collect_relationship_fields(
493
510
  }
494
511
 
495
512
 
496
- def _resolve_agent_config_update(
497
- request: AgentUpdateRequest, current_agent: Any
498
- ) -> dict[str, Any] | None:
513
+ def _resolve_agent_config_update(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any] | None:
514
+ """Determine the agent_config payload to send, if any."""
499
515
  effective_agent_config = _sanitize_agent_config(request.agent_config)
500
516
  if effective_agent_config is not None:
501
517
  return effective_agent_config
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ """Agent runs client for AIP SDK.
3
+
4
+ Authors:
5
+ Raymond Christopher (raymond.christopher@gdplabs.id)
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from glaip_sdk.client.base import BaseClient
13
+ from glaip_sdk.exceptions import TimeoutError, ValidationError
14
+ from glaip_sdk.models.agent_runs import RunSummary, RunsPage, RunWithOutput
15
+
16
+
17
+ class AgentRunsClient(BaseClient):
18
+ """Client for agent run operations."""
19
+
20
+ def list_runs(
21
+ self,
22
+ agent_id: str,
23
+ *,
24
+ limit: int = 20,
25
+ page: int = 1,
26
+ ) -> RunsPage:
27
+ """List agent runs with pagination.
28
+
29
+ Args:
30
+ agent_id: UUID of the agent
31
+ limit: Number of runs per page (1-100, default 20)
32
+ page: Page number (1-based, default 1)
33
+
34
+ Returns:
35
+ RunsPage containing paginated run summaries
36
+
37
+ Raises:
38
+ ValidationError: If pagination parameters are invalid
39
+ NotFoundError: If agent is not found
40
+ AuthenticationError: If authentication fails
41
+ TimeoutError: If request times out (30s default)
42
+ """
43
+ self._validate_pagination_params(limit, page)
44
+ envelope = self._fetch_runs_envelope(agent_id, limit, page)
45
+ normalized_data = self._normalize_runs_payload(envelope.get("data"))
46
+ runs = [RunSummary(**item) for item in normalized_data]
47
+ return self._build_runs_page(envelope, runs, limit, page)
48
+
49
+ def _fetch_runs_envelope(self, agent_id: str, limit: int, page: int) -> dict[str, Any]:
50
+ params = {"limit": limit, "page": page}
51
+ try:
52
+ envelope = self._request_with_envelope(
53
+ "GET",
54
+ f"/agents/{agent_id}/runs",
55
+ params=params,
56
+ )
57
+ except httpx.TimeoutException as e:
58
+ raise TimeoutError(f"Request timed out after {self._timeout}s while fetching agent runs") from e
59
+
60
+ if isinstance(envelope, dict):
61
+ return envelope
62
+ return {"data": envelope}
63
+
64
+ @staticmethod
65
+ def _validate_pagination_params(limit: int, page: int) -> None:
66
+ if limit < 1 or limit > 100:
67
+ raise ValidationError("limit must be between 1 and 100")
68
+ if page < 1:
69
+ raise ValidationError("page must be >= 1")
70
+
71
+ @staticmethod
72
+ def _normalize_runs_payload(data_payload: Any) -> list[Any]:
73
+ if not data_payload:
74
+ return []
75
+ normalized_data: list[Any] = []
76
+ for item in data_payload:
77
+ normalized_data.append(AgentRunsClient._normalize_run_item(item))
78
+ return normalized_data
79
+
80
+ @staticmethod
81
+ def _normalize_run_item(item: Any) -> Any:
82
+ if isinstance(item, dict):
83
+ if item.get("config") is None:
84
+ item["config"] = {}
85
+ schedule_id = item.get("schedule_id")
86
+ if schedule_id == "None" or schedule_id == "":
87
+ item["schedule_id"] = None
88
+ return item
89
+
90
+ @staticmethod
91
+ def _build_runs_page(
92
+ envelope: dict[str, Any],
93
+ runs: list[RunSummary],
94
+ limit: int,
95
+ page: int,
96
+ ) -> RunsPage:
97
+ return RunsPage(
98
+ data=runs,
99
+ total=envelope.get("total", 0),
100
+ page=envelope.get("page", page),
101
+ limit=envelope.get("limit", limit),
102
+ has_next=envelope.get("has_next", False),
103
+ has_prev=envelope.get("has_prev", False),
104
+ )
105
+
106
+ def get_run(
107
+ self,
108
+ agent_id: str,
109
+ run_id: str,
110
+ ) -> RunWithOutput:
111
+ """Get detailed run information including SSE event stream.
112
+
113
+ Args:
114
+ agent_id: UUID of the agent
115
+ run_id: UUID of the run
116
+
117
+ Returns:
118
+ RunWithOutput containing complete run details and event stream
119
+
120
+ Raises:
121
+ NotFoundError: If run or agent is not found
122
+ AuthenticationError: If authentication fails
123
+ TimeoutError: If request times out (30s default)
124
+ """
125
+ try:
126
+ envelope = self._request_with_envelope(
127
+ "GET",
128
+ f"/agents/{agent_id}/runs/{run_id}",
129
+ )
130
+ except httpx.TimeoutException as e:
131
+ raise TimeoutError(f"Request timed out after {self._timeout}s while fetching run detail") from e
132
+
133
+ if not isinstance(envelope, dict):
134
+ envelope = {"data": envelope}
135
+
136
+ data = envelope.get("data") or {}
137
+ # Normalize config, output, and schedule_id fields
138
+ if data.get("config") is None:
139
+ data["config"] = {}
140
+ if data.get("output") is None:
141
+ data["output"] = []
142
+ # Normalize schedule_id: convert string "None" to None
143
+ schedule_id = data.get("schedule_id")
144
+ if schedule_id == "None" or schedule_id == "":
145
+ data["schedule_id"] = None
146
+
147
+ return RunWithOutput(**data)