1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38#[non_exhaustive]
39pub enum ModelSpeed {
40 Standard,
42 Fast,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(untagged)]
50#[non_exhaustive]
51pub enum AgentModel {
52 String(String),
55 Config {
57 id: String,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 speed: Option<ModelSpeed>,
62 },
63}
64
65impl AgentModel {
66 #[must_use]
68 pub fn id(id: impl Into<String>) -> Self {
69 Self::String(id.into())
70 }
71
72 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(tag = "type", rename_all = "snake_case")]
116#[non_exhaustive]
117pub enum AgentMcpServer {
118 Url {
120 name: String,
122 url: String,
124 },
125}
126
127impl AgentMcpServer {
128 #[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#[derive(Debug, Clone, PartialEq)]
146pub enum PermissionPolicy {
147 AlwaysAllow,
149 AlwaysAsk,
151 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
196#[serde(rename_all = "snake_case")]
197#[non_exhaustive]
198pub enum BuiltinToolName {
199 #[default]
201 Bash,
202 Edit,
204 Read,
206 Write,
208 Glob,
210 Grep,
212 WebFetch,
214 WebSearch,
216}
217
218#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
220#[non_exhaustive]
221pub struct BuiltinToolConfig {
222 pub name: BuiltinToolName,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub enabled: Option<bool>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub permission_policy: Option<PermissionPolicy>,
230}
231
232#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
234#[non_exhaustive]
235pub struct McpToolConfig {
236 pub name: String,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub enabled: Option<bool>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub permission_policy: Option<PermissionPolicy>,
244}
245
246#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
248#[non_exhaustive]
249pub struct ToolsetDefaultConfig {
250 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub enabled: Option<bool>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub permission_policy: Option<PermissionPolicy>,
257}
258
259#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
261#[non_exhaustive]
262pub struct CustomToolInputSchema {
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub properties: Option<serde_json::Value>,
266 #[serde(default, skip_serializing_if = "Vec::is_empty")]
268 pub required: Vec<String>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
271 #[serde(rename = "type")]
272 pub ty: Option<String>,
273}
274
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279#[non_exhaustive]
280pub struct CustomTool {
281 pub name: String,
283 pub description: String,
285 pub input_schema: CustomToolInputSchema,
287}
288
289#[derive(Debug, Clone, PartialEq)]
298pub enum AgentTool {
299 BuiltinToolset(BuiltinToolset),
303 McpToolset(McpToolset),
306 Custom(CustomTool),
308 Other(serde_json::Value),
310}
311
312#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
314#[non_exhaustive]
315pub struct BuiltinToolset {
316 #[serde(default, skip_serializing_if = "Vec::is_empty")]
318 pub configs: Vec<BuiltinToolConfig>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub default_config: Option<ToolsetDefaultConfig>,
322}
323
324#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326#[non_exhaustive]
327pub struct McpToolset {
328 pub mcp_server_name: String,
330 #[serde(default, skip_serializing_if = "Vec::is_empty")]
332 pub configs: Vec<McpToolConfig>,
333 #[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 #[must_use]
410 pub fn builtin_toolset() -> Self {
411 Self::BuiltinToolset(BuiltinToolset::default())
412 }
413
414 #[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 #[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#[derive(Debug, Clone, PartialEq)]
446pub enum Skill {
447 Anthropic(AnthropicSkill),
449 Custom(CustomSkill),
451 Other(serde_json::Value),
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457#[non_exhaustive]
458pub struct AnthropicSkill {
459 pub skill_id: String,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub version: Option<String>,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469#[non_exhaustive]
470pub struct CustomSkill {
471 pub skill_id: String,
473 #[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 #[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 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
571#[non_exhaustive]
572pub struct CallableAgent {
573 #[serde(rename = "type")]
575 pub ty: String,
576 pub id: String,
578 pub version: u32,
580}
581
582impl CallableAgent {
583 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
600#[non_exhaustive]
601pub struct Agent {
602 pub id: String,
604 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
606 pub ty: Option<String>,
607 pub name: String,
609 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub description: Option<String>,
613 pub model: AgentModel,
615 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub system: Option<String>,
619 #[serde(default)]
621 pub mcp_servers: Vec<AgentMcpServer>,
622 #[serde(default)]
624 pub skills: Vec<Skill>,
625 #[serde(default)]
627 pub tools: Vec<AgentTool>,
628 #[serde(default)]
630 pub metadata: HashMap<String, String>,
631 #[serde(default)]
634 pub callable_agents: Vec<CallableAgent>,
635 pub version: u32,
637 pub created_at: String,
639 pub updated_at: String,
641 #[serde(default)]
643 pub archived_at: Option<String>,
644}
645
646#[derive(Debug, Clone, Serialize)]
652#[non_exhaustive]
653pub struct CreateAgentRequest {
654 pub name: String,
656 pub model: AgentModel,
658 #[serde(skip_serializing_if = "Option::is_none")]
660 pub description: Option<String>,
661 #[serde(skip_serializing_if = "Option::is_none")]
663 pub system: Option<String>,
664 #[serde(skip_serializing_if = "Vec::is_empty")]
666 pub mcp_servers: Vec<AgentMcpServer>,
667 #[serde(skip_serializing_if = "Vec::is_empty")]
669 pub skills: Vec<Skill>,
670 #[serde(skip_serializing_if = "Vec::is_empty")]
672 pub tools: Vec<AgentTool>,
673 #[serde(skip_serializing_if = "HashMap::is_empty")]
675 pub metadata: HashMap<String, String>,
676 #[serde(skip_serializing_if = "Vec::is_empty")]
683 pub callable_agents: Vec<CallableAgent>,
684}
685
686impl CreateAgentRequest {
687 #[must_use]
689 pub fn builder() -> CreateAgentRequestBuilder {
690 CreateAgentRequestBuilder::default()
691 }
692}
693
694#[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 #[must_use]
711 pub fn name(mut self, name: impl Into<String>) -> Self {
712 self.name = Some(name.into());
713 self
714 }
715
716 #[must_use]
718 pub fn model(mut self, model: impl Into<AgentModel>) -> Self {
719 self.model = Some(model.into());
720 self
721 }
722
723 #[must_use]
725 pub fn description(mut self, description: impl Into<String>) -> Self {
726 self.description = Some(description.into());
727 self
728 }
729
730 #[must_use]
732 pub fn system(mut self, system: impl Into<String>) -> Self {
733 self.system = Some(system.into());
734 self
735 }
736
737 #[must_use]
739 pub fn mcp_server(mut self, server: AgentMcpServer) -> Self {
740 self.mcp_servers.push(server);
741 self
742 }
743
744 #[must_use]
746 pub fn skill(mut self, skill: Skill) -> Self {
747 self.skills.push(skill);
748 self
749 }
750
751 #[must_use]
753 pub fn tool(mut self, tool: AgentTool) -> Self {
754 self.tools.push(tool);
755 self
756 }
757
758 #[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 #[must_use]
767 pub fn callable_agent(mut self, callable: CallableAgent) -> Self {
768 self.callable_agents.push(callable);
769 self
770 }
771
772 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#[derive(Debug, Clone, Default, Serialize)]
815#[non_exhaustive]
816pub struct UpdateAgentRequest {
817 pub version: u32,
819 #[serde(skip_serializing_if = "Option::is_none")]
821 pub name: Option<String>,
822 #[serde(skip_serializing_if = "Option::is_none")]
824 pub model: Option<AgentModel>,
825 #[serde(skip_serializing_if = "Option::is_none")]
827 pub description: Option<String>,
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub system: Option<String>,
831 #[serde(skip_serializing_if = "Option::is_none")]
833 pub mcp_servers: Option<Vec<AgentMcpServer>>,
834 #[serde(skip_serializing_if = "Option::is_none")]
836 pub skills: Option<Vec<Skill>>,
837 #[serde(skip_serializing_if = "Option::is_none")]
839 pub tools: Option<Vec<AgentTool>>,
840 #[serde(skip_serializing_if = "Option::is_none")]
842 pub metadata: Option<MetadataPatch>,
843 #[serde(skip_serializing_if = "Option::is_none")]
846 pub callable_agents: Option<Vec<CallableAgent>>,
847}
848
849impl UpdateAgentRequest {
850 #[must_use]
853 pub fn at_version(version: u32) -> Self {
854 Self {
855 version,
856 ..Self::default()
857 }
858 }
859
860 #[must_use]
862 pub fn name(mut self, name: impl Into<String>) -> Self {
863 self.name = Some(name.into());
864 self
865 }
866
867 #[must_use]
869 pub fn model(mut self, model: impl Into<AgentModel>) -> Self {
870 self.model = Some(model.into());
871 self
872 }
873
874 #[must_use]
876 pub fn description(mut self, description: impl Into<String>) -> Self {
877 self.description = Some(description.into());
878 self
879 }
880
881 #[must_use]
883 pub fn system(mut self, system: impl Into<String>) -> Self {
884 self.system = Some(system.into());
885 self
886 }
887
888 #[must_use]
890 pub fn mcp_servers(mut self, servers: Vec<AgentMcpServer>) -> Self {
891 self.mcp_servers = Some(servers);
892 self
893 }
894
895 #[must_use]
897 pub fn skills(mut self, skills: Vec<Skill>) -> Self {
898 self.skills = Some(skills);
899 self
900 }
901
902 #[must_use]
904 pub fn tools(mut self, tools: Vec<AgentTool>) -> Self {
905 self.tools = Some(tools);
906 self
907 }
908
909 #[must_use]
911 pub fn metadata(mut self, patch: MetadataPatch) -> Self {
912 self.metadata = Some(patch);
913 self
914 }
915
916 #[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#[derive(Debug, Clone, Default, Serialize)]
928pub struct MetadataPatch(pub HashMap<String, Option<String>>);
929
930impl MetadataPatch {
931 #[must_use]
933 pub fn new() -> Self {
934 Self::default()
935 }
936
937 #[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 #[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#[derive(Debug, Clone, Default)]
958#[non_exhaustive]
959pub struct ListAgentsParams {
960 pub created_at_gte: Option<String>,
962 pub created_at_lte: Option<String>,
964 pub include_archived: Option<bool>,
966 pub limit: Option<u32>,
968 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#[derive(Debug, Clone, Default)]
996#[non_exhaustive]
997pub struct ListAgentVersionsParams {
998 pub limit: Option<u32>,
1000 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
1017pub 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 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 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 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 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 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 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}