Skip to main content

claude_api/managed_agents/
agents.rs

1//! Agents: full CRUD + version history.
2//!
3//! An agent defines how Claude behaves within a session: model
4//! (optionally with `fast` inference speed), system prompt, MCP
5//! servers, skills, tools (built-in toolset + MCP toolsets + custom
6//! tools), description, metadata. Agents are versioned -- creating one
7//! starts at version 1 and any [`Agents::update`] increments the
8//! version. Sessions reference agents either at the latest version
9//! (string ID) or pinned to a specific version (see
10//! [`AgentRef`](super::sessions::AgentRef)).
11//!
12//! # Forward compatibility
13//!
14//! All wrapper enums in this module follow the
15//! `Known | Other(serde_json::Value)` pattern. Brand-new server
16//! variants -- a new permission policy, a new toolset shape, a new
17//! skill type -- deserialize into `Other` preserving the raw JSON, so
18//! upgrading the server without upgrading the SDK doesn't break parsing.
19
20use std::collections::HashMap;
21
22use serde::{Deserialize, Serialize};
23
24use crate::client::Client;
25use crate::error::Result;
26use crate::pagination::Paginated;
27
28use super::MANAGED_AGENTS_BETA;
29
30// =====================================================================
31// Model + speed
32// =====================================================================
33
34/// Inference speed mode. `Fast` charges premium pricing; not all models
35/// support it.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38#[non_exhaustive]
39pub enum ModelSpeed {
40    /// Default inference speed.
41    Standard,
42    /// Premium-priced fast inference.
43    Fast,
44}
45
46/// Model identifier. Wire form is either a bare string or a
47/// `{id, speed}` object.
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(untagged)]
50#[non_exhaustive]
51pub enum AgentModel {
52    /// Bare model string. Use this for the common case where you don't
53    /// need to override the inference speed.
54    String(String),
55    /// Object form with explicit speed.
56    Config {
57        /// Model ID (e.g. `claude-opus-4-7`).
58        id: String,
59        /// Inference speed.
60        #[serde(default, skip_serializing_if = "Option::is_none")]
61        speed: Option<ModelSpeed>,
62    },
63}
64
65impl AgentModel {
66    /// Bare model string (latest speed default).
67    #[must_use]
68    pub fn id(id: impl Into<String>) -> Self {
69        Self::String(id.into())
70    }
71
72    /// Model with an explicit speed override.
73    #[must_use]
74    pub fn config(id: impl Into<String>, speed: ModelSpeed) -> Self {
75        Self::Config {
76            id: id.into(),
77            speed: Some(speed),
78        }
79    }
80
81    /// Borrow the underlying model ID regardless of variant.
82    #[must_use]
83    pub fn model_id(&self) -> &str {
84        match self {
85            Self::String(s) => s,
86            Self::Config { id, .. } => id,
87        }
88    }
89}
90
91impl From<&str> for AgentModel {
92    fn from(s: &str) -> Self {
93        Self::String(s.to_owned())
94    }
95}
96
97impl From<String> for AgentModel {
98    fn from(s: String) -> Self {
99        Self::String(s)
100    }
101}
102
103impl From<crate::types::ModelId> for AgentModel {
104    fn from(m: crate::types::ModelId) -> Self {
105        Self::String(m.as_str().to_owned())
106    }
107}
108
109// =====================================================================
110// MCP server
111// =====================================================================
112
113/// MCP server reference on an agent.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(tag = "type", rename_all = "snake_case")]
116#[non_exhaustive]
117pub enum AgentMcpServer {
118    /// HTTP(S)-reachable MCP server.
119    Url {
120        /// Server name; referenced by `mcp_toolset` configs.
121        name: String,
122        /// Endpoint URL.
123        url: String,
124    },
125}
126
127impl AgentMcpServer {
128    /// Build a URL-typed MCP server reference.
129    #[must_use]
130    pub fn url(name: impl Into<String>, url: impl Into<String>) -> Self {
131        Self::Url {
132            name: name.into(),
133            url: url.into(),
134        }
135    }
136}
137
138// =====================================================================
139// Permission policies
140// =====================================================================
141
142/// Permission policy controlling whether tool calls require user
143/// confirmation. Forward-compatible: unknown policy types fall through
144/// to [`Self::Other`].
145#[derive(Debug, Clone, PartialEq)]
146pub enum PermissionPolicy {
147    /// Auto-approve every invocation.
148    AlwaysAllow,
149    /// Require a `user.tool_confirmation` event before each invocation.
150    AlwaysAsk,
151    /// Unknown policy type; raw JSON preserved.
152    Other(serde_json::Value),
153}
154
155const KNOWN_PERMISSION_POLICY_TAGS: &[&str] = &["always_allow", "always_ask"];
156
157impl Serialize for PermissionPolicy {
158    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
159        use serde::ser::SerializeMap;
160        match self {
161            Self::AlwaysAllow => {
162                let mut map = s.serialize_map(Some(1))?;
163                map.serialize_entry("type", "always_allow")?;
164                map.end()
165            }
166            Self::AlwaysAsk => {
167                let mut map = s.serialize_map(Some(1))?;
168                map.serialize_entry("type", "always_ask")?;
169                map.end()
170            }
171            Self::Other(v) => v.serialize(s),
172        }
173    }
174}
175
176impl<'de> Deserialize<'de> for PermissionPolicy {
177    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
178        let raw = serde_json::Value::deserialize(d)?;
179        let tag = raw.get("type").and_then(serde_json::Value::as_str);
180        match tag {
181            Some("always_allow") if KNOWN_PERMISSION_POLICY_TAGS.contains(&"always_allow") => {
182                Ok(Self::AlwaysAllow)
183            }
184            Some("always_ask") => Ok(Self::AlwaysAsk),
185            _ => Ok(Self::Other(raw)),
186        }
187    }
188}
189
190// =====================================================================
191// Toolset configs
192// =====================================================================
193
194/// Built-in agent tool identifier.
195#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
196#[serde(rename_all = "snake_case")]
197#[non_exhaustive]
198pub enum BuiltinToolName {
199    /// Shell.
200    #[default]
201    Bash,
202    /// File edit.
203    Edit,
204    /// File read.
205    Read,
206    /// File write.
207    Write,
208    /// Glob match.
209    Glob,
210    /// Grep search.
211    Grep,
212    /// Fetch a URL.
213    WebFetch,
214    /// Web search.
215    WebSearch,
216}
217
218/// Per-tool override on a built-in toolset.
219#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
220#[non_exhaustive]
221pub struct BuiltinToolConfig {
222    /// Tool identifier.
223    pub name: BuiltinToolName,
224    /// Whether this tool is enabled. Overrides the toolset default.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub enabled: Option<bool>,
227    /// Permission policy. Overrides the toolset default.
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub permission_policy: Option<PermissionPolicy>,
230}
231
232/// Per-tool override on an MCP toolset.
233#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
234#[non_exhaustive]
235pub struct McpToolConfig {
236    /// MCP tool name.
237    pub name: String,
238    /// Whether this tool is enabled. Overrides the toolset default.
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub enabled: Option<bool>,
241    /// Permission policy. Overrides the toolset default.
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub permission_policy: Option<PermissionPolicy>,
244}
245
246/// Default configuration for all tools in a toolset.
247#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
248#[non_exhaustive]
249pub struct ToolsetDefaultConfig {
250    /// Whether tools are enabled by default. Defaults to `true`
251    /// server-side when omitted.
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub enabled: Option<bool>,
254    /// Default permission policy.
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub permission_policy: Option<PermissionPolicy>,
257}
258
259/// JSON Schema for a custom tool's input parameters.
260#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
261#[non_exhaustive]
262pub struct CustomToolInputSchema {
263    /// JSON Schema `properties` map.
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub properties: Option<serde_json::Value>,
266    /// Required property names.
267    #[serde(default, skip_serializing_if = "Vec::is_empty")]
268    pub required: Vec<String>,
269    /// Always `"object"`.
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    #[serde(rename = "type")]
272    pub ty: Option<String>,
273}
274
275/// Custom tool executed by the API client (not the agent). Calls
276/// surface as `agent.custom_tool_use` events; respond with
277/// `user.custom_tool_result`.
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279#[non_exhaustive]
280pub struct CustomTool {
281    /// Tool name. 1-128 chars; letters, digits, underscores, hyphens.
282    pub name: String,
283    /// Description shown to the agent. 1-1024 chars.
284    pub description: String,
285    /// Input JSON Schema.
286    pub input_schema: CustomToolInputSchema,
287}
288
289// =====================================================================
290// AgentTool wrapper
291// =====================================================================
292
293/// One tool entry on an agent.
294///
295/// Forward-compatible: unknown wire `type` tags fall through to
296/// [`Self::Other`] preserving the raw JSON.
297#[derive(Debug, Clone, PartialEq)]
298pub enum AgentTool {
299    /// Pre-built `agent_toolset_20260401` (bash / edit / read / write /
300    /// glob / grep / `web_fetch` / `web_search`) with optional per-tool
301    /// overrides and a default config.
302    BuiltinToolset(BuiltinToolset),
303    /// MCP toolset bound to a server name from the agent's
304    /// `mcp_servers` array.
305    McpToolset(McpToolset),
306    /// Custom client-executed tool.
307    Custom(CustomTool),
308    /// Unknown tool kind; raw JSON preserved.
309    Other(serde_json::Value),
310}
311
312/// Body of [`AgentTool::BuiltinToolset`].
313#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
314#[non_exhaustive]
315pub struct BuiltinToolset {
316    /// Per-tool overrides.
317    #[serde(default, skip_serializing_if = "Vec::is_empty")]
318    pub configs: Vec<BuiltinToolConfig>,
319    /// Default config applied to tools not overridden in `configs`.
320    #[serde(default, skip_serializing_if = "Option::is_none")]
321    pub default_config: Option<ToolsetDefaultConfig>,
322}
323
324/// Body of [`AgentTool::McpToolset`].
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326#[non_exhaustive]
327pub struct McpToolset {
328    /// MCP server name from the agent's `mcp_servers` array.
329    pub mcp_server_name: String,
330    /// Per-tool overrides.
331    #[serde(default, skip_serializing_if = "Vec::is_empty")]
332    pub configs: Vec<McpToolConfig>,
333    /// Default config applied to tools not overridden in `configs`.
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub default_config: Option<ToolsetDefaultConfig>,
336}
337
338const KNOWN_AGENT_TOOL_TAGS: &[&str] = &["agent_toolset_20260401", "mcp_toolset", "custom"];
339
340impl Serialize for AgentTool {
341    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
342        use serde::ser::SerializeMap;
343        match self {
344            Self::BuiltinToolset(b) => {
345                let mut map = s.serialize_map(None)?;
346                map.serialize_entry("type", "agent_toolset_20260401")?;
347                if !b.configs.is_empty() {
348                    map.serialize_entry("configs", &b.configs)?;
349                }
350                if let Some(d) = &b.default_config {
351                    map.serialize_entry("default_config", d)?;
352                }
353                map.end()
354            }
355            Self::McpToolset(m) => {
356                let mut map = s.serialize_map(None)?;
357                map.serialize_entry("type", "mcp_toolset")?;
358                map.serialize_entry("mcp_server_name", &m.mcp_server_name)?;
359                if !m.configs.is_empty() {
360                    map.serialize_entry("configs", &m.configs)?;
361                }
362                if let Some(d) = &m.default_config {
363                    map.serialize_entry("default_config", d)?;
364                }
365                map.end()
366            }
367            Self::Custom(c) => {
368                let mut map = s.serialize_map(None)?;
369                map.serialize_entry("type", "custom")?;
370                map.serialize_entry("name", &c.name)?;
371                map.serialize_entry("description", &c.description)?;
372                map.serialize_entry("input_schema", &c.input_schema)?;
373                map.end()
374            }
375            Self::Other(v) => v.serialize(s),
376        }
377    }
378}
379
380impl<'de> Deserialize<'de> for AgentTool {
381    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
382        let raw = serde_json::Value::deserialize(d)?;
383        let tag = raw.get("type").and_then(serde_json::Value::as_str);
384        match tag {
385            Some("agent_toolset_20260401")
386                if KNOWN_AGENT_TOOL_TAGS.contains(&"agent_toolset_20260401") =>
387            {
388                let b = serde_json::from_value::<BuiltinToolset>(raw)
389                    .map_err(serde::de::Error::custom)?;
390                Ok(Self::BuiltinToolset(b))
391            }
392            Some("mcp_toolset") => {
393                let m =
394                    serde_json::from_value::<McpToolset>(raw).map_err(serde::de::Error::custom)?;
395                Ok(Self::McpToolset(m))
396            }
397            Some("custom") => {
398                let c =
399                    serde_json::from_value::<CustomTool>(raw).map_err(serde::de::Error::custom)?;
400                Ok(Self::Custom(c))
401            }
402            _ => Ok(Self::Other(raw)),
403        }
404    }
405}
406
407impl AgentTool {
408    /// Enable the pre-built `agent_toolset_20260401` (no overrides).
409    #[must_use]
410    pub fn builtin_toolset() -> Self {
411        Self::BuiltinToolset(BuiltinToolset::default())
412    }
413
414    /// Expose tools from a named MCP server.
415    #[must_use]
416    pub fn mcp_toolset(server_name: impl Into<String>) -> Self {
417        Self::McpToolset(McpToolset {
418            mcp_server_name: server_name.into(),
419            configs: Vec::new(),
420            default_config: None,
421        })
422    }
423
424    /// Build a custom tool.
425    #[must_use]
426    pub fn custom(
427        name: impl Into<String>,
428        description: impl Into<String>,
429        input_schema: CustomToolInputSchema,
430    ) -> Self {
431        Self::Custom(CustomTool {
432            name: name.into(),
433            description: description.into(),
434            input_schema,
435        })
436    }
437}
438
439// =====================================================================
440// Skills
441// =====================================================================
442
443/// A skill referenced on an agent. Forward-compatible: unknown skill
444/// types fall through to [`Self::Other`].
445#[derive(Debug, Clone, PartialEq)]
446pub enum Skill {
447    /// Anthropic-managed skill.
448    Anthropic(AnthropicSkill),
449    /// User-created custom skill.
450    Custom(CustomSkill),
451    /// Unknown skill type; raw JSON preserved.
452    Other(serde_json::Value),
453}
454
455/// Body of [`Skill::Anthropic`].
456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457#[non_exhaustive]
458pub struct AnthropicSkill {
459    /// Skill ID (e.g. `"xlsx"`).
460    pub skill_id: String,
461    /// Pinned version. Defaults to latest if omitted on requests; the
462    /// resolved version is always echoed on responses.
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    pub version: Option<String>,
465}
466
467/// Body of [`Skill::Custom`].
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469#[non_exhaustive]
470pub struct CustomSkill {
471    /// Skill ID (e.g. `"skill_01XJ5..."`).
472    pub skill_id: String,
473    /// Pinned version. Defaults to latest if omitted.
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub version: Option<String>,
476}
477
478const KNOWN_SKILL_TAGS: &[&str] = &["anthropic", "custom"];
479
480impl Serialize for Skill {
481    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
482        use serde::ser::SerializeMap;
483        match self {
484            Self::Anthropic(a) => {
485                let mut map = s.serialize_map(None)?;
486                map.serialize_entry("type", "anthropic")?;
487                map.serialize_entry("skill_id", &a.skill_id)?;
488                if let Some(v) = &a.version {
489                    map.serialize_entry("version", v)?;
490                }
491                map.end()
492            }
493            Self::Custom(c) => {
494                let mut map = s.serialize_map(None)?;
495                map.serialize_entry("type", "custom")?;
496                map.serialize_entry("skill_id", &c.skill_id)?;
497                if let Some(v) = &c.version {
498                    map.serialize_entry("version", v)?;
499                }
500                map.end()
501            }
502            Self::Other(v) => v.serialize(s),
503        }
504    }
505}
506
507impl<'de> Deserialize<'de> for Skill {
508    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
509        let raw = serde_json::Value::deserialize(d)?;
510        let tag = raw.get("type").and_then(serde_json::Value::as_str);
511        match tag {
512            Some("anthropic") if KNOWN_SKILL_TAGS.contains(&"anthropic") => {
513                let a = serde_json::from_value::<AnthropicSkill>(raw)
514                    .map_err(serde::de::Error::custom)?;
515                Ok(Self::Anthropic(a))
516            }
517            Some("custom") => {
518                let c =
519                    serde_json::from_value::<CustomSkill>(raw).map_err(serde::de::Error::custom)?;
520                Ok(Self::Custom(c))
521            }
522            _ => Ok(Self::Other(raw)),
523        }
524    }
525}
526
527impl Skill {
528    /// Reference an Anthropic-managed skill (latest version).
529    #[must_use]
530    pub fn anthropic(skill_id: impl Into<String>) -> Self {
531        Self::Anthropic(AnthropicSkill {
532            skill_id: skill_id.into(),
533            version: None,
534        })
535    }
536
537    /// Reference an Anthropic-managed skill pinned to a version.
538    #[must_use]
539    pub fn anthropic_pinned(skill_id: impl Into<String>, version: impl Into<String>) -> Self {
540        Self::Anthropic(AnthropicSkill {
541            skill_id: skill_id.into(),
542            version: Some(version.into()),
543        })
544    }
545
546    /// Reference a user-created custom skill (latest version).
547    #[must_use]
548    pub fn custom(skill_id: impl Into<String>) -> Self {
549        Self::Custom(CustomSkill {
550            skill_id: skill_id.into(),
551            version: None,
552        })
553    }
554}
555
556// =====================================================================
557// Callable agents (multi-agent / threads)
558// =====================================================================
559
560/// Reference to another agent that this agent is permitted to call.
561///
562/// Used in [`CreateAgentRequest::callable_agents`] and
563/// [`UpdateAgentRequest::callable_agents`] when configuring a
564/// multi-agent coordinator. At runtime, when the coordinator delegates
565/// to one of these, the platform spawns a new
566/// [`Thread`](crate::managed_agents::threads::Thread).
567///
568/// Wire form is `{type: "agent", id, version}` -- the same shape as
569/// the pinned [`AgentRef`](super::sessions::AgentRef).
570#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
571#[non_exhaustive]
572pub struct CallableAgent {
573    /// Always `"agent"`.
574    #[serde(rename = "type")]
575    pub ty: String,
576    /// Agent ID.
577    pub id: String,
578    /// Pinned version. Required.
579    pub version: u32,
580}
581
582impl CallableAgent {
583    /// Build a callable-agent reference pinned to a version.
584    #[must_use]
585    pub fn new(id: impl Into<String>, version: u32) -> Self {
586        Self {
587            ty: "agent".into(),
588            id: id.into(),
589            version,
590        }
591    }
592}
593
594// =====================================================================
595// Agent (response shape)
596// =====================================================================
597
598/// An agent definition.
599#[derive(Debug, Clone, Serialize, Deserialize)]
600#[non_exhaustive]
601pub struct Agent {
602    /// Stable identifier (`agent_...`).
603    pub id: String,
604    /// Wire type tag (`"agent"`).
605    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
606    pub ty: Option<String>,
607    /// Agent name.
608    pub name: String,
609    /// Description. May be `null` on the wire when no description was
610    /// provided at create time.
611    #[serde(default, skip_serializing_if = "Option::is_none")]
612    pub description: Option<String>,
613    /// Model identifier and configuration.
614    pub model: AgentModel,
615    /// System prompt. May be `null` on the wire when no system prompt
616    /// was provided at create time.
617    #[serde(default, skip_serializing_if = "Option::is_none")]
618    pub system: Option<String>,
619    /// Configured MCP servers.
620    #[serde(default)]
621    pub mcp_servers: Vec<AgentMcpServer>,
622    /// Configured skills.
623    #[serde(default)]
624    pub skills: Vec<Skill>,
625    /// Configured tools.
626    #[serde(default)]
627    pub tools: Vec<AgentTool>,
628    /// Free-form metadata.
629    #[serde(default)]
630    pub metadata: HashMap<String, String>,
631    /// Other agents this agent is permitted to call (multi-agent
632    /// coordinators only). Empty for non-coordinator agents.
633    #[serde(default)]
634    pub callable_agents: Vec<CallableAgent>,
635    /// Current version. Starts at 1, increments on every update.
636    pub version: u32,
637    /// Creation timestamp (RFC3339).
638    pub created_at: String,
639    /// Last-modified timestamp (RFC3339).
640    pub updated_at: String,
641    /// Set when archived (RFC3339), `None` for live agents.
642    #[serde(default)]
643    pub archived_at: Option<String>,
644}
645
646// =====================================================================
647// Create request
648// =====================================================================
649
650/// Request body for `POST /v1/agents`.
651#[derive(Debug, Clone, Serialize)]
652#[non_exhaustive]
653pub struct CreateAgentRequest {
654    /// Agent name. 1-256 chars.
655    pub name: String,
656    /// Model.
657    pub model: AgentModel,
658    /// Description. Up to 2048 chars.
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub description: Option<String>,
661    /// System prompt. Up to 100,000 chars.
662    #[serde(skip_serializing_if = "Option::is_none")]
663    pub system: Option<String>,
664    /// MCP servers. Max 20.
665    #[serde(skip_serializing_if = "Vec::is_empty")]
666    pub mcp_servers: Vec<AgentMcpServer>,
667    /// Skills. Max 20.
668    #[serde(skip_serializing_if = "Vec::is_empty")]
669    pub skills: Vec<Skill>,
670    /// Tools. Max 128 across all toolsets.
671    #[serde(skip_serializing_if = "Vec::is_empty")]
672    pub tools: Vec<AgentTool>,
673    /// Metadata. Max 16 pairs (64-char keys, 512-char values).
674    #[serde(skip_serializing_if = "HashMap::is_empty")]
675    pub metadata: HashMap<String, String>,
676    /// Other agents this agent is permitted to call. Setting this
677    /// makes the agent a multi-agent coordinator: at runtime,
678    /// delegations spawn new
679    /// [`Thread`](crate::managed_agents::threads::Thread)s. Only one
680    /// level of delegation is supported -- callable agents cannot
681    /// themselves have callable agents.
682    #[serde(skip_serializing_if = "Vec::is_empty")]
683    pub callable_agents: Vec<CallableAgent>,
684}
685
686impl CreateAgentRequest {
687    /// Begin configuring a request.
688    #[must_use]
689    pub fn builder() -> CreateAgentRequestBuilder {
690        CreateAgentRequestBuilder::default()
691    }
692}
693
694/// Builder for [`CreateAgentRequest`].
695#[derive(Debug, Default)]
696pub struct CreateAgentRequestBuilder {
697    name: Option<String>,
698    model: Option<AgentModel>,
699    description: Option<String>,
700    system: Option<String>,
701    mcp_servers: Vec<AgentMcpServer>,
702    skills: Vec<Skill>,
703    tools: Vec<AgentTool>,
704    metadata: HashMap<String, String>,
705    callable_agents: Vec<CallableAgent>,
706}
707
708impl CreateAgentRequestBuilder {
709    /// Set the name. Required.
710    #[must_use]
711    pub fn name(mut self, name: impl Into<String>) -> Self {
712        self.name = Some(name.into());
713        self
714    }
715
716    /// Set the model. Required.
717    #[must_use]
718    pub fn model(mut self, model: impl Into<AgentModel>) -> Self {
719        self.model = Some(model.into());
720        self
721    }
722
723    /// Set the description.
724    #[must_use]
725    pub fn description(mut self, description: impl Into<String>) -> Self {
726        self.description = Some(description.into());
727        self
728    }
729
730    /// Set the system prompt.
731    #[must_use]
732    pub fn system(mut self, system: impl Into<String>) -> Self {
733        self.system = Some(system.into());
734        self
735    }
736
737    /// Append an MCP server.
738    #[must_use]
739    pub fn mcp_server(mut self, server: AgentMcpServer) -> Self {
740        self.mcp_servers.push(server);
741        self
742    }
743
744    /// Append a skill.
745    #[must_use]
746    pub fn skill(mut self, skill: Skill) -> Self {
747        self.skills.push(skill);
748        self
749    }
750
751    /// Append a tool.
752    #[must_use]
753    pub fn tool(mut self, tool: AgentTool) -> Self {
754        self.tools.push(tool);
755        self
756    }
757
758    /// Insert a metadata entry.
759    #[must_use]
760    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
761        self.metadata.insert(key.into(), value.into());
762        self
763    }
764
765    /// Append a callable-agent reference (for multi-agent coordinators).
766    #[must_use]
767    pub fn callable_agent(mut self, callable: CallableAgent) -> Self {
768        self.callable_agents.push(callable);
769        self
770    }
771
772    /// Finalize.
773    ///
774    /// # Errors
775    ///
776    /// Returns [`Error::InvalidConfig`](crate::Error::InvalidConfig)
777    /// if `name` or `model` was not set.
778    pub fn build(self) -> Result<CreateAgentRequest> {
779        let name = self
780            .name
781            .ok_or_else(|| crate::Error::InvalidConfig("name is required".into()))?;
782        let model = self
783            .model
784            .ok_or_else(|| crate::Error::InvalidConfig("model is required".into()))?;
785        Ok(CreateAgentRequest {
786            name,
787            model,
788            description: self.description,
789            system: self.system,
790            mcp_servers: self.mcp_servers,
791            skills: self.skills,
792            tools: self.tools,
793            metadata: self.metadata,
794            callable_agents: self.callable_agents,
795        })
796    }
797}
798
799// =====================================================================
800// Update request
801// =====================================================================
802
803/// Request body for `POST /v1/agents/{id}` (update).
804///
805/// **Optimistic concurrency**: pass the agent's current `version`. The
806/// request is rejected if the server's stored version no longer
807/// matches.
808///
809/// **Field semantics**:
810/// - `Option::None` → omit the field → preserve the existing value.
811/// - `Option::Some` with empty string / empty array / `null` → clear,
812///   except for `name` and `model` which cannot be cleared.
813/// - For `metadata`, see [`MetadataPatch`] for per-key delete semantics.
814#[derive(Debug, Clone, Default, Serialize)]
815#[non_exhaustive]
816pub struct UpdateAgentRequest {
817    /// Current version, used for optimistic concurrency. Required.
818    pub version: u32,
819    /// New name (cannot be cleared).
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub name: Option<String>,
822    /// New model (cannot be cleared).
823    #[serde(skip_serializing_if = "Option::is_none")]
824    pub model: Option<AgentModel>,
825    /// New description. `Some("")` clears.
826    #[serde(skip_serializing_if = "Option::is_none")]
827    pub description: Option<String>,
828    /// New system prompt. `Some("")` clears.
829    #[serde(skip_serializing_if = "Option::is_none")]
830    pub system: Option<String>,
831    /// Replacement MCP-servers list. `Some(vec![])` clears.
832    #[serde(skip_serializing_if = "Option::is_none")]
833    pub mcp_servers: Option<Vec<AgentMcpServer>>,
834    /// Replacement skills list. `Some(vec![])` clears.
835    #[serde(skip_serializing_if = "Option::is_none")]
836    pub skills: Option<Vec<Skill>>,
837    /// Replacement tools list. `Some(vec![])` clears.
838    #[serde(skip_serializing_if = "Option::is_none")]
839    pub tools: Option<Vec<AgentTool>>,
840    /// Per-key metadata patch. See [`MetadataPatch`].
841    #[serde(skip_serializing_if = "Option::is_none")]
842    pub metadata: Option<MetadataPatch>,
843    /// Replacement callable-agents list. `Some(vec![])` clears (turns
844    /// the agent back into a non-coordinator).
845    #[serde(skip_serializing_if = "Option::is_none")]
846    pub callable_agents: Option<Vec<CallableAgent>>,
847}
848
849impl UpdateAgentRequest {
850    /// Build a minimal update request (pass `version`, then chain
851    /// setters).
852    #[must_use]
853    pub fn at_version(version: u32) -> Self {
854        Self {
855            version,
856            ..Self::default()
857        }
858    }
859
860    /// Set the name.
861    #[must_use]
862    pub fn name(mut self, name: impl Into<String>) -> Self {
863        self.name = Some(name.into());
864        self
865    }
866
867    /// Set the model.
868    #[must_use]
869    pub fn model(mut self, model: impl Into<AgentModel>) -> Self {
870        self.model = Some(model.into());
871        self
872    }
873
874    /// Set or clear (`Some("")`) the description.
875    #[must_use]
876    pub fn description(mut self, description: impl Into<String>) -> Self {
877        self.description = Some(description.into());
878        self
879    }
880
881    /// Set or clear (`Some("")`) the system prompt.
882    #[must_use]
883    pub fn system(mut self, system: impl Into<String>) -> Self {
884        self.system = Some(system.into());
885        self
886    }
887
888    /// Replace the MCP servers list. Pass an empty Vec to clear.
889    #[must_use]
890    pub fn mcp_servers(mut self, servers: Vec<AgentMcpServer>) -> Self {
891        self.mcp_servers = Some(servers);
892        self
893    }
894
895    /// Replace the skills list. Pass an empty Vec to clear.
896    #[must_use]
897    pub fn skills(mut self, skills: Vec<Skill>) -> Self {
898        self.skills = Some(skills);
899        self
900    }
901
902    /// Replace the tools list. Pass an empty Vec to clear.
903    #[must_use]
904    pub fn tools(mut self, tools: Vec<AgentTool>) -> Self {
905        self.tools = Some(tools);
906        self
907    }
908
909    /// Apply a metadata patch.
910    #[must_use]
911    pub fn metadata(mut self, patch: MetadataPatch) -> Self {
912        self.metadata = Some(patch);
913        self
914    }
915
916    /// Replace the callable-agents list. Pass an empty Vec to clear.
917    #[must_use]
918    pub fn callable_agents(mut self, callable: Vec<CallableAgent>) -> Self {
919        self.callable_agents = Some(callable);
920        self
921    }
922}
923
924/// Metadata patch on [`UpdateAgentRequest`]. Each entry is either a
925/// `String` (upsert that key to the given value) or `None` (delete that
926/// key). Keys not present in the patch are preserved.
927#[derive(Debug, Clone, Default, Serialize)]
928pub struct MetadataPatch(pub HashMap<String, Option<String>>);
929
930impl MetadataPatch {
931    /// Empty patch.
932    #[must_use]
933    pub fn new() -> Self {
934        Self::default()
935    }
936
937    /// Upsert a key to a value.
938    #[must_use]
939    pub fn set(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
940        self.0.insert(key.into(), Some(value.into()));
941        self
942    }
943
944    /// Delete a key.
945    #[must_use]
946    pub fn delete(mut self, key: impl Into<String>) -> Self {
947        self.0.insert(key.into(), None);
948        self
949    }
950}
951
952// =====================================================================
953// List params
954// =====================================================================
955
956/// Optional knobs for [`Agents::list`].
957#[derive(Debug, Clone, Default)]
958#[non_exhaustive]
959pub struct ListAgentsParams {
960    /// Created at or after this RFC3339 time.
961    pub created_at_gte: Option<String>,
962    /// Created at or before this RFC3339 time.
963    pub created_at_lte: Option<String>,
964    /// Include archived agents. Defaults to `false`.
965    pub include_archived: Option<bool>,
966    /// Page size. Default 20, max 100.
967    pub limit: Option<u32>,
968    /// Pagination cursor from a previous response's `next_page`.
969    pub page: Option<String>,
970}
971
972impl ListAgentsParams {
973    fn to_query(&self) -> Vec<(&'static str, String)> {
974        let mut q = Vec::new();
975        if let Some(t) = &self.created_at_gte {
976            q.push(("created_at[gte]", t.clone()));
977        }
978        if let Some(t) = &self.created_at_lte {
979            q.push(("created_at[lte]", t.clone()));
980        }
981        if let Some(b) = self.include_archived {
982            q.push(("include_archived", b.to_string()));
983        }
984        if let Some(l) = self.limit {
985            q.push(("limit", l.to_string()));
986        }
987        if let Some(p) = &self.page {
988            q.push(("page", p.clone()));
989        }
990        q
991    }
992}
993
994/// Optional knobs for [`Agents::list_versions`].
995#[derive(Debug, Clone, Default)]
996#[non_exhaustive]
997pub struct ListAgentVersionsParams {
998    /// Page size. Default 20, max 100.
999    pub limit: Option<u32>,
1000    /// Pagination cursor.
1001    pub page: Option<String>,
1002}
1003
1004impl ListAgentVersionsParams {
1005    fn to_query(&self) -> Vec<(&'static str, String)> {
1006        let mut q = Vec::new();
1007        if let Some(l) = self.limit {
1008            q.push(("limit", l.to_string()));
1009        }
1010        if let Some(p) = &self.page {
1011            q.push(("page", p.clone()));
1012        }
1013        q
1014    }
1015}
1016
1017// =====================================================================
1018// Namespace handle
1019// =====================================================================
1020
1021/// Namespace handle for the Agents API.
1022pub struct Agents<'a> {
1023    client: &'a Client,
1024}
1025
1026impl<'a> Agents<'a> {
1027    pub(crate) fn new(client: &'a Client) -> Self {
1028        Self { client }
1029    }
1030
1031    /// `POST /v1/agents`.
1032    pub async fn create(&self, request: CreateAgentRequest) -> Result<Agent> {
1033        let body = &request;
1034        self.client
1035            .execute_with_retry(
1036                || {
1037                    self.client
1038                        .request_builder(reqwest::Method::POST, "/v1/agents")
1039                        .json(body)
1040                },
1041                &[MANAGED_AGENTS_BETA],
1042            )
1043            .await
1044    }
1045
1046    /// `GET /v1/agents/{id}`. Pass `version = Some(n)` to retrieve a
1047    /// specific historical version; `None` returns the latest.
1048    pub async fn retrieve(&self, agent_id: &str, version: Option<u32>) -> Result<Agent> {
1049        let path = format!("/v1/agents/{agent_id}");
1050        let v = version;
1051        self.client
1052            .execute_with_retry(
1053                || {
1054                    let mut req = self.client.request_builder(reqwest::Method::GET, &path);
1055                    if let Some(version) = v {
1056                        req = req.query(&[("version", version.to_string())]);
1057                    }
1058                    req
1059                },
1060                &[MANAGED_AGENTS_BETA],
1061            )
1062            .await
1063    }
1064
1065    /// `GET /v1/agents`.
1066    pub async fn list(&self, params: ListAgentsParams) -> Result<Paginated<Agent>> {
1067        let query = params.to_query();
1068        self.client
1069            .execute_with_retry(
1070                || {
1071                    let mut req = self
1072                        .client
1073                        .request_builder(reqwest::Method::GET, "/v1/agents");
1074                    for (k, v) in &query {
1075                        req = req.query(&[(k, v)]);
1076                    }
1077                    req
1078                },
1079                &[MANAGED_AGENTS_BETA],
1080            )
1081            .await
1082    }
1083
1084    /// `POST /v1/agents/{id}` (update). Bumps the version on success.
1085    /// Fails with HTTP 409 if `request.version` doesn't match the
1086    /// server's current version.
1087    pub async fn update(&self, agent_id: &str, request: UpdateAgentRequest) -> Result<Agent> {
1088        let path = format!("/v1/agents/{agent_id}");
1089        let body = &request;
1090        self.client
1091            .execute_with_retry(
1092                || {
1093                    self.client
1094                        .request_builder(reqwest::Method::POST, &path)
1095                        .json(body)
1096                },
1097                &[MANAGED_AGENTS_BETA],
1098            )
1099            .await
1100    }
1101
1102    /// `POST /v1/agents/{id}/archive`.
1103    pub async fn archive(&self, agent_id: &str) -> Result<Agent> {
1104        let path = format!("/v1/agents/{agent_id}/archive");
1105        self.client
1106            .execute_with_retry(
1107                || self.client.request_builder(reqwest::Method::POST, &path),
1108                &[MANAGED_AGENTS_BETA],
1109            )
1110            .await
1111    }
1112
1113    /// `GET /v1/agents/{id}/versions`. Returns the agent's full version
1114    /// history, newest first.
1115    pub async fn list_versions(
1116        &self,
1117        agent_id: &str,
1118        params: ListAgentVersionsParams,
1119    ) -> Result<Paginated<Agent>> {
1120        let path = format!("/v1/agents/{agent_id}/versions");
1121        let query = params.to_query();
1122        self.client
1123            .execute_with_retry(
1124                || {
1125                    let mut req = self.client.request_builder(reqwest::Method::GET, &path);
1126                    for (k, v) in &query {
1127                        req = req.query(&[(k, v)]);
1128                    }
1129                    req
1130                },
1131                &[MANAGED_AGENTS_BETA],
1132            )
1133            .await
1134    }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use super::*;
1140    use pretty_assertions::assert_eq;
1141    use serde_json::json;
1142    use wiremock::matchers::{body_partial_json, method, path};
1143    use wiremock::{Mock, MockServer, ResponseTemplate};
1144
1145    fn client_for(mock: &MockServer) -> Client {
1146        Client::builder()
1147            .api_key("sk-ant-test")
1148            .base_url(mock.uri())
1149            .build()
1150            .unwrap()
1151    }
1152
1153    fn fake_agent_response() -> serde_json::Value {
1154        json!({
1155            "id": "agent_01",
1156            "type": "agent",
1157            "name": "Reviewer",
1158            "description": "",
1159            "model": "claude-opus-4-7",
1160            "system": "",
1161            "mcp_servers": [],
1162            "skills": [],
1163            "tools": [],
1164            "metadata": {},
1165            "version": 1,
1166            "created_at": "2026-04-30T12:00:00Z",
1167            "updated_at": "2026-04-30T12:00:00Z"
1168        })
1169    }
1170
1171    #[test]
1172    fn agent_model_serializes_string_form_untagged() {
1173        let m = AgentModel::id("claude-opus-4-7");
1174        let v = serde_json::to_value(&m).unwrap();
1175        assert_eq!(v, json!("claude-opus-4-7"));
1176    }
1177
1178    #[test]
1179    fn agent_model_serializes_config_form_with_speed() {
1180        let m = AgentModel::config("claude-opus-4-7", ModelSpeed::Fast);
1181        let v = serde_json::to_value(&m).unwrap();
1182        assert_eq!(v, json!({"id": "claude-opus-4-7", "speed": "fast"}));
1183        let parsed: AgentModel = serde_json::from_value(v).unwrap();
1184        assert_eq!(parsed, m);
1185    }
1186
1187    #[test]
1188    fn permission_policy_round_trips_known_variants() {
1189        assert_eq!(
1190            serde_json::to_value(PermissionPolicy::AlwaysAllow).unwrap(),
1191            json!({"type": "always_allow"})
1192        );
1193        assert_eq!(
1194            serde_json::to_value(PermissionPolicy::AlwaysAsk).unwrap(),
1195            json!({"type": "always_ask"})
1196        );
1197    }
1198
1199    #[test]
1200    fn permission_policy_unknown_variant_falls_to_other() {
1201        let raw = json!({"type": "future_policy", "x": 1});
1202        let parsed: PermissionPolicy = serde_json::from_value(raw.clone()).unwrap();
1203        match parsed {
1204            PermissionPolicy::Other(v) => assert_eq!(v, raw),
1205            PermissionPolicy::AlwaysAllow | PermissionPolicy::AlwaysAsk => panic!("expected Other"),
1206        }
1207    }
1208
1209    #[test]
1210    fn agent_tool_builtin_toolset_serializes_with_configs() {
1211        let tool = AgentTool::BuiltinToolset(BuiltinToolset {
1212            configs: vec![BuiltinToolConfig {
1213                name: BuiltinToolName::Bash,
1214                enabled: Some(true),
1215                permission_policy: Some(PermissionPolicy::AlwaysAsk),
1216            }],
1217            default_config: Some(ToolsetDefaultConfig {
1218                enabled: Some(true),
1219                permission_policy: Some(PermissionPolicy::AlwaysAllow),
1220            }),
1221        });
1222        let v = serde_json::to_value(&tool).unwrap();
1223        assert_eq!(v["type"], "agent_toolset_20260401");
1224        assert_eq!(v["configs"][0]["name"], "bash");
1225        assert_eq!(v["configs"][0]["permission_policy"]["type"], "always_ask");
1226        assert_eq!(v["default_config"]["enabled"], true);
1227    }
1228
1229    #[test]
1230    fn agent_tool_mcp_toolset_round_trips_with_server_name() {
1231        let tool = AgentTool::mcp_toolset("github");
1232        let v = serde_json::to_value(&tool).unwrap();
1233        assert_eq!(
1234            v,
1235            json!({"type": "mcp_toolset", "mcp_server_name": "github"})
1236        );
1237        let parsed: AgentTool = serde_json::from_value(v).unwrap();
1238        assert_eq!(parsed, tool);
1239    }
1240
1241    #[test]
1242    fn agent_tool_custom_round_trips_with_input_schema() {
1243        let tool = AgentTool::custom(
1244            "lookup",
1245            "Find a record by id",
1246            CustomToolInputSchema {
1247                properties: Some(json!({"id": {"type": "string"}})),
1248                required: vec!["id".into()],
1249                ty: Some("object".into()),
1250            },
1251        );
1252        let v = serde_json::to_value(&tool).unwrap();
1253        assert_eq!(v["type"], "custom");
1254        assert_eq!(v["name"], "lookup");
1255        assert_eq!(v["input_schema"]["required"], json!(["id"]));
1256        let parsed: AgentTool = serde_json::from_value(v).unwrap();
1257        assert_eq!(parsed, tool);
1258    }
1259
1260    #[test]
1261    fn agent_tool_unknown_kind_falls_to_other() {
1262        let raw = json!({"type": "future_tool", "x": 1});
1263        let parsed: AgentTool = serde_json::from_value(raw.clone()).unwrap();
1264        match parsed {
1265            AgentTool::Other(v) => assert_eq!(v, raw),
1266            AgentTool::BuiltinToolset(_) | AgentTool::McpToolset(_) | AgentTool::Custom(_) => {
1267                panic!("expected Other")
1268            }
1269        }
1270    }
1271
1272    #[test]
1273    fn skill_round_trips_anthropic_and_custom_with_version() {
1274        let a = Skill::anthropic_pinned("xlsx", "1.2.3");
1275        let v = serde_json::to_value(&a).unwrap();
1276        assert_eq!(
1277            v,
1278            json!({"type": "anthropic", "skill_id": "xlsx", "version": "1.2.3"})
1279        );
1280        let parsed: Skill = serde_json::from_value(v).unwrap();
1281        assert_eq!(parsed, a);
1282
1283        let c = Skill::custom("skill_01XJ5");
1284        let v = serde_json::to_value(&c).unwrap();
1285        assert_eq!(v, json!({"type": "custom", "skill_id": "skill_01XJ5"}));
1286    }
1287
1288    #[test]
1289    fn skill_unknown_type_falls_to_other() {
1290        let raw = json!({"type": "future_skill", "blob": 1});
1291        let parsed: Skill = serde_json::from_value(raw.clone()).unwrap();
1292        match parsed {
1293            Skill::Other(v) => assert_eq!(v, raw),
1294            Skill::Anthropic(_) | Skill::Custom(_) => panic!("expected Other"),
1295        }
1296    }
1297
1298    #[test]
1299    fn metadata_patch_serializes_set_and_delete() {
1300        let p = MetadataPatch::new().set("env", "prod").delete("legacy_key");
1301        let v = serde_json::to_value(&p).unwrap();
1302        assert_eq!(v["env"], "prod");
1303        assert_eq!(v["legacy_key"], serde_json::Value::Null);
1304    }
1305
1306    #[tokio::test]
1307    async fn create_agent_posts_minimal_payload() {
1308        let mock = MockServer::start().await;
1309        Mock::given(method("POST"))
1310            .and(path("/v1/agents"))
1311            .and(body_partial_json(json!({
1312                "name": "Reviewer",
1313                "model": "claude-opus-4-7"
1314            })))
1315            .respond_with(ResponseTemplate::new(200).set_body_json(fake_agent_response()))
1316            .mount(&mock)
1317            .await;
1318
1319        let client = client_for(&mock);
1320        let req = CreateAgentRequest::builder()
1321            .name("Reviewer")
1322            .model("claude-opus-4-7")
1323            .build()
1324            .unwrap();
1325        let agent = client.managed_agents().agents().create(req).await.unwrap();
1326        assert_eq!(agent.id, "agent_01");
1327        assert_eq!(agent.version, 1);
1328    }
1329
1330    #[tokio::test]
1331    async fn create_coordinator_agent_includes_callable_agents() {
1332        let mock = MockServer::start().await;
1333        Mock::given(method("POST"))
1334            .and(path("/v1/agents"))
1335            .and(body_partial_json(json!({
1336                "name": "Engineering Lead",
1337                "model": "claude-opus-4-7",
1338                "callable_agents": [
1339                    {"type": "agent", "id": "agent_reviewer", "version": 2},
1340                    {"type": "agent", "id": "agent_test_writer", "version": 5}
1341                ]
1342            })))
1343            .respond_with(ResponseTemplate::new(200).set_body_json({
1344                let mut r = fake_agent_response();
1345                r["callable_agents"] = json!([
1346                    {"type": "agent", "id": "agent_reviewer", "version": 2},
1347                    {"type": "agent", "id": "agent_test_writer", "version": 5}
1348                ]);
1349                r
1350            }))
1351            .mount(&mock)
1352            .await;
1353
1354        let client = client_for(&mock);
1355        let req = CreateAgentRequest::builder()
1356            .name("Engineering Lead")
1357            .model("claude-opus-4-7")
1358            .callable_agent(CallableAgent::new("agent_reviewer", 2))
1359            .callable_agent(CallableAgent::new("agent_test_writer", 5))
1360            .build()
1361            .unwrap();
1362        let agent = client.managed_agents().agents().create(req).await.unwrap();
1363        assert_eq!(agent.callable_agents.len(), 2);
1364        assert_eq!(agent.callable_agents[0].id, "agent_reviewer");
1365        assert_eq!(agent.callable_agents[0].version, 2);
1366    }
1367
1368    #[test]
1369    fn callable_agent_serializes_with_type_tag() {
1370        let c = CallableAgent::new("agent_x", 3);
1371        let v = serde_json::to_value(&c).unwrap();
1372        assert_eq!(v, json!({"type": "agent", "id": "agent_x", "version": 3}));
1373    }
1374
1375    #[tokio::test]
1376    async fn create_agent_full_payload_round_trips() {
1377        let mock = MockServer::start().await;
1378        Mock::given(method("POST"))
1379            .and(path("/v1/agents"))
1380            .and(body_partial_json(json!({
1381                "name": "Reviewer",
1382                "model": {"id": "claude-opus-4-7", "speed": "fast"},
1383                "system": "Be helpful.",
1384                "description": "Code review assistant.",
1385                "mcp_servers": [
1386                    {"type": "url", "name": "github", "url": "https://api.githubcopilot.com/mcp/"}
1387                ],
1388                "tools": [
1389                    {"type": "agent_toolset_20260401"},
1390                    {"type": "mcp_toolset", "mcp_server_name": "github"}
1391                ],
1392                "skills": [{"type": "anthropic", "skill_id": "xlsx"}],
1393                "metadata": {"env": "prod"}
1394            })))
1395            .respond_with(ResponseTemplate::new(200).set_body_json(fake_agent_response()))
1396            .mount(&mock)
1397            .await;
1398
1399        let client = client_for(&mock);
1400        let req = CreateAgentRequest::builder()
1401            .name("Reviewer")
1402            .model(AgentModel::config("claude-opus-4-7", ModelSpeed::Fast))
1403            .system("Be helpful.")
1404            .description("Code review assistant.")
1405            .mcp_server(AgentMcpServer::url(
1406                "github",
1407                "https://api.githubcopilot.com/mcp/",
1408            ))
1409            .tool(AgentTool::builtin_toolset())
1410            .tool(AgentTool::mcp_toolset("github"))
1411            .skill(Skill::anthropic("xlsx"))
1412            .metadata("env", "prod")
1413            .build()
1414            .unwrap();
1415        client.managed_agents().agents().create(req).await.unwrap();
1416    }
1417
1418    #[tokio::test]
1419    async fn retrieve_agent_passes_version_query_when_supplied() {
1420        let mock = MockServer::start().await;
1421        Mock::given(method("GET"))
1422            .and(path("/v1/agents/agent_01"))
1423            .and(wiremock::matchers::query_param("version", "3"))
1424            .respond_with(ResponseTemplate::new(200).set_body_json(fake_agent_response()))
1425            .mount(&mock)
1426            .await;
1427
1428        let client = client_for(&mock);
1429        let _ = client
1430            .managed_agents()
1431            .agents()
1432            .retrieve("agent_01", Some(3))
1433            .await
1434            .unwrap();
1435    }
1436
1437    #[tokio::test]
1438    async fn list_agents_passes_created_at_brackets_in_query() {
1439        let mock = MockServer::start().await;
1440        Mock::given(method("GET"))
1441            .and(path("/v1/agents"))
1442            .and(wiremock::matchers::query_param(
1443                "created_at[gte]",
1444                "2026-04-01T00:00:00Z",
1445            ))
1446            .and(wiremock::matchers::query_param("include_archived", "true"))
1447            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1448                "data": [fake_agent_response()],
1449                "next_page": null
1450            })))
1451            .mount(&mock)
1452            .await;
1453
1454        let client = client_for(&mock);
1455        let page = client
1456            .managed_agents()
1457            .agents()
1458            .list(ListAgentsParams {
1459                created_at_gte: Some("2026-04-01T00:00:00Z".into()),
1460                include_archived: Some(true),
1461                ..Default::default()
1462            })
1463            .await
1464            .unwrap();
1465        assert_eq!(page.data.len(), 1);
1466    }
1467
1468    #[tokio::test]
1469    async fn update_agent_sends_version_for_optimistic_concurrency() {
1470        let mock = MockServer::start().await;
1471        Mock::given(method("POST"))
1472            .and(path("/v1/agents/agent_01"))
1473            .and(body_partial_json(json!({
1474                "version": 1,
1475                "name": "Reviewer v2",
1476                "metadata": {"env": "staging", "old_key": null}
1477            })))
1478            .respond_with(ResponseTemplate::new(200).set_body_json(fake_agent_response()))
1479            .mount(&mock)
1480            .await;
1481
1482        let client = client_for(&mock);
1483        let req = UpdateAgentRequest::at_version(1)
1484            .name("Reviewer v2")
1485            .metadata(MetadataPatch::new().set("env", "staging").delete("old_key"));
1486        client
1487            .managed_agents()
1488            .agents()
1489            .update("agent_01", req)
1490            .await
1491            .unwrap();
1492    }
1493
1494    #[tokio::test]
1495    async fn archive_agent_posts_to_archive_subpath() {
1496        let mock = MockServer::start().await;
1497        Mock::given(method("POST"))
1498            .and(path("/v1/agents/agent_01/archive"))
1499            .respond_with(ResponseTemplate::new(200).set_body_json({
1500                let mut a = fake_agent_response();
1501                a["archived_at"] = json!("2026-04-30T12:00:00Z");
1502                a
1503            }))
1504            .mount(&mock)
1505            .await;
1506
1507        let client = client_for(&mock);
1508        let agent = client
1509            .managed_agents()
1510            .agents()
1511            .archive("agent_01")
1512            .await
1513            .unwrap();
1514        assert!(agent.archived_at.is_some());
1515    }
1516
1517    #[tokio::test]
1518    async fn list_versions_returns_paginated_history() {
1519        let mock = MockServer::start().await;
1520        Mock::given(method("GET"))
1521            .and(path("/v1/agents/agent_01/versions"))
1522            .and(wiremock::matchers::query_param("limit", "5"))
1523            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1524                "data": [fake_agent_response()],
1525                "next_page": "cursor_x"
1526            })))
1527            .mount(&mock)
1528            .await;
1529
1530        let client = client_for(&mock);
1531        let page = client
1532            .managed_agents()
1533            .agents()
1534            .list_versions(
1535                "agent_01",
1536                ListAgentVersionsParams {
1537                    limit: Some(5),
1538                    ..Default::default()
1539                },
1540            )
1541            .await
1542            .unwrap();
1543        assert_eq!(page.data.len(), 1);
1544    }
1545}