glaip-sdk 0.0.16__py3-none-any.whl → 0.0.18__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.
@@ -7,7 +7,9 @@ Authors:
7
7
 
8
8
  import json
9
9
  import logging
10
- from collections.abc import AsyncGenerator, Mapping
10
+ from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
11
+ from os import PathLike
12
+ from pathlib import Path
11
13
  from typing import Any, BinaryIO
12
14
 
13
15
  import httpx
@@ -19,10 +21,12 @@ from glaip_sdk.client._agent_payloads import (
19
21
  AgentUpdateRequest,
20
22
  )
21
23
  from glaip_sdk.client.base import BaseClient
24
+ from glaip_sdk.client.mcps import MCPClient
22
25
  from glaip_sdk.client.run_rendering import (
23
26
  AgentRunRenderingManager,
24
27
  compute_timeout_seconds,
25
28
  )
29
+ from glaip_sdk.client.tools import ToolClient
26
30
  from glaip_sdk.config.constants import (
27
31
  DEFAULT_AGENT_FRAMEWORK,
28
32
  DEFAULT_AGENT_RUN_TIMEOUT,
@@ -32,13 +36,21 @@ from glaip_sdk.config.constants import (
32
36
  )
33
37
  from glaip_sdk.exceptions import NotFoundError
34
38
  from glaip_sdk.models import Agent
39
+ from glaip_sdk.payload_schemas.agent import list_server_only_fields
40
+ from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
35
41
  from glaip_sdk.utils.client_utils import (
36
42
  aiter_sse_events,
37
43
  create_model_instances,
38
44
  find_by_name,
39
45
  prepare_multipart_data,
40
46
  )
47
+ from glaip_sdk.utils.import_export import (
48
+ convert_export_to_import_format,
49
+ merge_import_with_cli_args,
50
+ )
41
51
  from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
52
+ from glaip_sdk.utils.resource_refs import is_uuid
53
+ from glaip_sdk.utils.serialization import load_resource_from_file
42
54
  from glaip_sdk.utils.validation import validate_agent_instruction
43
55
 
44
56
  # API endpoints
@@ -50,6 +62,188 @@ SSE_CONTENT_TYPE = "text/event-stream"
50
62
  # Set up module-level logger
51
63
  logger = logging.getLogger("glaip_sdk.agents")
52
64
 
65
+ _SERVER_ONLY_IMPORT_FIELDS = set(list_server_only_fields()) | {"success", "message"}
66
+ _MERGED_SEQUENCE_FIELDS = ("tools", "agents", "mcps")
67
+ _DEFAULT_METADATA_TYPE = "custom"
68
+
69
+
70
+ def _normalise_sequence(value: Any) -> list[Any] | None:
71
+ """Normalise optional sequence inputs to plain lists."""
72
+ if value is None:
73
+ return None
74
+ if isinstance(value, list):
75
+ return value
76
+ if isinstance(value, (tuple, set)):
77
+ return list(value)
78
+ return [value]
79
+
80
+
81
+ def _normalise_sequence_fields(mapping: dict[str, Any]) -> None:
82
+ """Normalise merged sequence fields in-place."""
83
+ for field in _MERGED_SEQUENCE_FIELDS:
84
+ if field in mapping:
85
+ normalised = _normalise_sequence(mapping[field])
86
+ if normalised is not None:
87
+ mapping[field] = normalised
88
+
89
+
90
+ def _merge_override_maps(
91
+ base_values: Mapping[str, Any],
92
+ extra_values: Mapping[str, Any],
93
+ ) -> dict[str, Any]:
94
+ """Merge override mappings while normalising sequence fields."""
95
+ merged: dict[str, Any] = {}
96
+ for source in (base_values, extra_values):
97
+ for key, value in source.items():
98
+ if value is None:
99
+ continue
100
+ merged[key] = (
101
+ _normalise_sequence(value) if key in _MERGED_SEQUENCE_FIELDS else value
102
+ )
103
+ return merged
104
+
105
+
106
+ def _split_known_and_extra(
107
+ payload: Mapping[str, Any],
108
+ known_fields: Mapping[str, Any],
109
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
110
+ """Split payload mapping into known request fields and extras."""
111
+ known: dict[str, Any] = {}
112
+ extras: dict[str, Any] = {}
113
+ for key, value in payload.items():
114
+ if value is None:
115
+ continue
116
+ if key in known_fields:
117
+ known[key] = value
118
+ else:
119
+ extras[key] = value
120
+ return known, extras
121
+
122
+
123
+ def _prepare_agent_metadata(value: Any) -> dict[str, Any]:
124
+ """Ensure agent metadata contains ``type: custom`` by default."""
125
+ if value is None:
126
+ return {"type": _DEFAULT_METADATA_TYPE}
127
+ if not isinstance(value, Mapping):
128
+ return {"type": _DEFAULT_METADATA_TYPE}
129
+
130
+ prepared = dict(value)
131
+ metadata_type = prepared.get("type")
132
+ if not metadata_type:
133
+ prepared["type"] = _DEFAULT_METADATA_TYPE
134
+ return prepared
135
+
136
+
137
+ def _load_agent_file_payload(
138
+ file_path: Path, *, model_override: str | None
139
+ ) -> dict[str, Any]:
140
+ """Load agent configuration from disk and normalise legacy fields."""
141
+ if not file_path.exists():
142
+ raise FileNotFoundError(f"Agent configuration file not found: {file_path}")
143
+ if not file_path.is_file():
144
+ raise ValueError(f"Agent configuration path must point to a file: {file_path}")
145
+
146
+ raw_data = load_resource_from_file(file_path)
147
+ if not isinstance(raw_data, Mapping):
148
+ raise ValueError("Agent configuration file must contain a mapping/object.")
149
+
150
+ payload = convert_export_to_import_format(dict(raw_data))
151
+ payload = normalize_agent_config_for_import(payload, model_override)
152
+
153
+ for field in _SERVER_ONLY_IMPORT_FIELDS:
154
+ payload.pop(field, None)
155
+
156
+ return payload
157
+
158
+
159
+ def _prepare_import_payload(
160
+ file_path: Path,
161
+ overrides: Mapping[str, Any],
162
+ *,
163
+ drop_model_fields: bool = False,
164
+ ) -> dict[str, Any]:
165
+ """Prepare merged payload from file contents and explicit overrides."""
166
+ overrides_dict = dict(overrides)
167
+
168
+ raw_definition = load_resource_from_file(file_path)
169
+ original_refs = _extract_original_refs(raw_definition)
170
+
171
+ base_payload = _load_agent_file_payload(
172
+ file_path, model_override=overrides_dict.get("model")
173
+ )
174
+
175
+ cli_args = _build_cli_args(overrides_dict)
176
+
177
+ merged = merge_import_with_cli_args(base_payload, cli_args)
178
+
179
+ additional = _build_additional_args(overrides_dict, cli_args)
180
+ merged.update(additional)
181
+
182
+ if drop_model_fields:
183
+ _remove_model_fields_if_needed(merged, overrides_dict)
184
+
185
+ _set_default_refs(merged, original_refs)
186
+
187
+ _normalise_sequence_fields(merged)
188
+ return merged
189
+
190
+
191
+ def _extract_original_refs(raw_definition: dict) -> dict[str, list]:
192
+ """Extract original tool/agent/mcp references from raw definition."""
193
+ return {
194
+ "tools": list(raw_definition.get("tools") or []),
195
+ "agents": list(raw_definition.get("agents") or []),
196
+ "mcps": list(raw_definition.get("mcps") or []),
197
+ }
198
+
199
+
200
+ def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
201
+ """Build CLI args from overrides, filtering out None values."""
202
+ cli_args = {
203
+ key: overrides_dict.get(key)
204
+ for key in (
205
+ "name",
206
+ "instruction",
207
+ "model",
208
+ "tools",
209
+ "agents",
210
+ "mcps",
211
+ "timeout",
212
+ )
213
+ if overrides_dict.get(key) is not None
214
+ }
215
+
216
+ # Normalize sequence fields
217
+ for field in _MERGED_SEQUENCE_FIELDS:
218
+ if field in cli_args:
219
+ cli_args[field] = tuple(_normalise_sequence(cli_args[field]) or [])
220
+
221
+ return cli_args
222
+
223
+
224
+ def _build_additional_args(overrides_dict: dict, cli_args: dict) -> dict[str, Any]:
225
+ """Build additional args not already in CLI args."""
226
+ return {
227
+ key: value
228
+ for key, value in overrides_dict.items()
229
+ if value is not None and key not in cli_args
230
+ }
231
+
232
+
233
+ def _remove_model_fields_if_needed(merged: dict, overrides_dict: dict) -> None:
234
+ """Remove model fields if not explicitly overridden."""
235
+ if overrides_dict.get("language_model_id") is None:
236
+ merged.pop("language_model_id", None)
237
+ if overrides_dict.get("provider") is None:
238
+ merged.pop("provider", None)
239
+
240
+
241
+ def _set_default_refs(merged: dict, original_refs: dict) -> None:
242
+ """Set default references if not already present."""
243
+ merged.setdefault("_tool_refs", original_refs["tools"])
244
+ merged.setdefault("_agent_refs", original_refs["agents"])
245
+ merged.setdefault("_mcp_refs", original_refs["mcps"])
246
+
53
247
 
54
248
  class AgentClient(BaseClient):
55
249
  """Client for agent operations."""
@@ -68,6 +262,8 @@ class AgentClient(BaseClient):
68
262
  """
69
263
  super().__init__(parent_client=parent_client, **kwargs)
70
264
  self._renderer_manager = AgentRunRenderingManager(logger)
265
+ self._tool_client: ToolClient | None = None
266
+ self._mcp_client: MCPClient | None = None
71
267
 
72
268
  def list_agents(
73
269
  self,
@@ -219,129 +415,426 @@ class AgentClient(BaseClient):
219
415
  finished_monotonic,
220
416
  )
221
417
 
222
- def create_agent(
418
+ def _get_tool_client(self) -> ToolClient:
419
+ if self._tool_client is None:
420
+ self._tool_client = ToolClient(parent_client=self)
421
+ return self._tool_client
422
+
423
+ def _get_mcp_client(self) -> MCPClient:
424
+ if self._mcp_client is None:
425
+ self._mcp_client = MCPClient(parent_client=self)
426
+ return self._mcp_client
427
+
428
+ def _normalise_reference_entry(
223
429
  self,
224
- name: str,
225
- instruction: str,
226
- model: str = DEFAULT_MODEL,
227
- tools: list[str | Any] | None = None,
228
- agents: list[str | Any] | None = None,
229
- timeout: int = DEFAULT_AGENT_RUN_TIMEOUT,
430
+ entry: Any,
431
+ fallback_iter: Iterator[Any] | None,
432
+ ) -> tuple[str | None, str | None]:
433
+ entry_id: str | None = None
434
+ entry_name: str | None = None
435
+
436
+ if isinstance(entry, str):
437
+ if is_uuid(entry):
438
+ entry_id = entry
439
+ else:
440
+ entry_name = entry
441
+ elif isinstance(entry, dict):
442
+ entry_id = entry.get("id")
443
+ entry_name = entry.get("name")
444
+ else:
445
+ entry_name = str(entry)
446
+
447
+ if entry_name or fallback_iter is None:
448
+ return entry_id, entry_name
449
+
450
+ try:
451
+ ref = next(fallback_iter)
452
+ except StopIteration:
453
+ ref = None
454
+ if isinstance(ref, dict):
455
+ entry_name = ref.get("name") or entry_name
456
+
457
+ return entry_id, entry_name
458
+
459
+ def _resolve_resource_ids(
460
+ self,
461
+ items: list[Any] | None,
462
+ references: list[Any] | None,
230
463
  *,
231
- mcps: list[str | Any] | None = None,
232
- tool_configs: Mapping[str, Any] | None = None,
233
- **kwargs: Any,
234
- ) -> "Agent":
235
- """Create a new agent."""
236
- if not name or not name.strip():
237
- raise ValueError("Agent name cannot be empty or whitespace")
464
+ fetch_by_id: Callable[[str], Any],
465
+ find_by_name: Callable[[str], list[Any]],
466
+ label: str,
467
+ plural_label: str | None = None,
468
+ ) -> list[str] | None:
469
+ if not items:
470
+ return None
471
+
472
+ if references is None:
473
+ return [self._coerce_reference_value(entry) for entry in items]
474
+
475
+ singular = label
476
+ plural = plural_label or f"{label}s"
477
+ fallback_iter = iter(references or [])
478
+
479
+ return [
480
+ self._resolve_single_resource(
481
+ entry,
482
+ fallback_iter,
483
+ fetch_by_id,
484
+ find_by_name,
485
+ singular,
486
+ plural,
487
+ )
488
+ for entry in items
489
+ ]
490
+
491
+ def _resolve_single_resource(
492
+ self,
493
+ entry: Any,
494
+ fallback_iter: Iterator[Any] | None,
495
+ fetch_by_id: Callable[[str], Any],
496
+ find_by_name: Callable[[str], list[Any]],
497
+ singular: str,
498
+ plural: str,
499
+ ) -> str:
500
+ entry_id, entry_name = self._normalise_reference_entry(entry, fallback_iter)
501
+
502
+ validated_id = self._validate_resource_id(fetch_by_id, entry_id)
503
+ if validated_id:
504
+ return validated_id
505
+ if entry_id and entry_name is None:
506
+ return entry_id
507
+
508
+ if entry_name:
509
+ resolved, success = self._resolve_resource_by_name(
510
+ find_by_name, entry_name, singular, plural
511
+ )
512
+ return resolved if success else entry_name
513
+
514
+ raise ValueError(f"{singular} references must include a valid ID or name.")
515
+
516
+ @staticmethod
517
+ def _coerce_reference_value(entry: Any) -> str:
518
+ if isinstance(entry, dict):
519
+ if entry.get("id"):
520
+ return str(entry["id"])
521
+ if entry.get("name"):
522
+ return str(entry["name"])
523
+ return str(entry)
524
+
525
+ @staticmethod
526
+ def _validate_resource_id(
527
+ fetch_by_id: Callable[[str], Any], candidate_id: str | None
528
+ ) -> str | None:
529
+ if not candidate_id:
530
+ return None
531
+ try:
532
+ fetch_by_id(candidate_id)
533
+ except Exception:
534
+ return None
535
+ return candidate_id
536
+
537
+ @staticmethod
538
+ def _resolve_resource_by_name(
539
+ find_by_name: Callable[[str], list[Any]],
540
+ entry_name: str,
541
+ singular: str,
542
+ plural: str,
543
+ ) -> tuple[str, bool]:
544
+ try:
545
+ matches = find_by_name(entry_name)
546
+ except Exception:
547
+ return entry_name, False
238
548
 
239
- instruction = validate_agent_instruction(instruction)
240
-
241
- agent_type = kwargs.pop("agent_type", kwargs.pop("type", DEFAULT_AGENT_TYPE))
242
- framework = kwargs.pop("framework", DEFAULT_AGENT_FRAMEWORK)
243
- version = kwargs.pop("version", DEFAULT_AGENT_VERSION)
244
- language_model_id = kwargs.pop("language_model_id", None)
245
- provider_override = kwargs.pop("provider", None)
246
- model_name_override = kwargs.pop("model_name", None)
247
- account_id = kwargs.pop("account_id", None)
248
- description = kwargs.pop("description", None)
249
- metadata = kwargs.pop("metadata", None)
250
- agent_config = kwargs.pop("agent_config", None)
251
- a2a_profile = kwargs.pop("a2a_profile", None)
252
- mcps = mcps if mcps is not None else kwargs.pop("mcps", None)
253
- tool_configs = (
254
- tool_configs
255
- if tool_configs is not None
256
- else kwargs.pop("tool_configs", None)
549
+ if not matches:
550
+ raise ValueError(
551
+ f"{singular} '{entry_name}' not found in current workspace."
552
+ )
553
+ if len(matches) > 1:
554
+ exact = [
555
+ m
556
+ for m in matches
557
+ if getattr(m, "name", "").lower() == entry_name.lower()
558
+ ]
559
+ if len(exact) == 1:
560
+ matches = exact
561
+ else:
562
+ raise ValueError(
563
+ f"Multiple {plural} named '{entry_name}'. Please disambiguate."
564
+ )
565
+ return str(matches[0].id), True
566
+
567
+ def _resolve_tool_ids(
568
+ self,
569
+ tools: list[Any] | None,
570
+ references: list[Any] | None = None,
571
+ ) -> list[str] | None:
572
+ tool_client = self._get_tool_client()
573
+ return self._resolve_resource_ids(
574
+ tools,
575
+ references,
576
+ fetch_by_id=tool_client.get_tool_by_id,
577
+ find_by_name=tool_client.find_tools,
578
+ label="Tool",
579
+ plural_label="tools",
580
+ )
581
+
582
+ def _resolve_agent_ids(
583
+ self,
584
+ agents: list[Any] | None,
585
+ references: list[Any] | None = None,
586
+ ) -> list[str] | None:
587
+ return self._resolve_resource_ids(
588
+ agents,
589
+ references,
590
+ fetch_by_id=self.get_agent_by_id,
591
+ find_by_name=self.find_agents,
592
+ label="Agent",
593
+ plural_label="agents",
257
594
  )
258
595
 
596
+ def _resolve_mcp_ids(
597
+ self,
598
+ mcps: list[Any] | None,
599
+ references: list[Any] | None = None,
600
+ ) -> list[str] | None:
601
+ mcp_client = self._get_mcp_client()
602
+ return self._resolve_resource_ids(
603
+ mcps,
604
+ references,
605
+ fetch_by_id=mcp_client.get_mcp_by_id,
606
+ find_by_name=mcp_client.find_mcps,
607
+ label="MCP",
608
+ plural_label="MCPs",
609
+ )
610
+
611
+ def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
612
+ """Create an agent using a fully prepared payload mapping."""
613
+ known, extras = _split_known_and_extra(
614
+ payload, AgentCreateRequest.__dataclass_fields__
615
+ )
616
+
617
+ name = known.pop("name", None)
618
+ instruction = known.pop("instruction", None)
619
+ if not name or not str(name).strip():
620
+ raise ValueError("Agent name cannot be empty or whitespace")
621
+ if not instruction or not str(instruction).strip():
622
+ raise ValueError("Agent instruction cannot be empty or whitespace")
623
+
624
+ validated_instruction = validate_agent_instruction(str(instruction))
625
+ _normalise_sequence_fields(known)
626
+
627
+ resolved_model = known.pop("model", None) or DEFAULT_MODEL
628
+ tool_refs = extras.pop("_tool_refs", None)
629
+ agent_refs = extras.pop("_agent_refs", None)
630
+ mcp_refs = extras.pop("_mcp_refs", None)
631
+
632
+ tools_raw = known.pop("tools", None)
633
+ agents_raw = known.pop("agents", None)
634
+ mcps_raw = known.pop("mcps", None)
635
+
636
+ resolved_tools = self._resolve_tool_ids(tools_raw, tool_refs)
637
+ resolved_agents = self._resolve_agent_ids(agents_raw, agent_refs)
638
+ resolved_mcps = self._resolve_mcp_ids(mcps_raw, mcp_refs)
639
+
640
+ language_model_id = known.pop("language_model_id", None)
641
+ provider = known.pop("provider", None)
642
+ model_name = known.pop("model_name", None)
643
+
644
+ agent_type_value = known.pop("agent_type", None)
645
+ fallback_type_value = known.pop("type", None)
646
+ if agent_type_value is None:
647
+ agent_type_value = fallback_type_value or DEFAULT_AGENT_TYPE
648
+
649
+ framework_value = known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK
650
+ version_value = known.pop("version", None) or DEFAULT_AGENT_VERSION
651
+ account_id = known.pop("account_id", None)
652
+ description = known.pop("description", None)
653
+ metadata = _prepare_agent_metadata(known.pop("metadata", None))
654
+ tool_configs = known.pop("tool_configs", None)
655
+ agent_config = known.pop("agent_config", None)
656
+ timeout_value = known.pop("timeout", None)
657
+ a2a_profile = known.pop("a2a_profile", None)
658
+
659
+ final_extras = {**known, **extras}
660
+ final_extras.setdefault("model", resolved_model)
661
+
259
662
  request = AgentCreateRequest(
260
- name=name,
261
- instruction=instruction,
262
- model=model,
663
+ name=str(name).strip(),
664
+ instruction=validated_instruction,
665
+ model=resolved_model,
263
666
  language_model_id=language_model_id,
264
- provider=provider_override,
265
- model_name=model_name_override,
266
- agent_type=agent_type,
267
- framework=framework,
268
- version=version,
667
+ provider=provider,
668
+ model_name=model_name,
669
+ agent_type=agent_type_value,
670
+ framework=framework_value,
671
+ version=version_value,
269
672
  account_id=account_id,
270
673
  description=description,
271
674
  metadata=metadata,
272
- tools=tools,
273
- agents=agents,
274
- mcps=mcps,
675
+ tools=resolved_tools,
676
+ agents=resolved_agents,
677
+ mcps=resolved_mcps,
275
678
  tool_configs=tool_configs,
276
679
  agent_config=agent_config,
277
- timeout=timeout,
680
+ timeout=timeout_value or DEFAULT_AGENT_RUN_TIMEOUT,
278
681
  a2a_profile=a2a_profile,
279
- extras=kwargs,
682
+ extras=final_extras,
280
683
  )
281
684
 
282
- payload = request.to_payload()
685
+ payload_dict = request.to_payload()
686
+ payload_dict.setdefault("model", resolved_model)
283
687
 
284
688
  full_agent_data = self._post_then_fetch(
285
689
  id_key="id",
286
690
  post_endpoint=AGENTS_ENDPOINT,
287
691
  get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
288
- json=payload,
692
+ json=payload_dict,
289
693
  )
290
694
  return Agent(**full_agent_data)._set_client(self)
291
695
 
292
- def update_agent(
696
+ def create_agent(
293
697
  self,
294
- agent_id: str,
295
698
  name: str | None = None,
296
699
  instruction: str | None = None,
297
700
  model: str | None = None,
701
+ tools: list[str | Any] | None = None,
702
+ agents: list[str | Any] | None = None,
703
+ timeout: int | None = None,
704
+ *,
705
+ file: str | PathLike[str] | None = None,
706
+ mcps: list[str | Any] | None = None,
707
+ tool_configs: Mapping[str, Any] | None = None,
298
708
  **kwargs: Any,
299
709
  ) -> "Agent":
300
- """Update an existing agent."""
301
- current_agent = self.get_agent_by_id(agent_id)
710
+ """Create a new agent, optionally loading configuration from a file."""
711
+ base_overrides = {
712
+ "name": name,
713
+ "instruction": instruction,
714
+ "model": model,
715
+ "tools": tools,
716
+ "agents": agents,
717
+ "timeout": timeout,
718
+ "mcps": mcps,
719
+ "tool_configs": tool_configs,
720
+ }
721
+ overrides = _merge_override_maps(base_overrides, kwargs)
722
+
723
+ if file is not None:
724
+ payload = _prepare_import_payload(
725
+ Path(file).expanduser(), overrides, drop_model_fields=True
726
+ )
727
+ if overrides.get("model") is None:
728
+ payload.pop("model", None)
729
+ else:
730
+ payload = overrides
731
+
732
+ return self._create_agent_from_payload(payload)
733
+
734
+ def create_agent_from_file( # pragma: no cover - thin compatibility wrapper
735
+ self,
736
+ file_path: str | PathLike[str],
737
+ **overrides: Any,
738
+ ) -> "Agent":
739
+ """Backward-compatible helper to create an agent from a configuration file."""
740
+ return self.create_agent(file=file_path, **overrides)
741
+
742
+ def _update_agent_from_payload(
743
+ self,
744
+ agent_id: str,
745
+ current_agent: Agent,
746
+ payload: Mapping[str, Any],
747
+ ) -> "Agent":
748
+ """Update an agent using a prepared payload mapping."""
749
+ known, extras = _split_known_and_extra(
750
+ payload, AgentUpdateRequest.__dataclass_fields__
751
+ )
752
+ _normalise_sequence_fields(known)
753
+
754
+ tool_refs = extras.pop("_tool_refs", None)
755
+ agent_refs = extras.pop("_agent_refs", None)
756
+ mcp_refs = extras.pop("_mcp_refs", None)
757
+
758
+ tools_value = known.pop("tools", None)
759
+ agents_value = known.pop("agents", None)
760
+ mcps_value = known.pop("mcps", None)
302
761
 
303
- language_model_id = kwargs.pop("language_model_id", None)
304
- provider_override = kwargs.pop("provider", None)
305
- model_name_override = kwargs.pop("model_name", None)
306
- agent_type_override = kwargs.pop("agent_type", kwargs.pop("type", None))
307
- framework_override = kwargs.pop("framework", None)
308
- version_override = kwargs.pop("version", None)
309
- account_id = kwargs.pop("account_id", None)
310
- description = kwargs.pop("description", None)
311
- metadata = kwargs.pop("metadata", None)
312
- tools = kwargs.pop("tools", None)
313
- tool_configs = kwargs.pop("tool_configs", None)
314
- agents_value = kwargs.pop("agents", None)
315
- mcps = kwargs.pop("mcps", None)
316
- agent_config = kwargs.pop("agent_config", None)
317
- a2a_profile = kwargs.pop("a2a_profile", None)
762
+ if tools_value is not None:
763
+ tools_value = self._resolve_tool_ids(tools_value, tool_refs)
764
+ if agents_value is not None:
765
+ agents_value = self._resolve_agent_ids(agents_value, agent_refs)
766
+ if mcps_value is not None:
767
+ mcps_value = self._resolve_mcp_ids(mcps_value, mcp_refs) # pragma: no cover
318
768
 
319
769
  request = AgentUpdateRequest(
320
- name=name,
321
- instruction=instruction,
322
- description=description,
323
- model=model,
324
- language_model_id=language_model_id,
325
- provider=provider_override,
326
- model_name=model_name_override,
327
- agent_type=agent_type_override,
328
- framework=framework_override,
329
- version=version_override,
330
- account_id=account_id,
331
- metadata=metadata,
332
- tools=tools,
333
- tool_configs=tool_configs,
770
+ name=known.pop("name", None),
771
+ instruction=known.pop("instruction", None),
772
+ description=known.pop("description", None),
773
+ model=known.pop("model", None),
774
+ language_model_id=known.pop("language_model_id", None),
775
+ provider=known.pop("provider", None),
776
+ model_name=known.pop("model_name", None),
777
+ agent_type=known.pop("agent_type", known.pop("type", None)),
778
+ framework=known.pop("framework", None),
779
+ version=known.pop("version", None),
780
+ account_id=known.pop("account_id", None),
781
+ metadata=known.pop("metadata", None),
782
+ tools=tools_value,
783
+ tool_configs=known.pop("tool_configs", None),
334
784
  agents=agents_value,
335
- mcps=mcps,
336
- agent_config=agent_config,
337
- a2a_profile=a2a_profile,
338
- extras=kwargs,
785
+ mcps=mcps_value,
786
+ agent_config=known.pop("agent_config", None),
787
+ a2a_profile=known.pop("a2a_profile", None),
788
+ extras={**known, **extras},
339
789
  )
340
790
 
341
- payload = request.to_payload(current_agent)
791
+ payload_dict = request.to_payload(current_agent)
342
792
 
343
- data = self._request("PUT", f"/agents/{agent_id}", json=payload)
344
- return Agent(**data)._set_client(self)
793
+ response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
794
+ return Agent(**response)._set_client(self)
795
+
796
+ def update_agent(
797
+ self,
798
+ agent_id: str,
799
+ name: str | None = None,
800
+ instruction: str | None = None,
801
+ model: str | None = None,
802
+ *,
803
+ file: str | PathLike[str] | None = None,
804
+ tools: list[str | Any] | None = None,
805
+ agents: list[str | Any] | None = None,
806
+ mcps: list[str | Any] | None = None,
807
+ **kwargs: Any,
808
+ ) -> "Agent":
809
+ """Update an existing agent."""
810
+ base_overrides = {
811
+ "name": name,
812
+ "instruction": instruction,
813
+ "model": model,
814
+ "tools": tools,
815
+ "agents": agents,
816
+ "mcps": mcps,
817
+ }
818
+ overrides = _merge_override_maps(base_overrides, kwargs)
819
+
820
+ if file is not None:
821
+ payload = _prepare_import_payload(
822
+ Path(file).expanduser(), overrides, drop_model_fields=True
823
+ )
824
+ else:
825
+ payload = overrides
826
+
827
+ current_agent = self.get_agent_by_id(agent_id)
828
+ return self._update_agent_from_payload(agent_id, current_agent, payload)
829
+
830
+ def update_agent_from_file( # pragma: no cover - thin compatibility wrapper
831
+ self,
832
+ agent_id: str,
833
+ file_path: str | PathLike[str],
834
+ **overrides: Any,
835
+ ) -> "Agent":
836
+ """Backward-compatible helper to update an agent from a configuration file."""
837
+ return self.update_agent(agent_id, file=file_path, **overrides)
345
838
 
346
839
  def delete_agent(self, agent_id: str) -> None:
347
840
  """Delete an agent."""