use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::error::Result;
use crate::pagination::Paginated;
use super::MANAGED_AGENTS_BETA;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ModelSpeed {
Standard,
Fast,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum AgentModel {
String(String),
Config {
id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
speed: Option<ModelSpeed>,
},
}
impl AgentModel {
#[must_use]
pub fn id(id: impl Into<String>) -> Self {
Self::String(id.into())
}
#[must_use]
pub fn config(id: impl Into<String>, speed: ModelSpeed) -> Self {
Self::Config {
id: id.into(),
speed: Some(speed),
}
}
#[must_use]
pub fn model_id(&self) -> &str {
match self {
Self::String(s) => s,
Self::Config { id, .. } => id,
}
}
}
impl From<&str> for AgentModel {
fn from(s: &str) -> Self {
Self::String(s.to_owned())
}
}
impl From<String> for AgentModel {
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<crate::types::ModelId> for AgentModel {
fn from(m: crate::types::ModelId) -> Self {
Self::String(m.as_str().to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum AgentMcpServer {
Url {
name: String,
url: String,
},
}
impl AgentMcpServer {
#[must_use]
pub fn url(name: impl Into<String>, url: impl Into<String>) -> Self {
Self::Url {
name: name.into(),
url: url.into(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PermissionPolicy {
AlwaysAllow,
AlwaysAsk,
Other(serde_json::Value),
}
const KNOWN_PERMISSION_POLICY_TAGS: &[&str] = &["always_allow", "always_ask"];
impl Serialize for PermissionPolicy {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
match self {
Self::AlwaysAllow => {
let mut map = s.serialize_map(Some(1))?;
map.serialize_entry("type", "always_allow")?;
map.end()
}
Self::AlwaysAsk => {
let mut map = s.serialize_map(Some(1))?;
map.serialize_entry("type", "always_ask")?;
map.end()
}
Self::Other(v) => v.serialize(s),
}
}
}
impl<'de> Deserialize<'de> for PermissionPolicy {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
let raw = serde_json::Value::deserialize(d)?;
let tag = raw.get("type").and_then(serde_json::Value::as_str);
match tag {
Some("always_allow") if KNOWN_PERMISSION_POLICY_TAGS.contains(&"always_allow") => {
Ok(Self::AlwaysAllow)
}
Some("always_ask") => Ok(Self::AlwaysAsk),
_ => Ok(Self::Other(raw)),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum BuiltinToolName {
#[default]
Bash,
Edit,
Read,
Write,
Glob,
Grep,
WebFetch,
WebSearch,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BuiltinToolConfig {
pub name: BuiltinToolName,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission_policy: Option<PermissionPolicy>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct McpToolConfig {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission_policy: Option<PermissionPolicy>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ToolsetDefaultConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission_policy: Option<PermissionPolicy>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CustomToolInputSchema {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub properties: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
pub ty: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CustomTool {
pub name: String,
pub description: String,
pub input_schema: CustomToolInputSchema,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AgentTool {
BuiltinToolset(BuiltinToolset),
McpToolset(McpToolset),
Custom(CustomTool),
Other(serde_json::Value),
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BuiltinToolset {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub configs: Vec<BuiltinToolConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_config: Option<ToolsetDefaultConfig>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct McpToolset {
pub mcp_server_name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub configs: Vec<McpToolConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_config: Option<ToolsetDefaultConfig>,
}
const KNOWN_AGENT_TOOL_TAGS: &[&str] = &["agent_toolset_20260401", "mcp_toolset", "custom"];
impl Serialize for AgentTool {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
match self {
Self::BuiltinToolset(b) => {
let mut map = s.serialize_map(None)?;
map.serialize_entry("type", "agent_toolset_20260401")?;
if !b.configs.is_empty() {
map.serialize_entry("configs", &b.configs)?;
}
if let Some(d) = &b.default_config {
map.serialize_entry("default_config", d)?;
}
map.end()
}
Self::McpToolset(m) => {
let mut map = s.serialize_map(None)?;
map.serialize_entry("type", "mcp_toolset")?;
map.serialize_entry("mcp_server_name", &m.mcp_server_name)?;
if !m.configs.is_empty() {
map.serialize_entry("configs", &m.configs)?;
}
if let Some(d) = &m.default_config {
map.serialize_entry("default_config", d)?;
}
map.end()
}
Self::Custom(c) => {
let mut map = s.serialize_map(None)?;
map.serialize_entry("type", "custom")?;
map.serialize_entry("name", &c.name)?;
map.serialize_entry("description", &c.description)?;
map.serialize_entry("input_schema", &c.input_schema)?;
map.end()
}
Self::Other(v) => v.serialize(s),
}
}
}
impl<'de> Deserialize<'de> for AgentTool {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
let raw = serde_json::Value::deserialize(d)?;
let tag = raw.get("type").and_then(serde_json::Value::as_str);
match tag {
Some("agent_toolset_20260401")
if KNOWN_AGENT_TOOL_TAGS.contains(&"agent_toolset_20260401") =>
{
let b = serde_json::from_value::<BuiltinToolset>(raw)
.map_err(serde::de::Error::custom)?;
Ok(Self::BuiltinToolset(b))
}
Some("mcp_toolset") => {
let m =
serde_json::from_value::<McpToolset>(raw).map_err(serde::de::Error::custom)?;
Ok(Self::McpToolset(m))
}
Some("custom") => {
let c =
serde_json::from_value::<CustomTool>(raw).map_err(serde::de::Error::custom)?;
Ok(Self::Custom(c))
}
_ => Ok(Self::Other(raw)),
}
}
}
impl AgentTool {
#[must_use]
pub fn builtin_toolset() -> Self {
Self::BuiltinToolset(BuiltinToolset::default())
}
#[must_use]
pub fn mcp_toolset(server_name: impl Into<String>) -> Self {
Self::McpToolset(McpToolset {
mcp_server_name: server_name.into(),
configs: Vec::new(),
default_config: None,
})
}
#[must_use]
pub fn custom(
name: impl Into<String>,
description: impl Into<String>,
input_schema: CustomToolInputSchema,
) -> Self {
Self::Custom(CustomTool {
name: name.into(),
description: description.into(),
input_schema,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Skill {
Anthropic(AnthropicSkill),
Custom(CustomSkill),
Other(serde_json::Value),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AnthropicSkill {
pub skill_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CustomSkill {
pub skill_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
const KNOWN_SKILL_TAGS: &[&str] = &["anthropic", "custom"];
impl Serialize for Skill {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
match self {
Self::Anthropic(a) => {
let mut map = s.serialize_map(None)?;
map.serialize_entry("type", "anthropic")?;
map.serialize_entry("skill_id", &a.skill_id)?;
if let Some(v) = &a.version {
map.serialize_entry("version", v)?;
}
map.end()
}
Self::Custom(c) => {
let mut map = s.serialize_map(None)?;
map.serialize_entry("type", "custom")?;
map.serialize_entry("skill_id", &c.skill_id)?;
if let Some(v) = &c.version {
map.serialize_entry("version", v)?;
}
map.end()
}
Self::Other(v) => v.serialize(s),
}
}
}
impl<'de> Deserialize<'de> for Skill {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
let raw = serde_json::Value::deserialize(d)?;
let tag = raw.get("type").and_then(serde_json::Value::as_str);
match tag {
Some("anthropic") if KNOWN_SKILL_TAGS.contains(&"anthropic") => {
let a = serde_json::from_value::<AnthropicSkill>(raw)
.map_err(serde::de::Error::custom)?;
Ok(Self::Anthropic(a))
}
Some("custom") => {
let c =
serde_json::from_value::<CustomSkill>(raw).map_err(serde::de::Error::custom)?;
Ok(Self::Custom(c))
}
_ => Ok(Self::Other(raw)),
}
}
}
impl Skill {
#[must_use]
pub fn anthropic(skill_id: impl Into<String>) -> Self {
Self::Anthropic(AnthropicSkill {
skill_id: skill_id.into(),
version: None,
})
}
#[must_use]
pub fn anthropic_pinned(skill_id: impl Into<String>, version: impl Into<String>) -> Self {
Self::Anthropic(AnthropicSkill {
skill_id: skill_id.into(),
version: Some(version.into()),
})
}
#[must_use]
pub fn custom(skill_id: impl Into<String>) -> Self {
Self::Custom(CustomSkill {
skill_id: skill_id.into(),
version: None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CallableAgent {
#[serde(rename = "type")]
pub ty: String,
pub id: String,
pub version: u32,
}
impl CallableAgent {
#[must_use]
pub fn new(id: impl Into<String>, version: u32) -> Self {
Self {
ty: "agent".into(),
id: id.into(),
version,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Agent {
pub id: String,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub ty: Option<String>,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub model: AgentModel,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
#[serde(default)]
pub mcp_servers: Vec<AgentMcpServer>,
#[serde(default)]
pub skills: Vec<Skill>,
#[serde(default)]
pub tools: Vec<AgentTool>,
#[serde(default)]
pub metadata: HashMap<String, String>,
#[serde(default)]
pub callable_agents: Vec<CallableAgent>,
pub version: u32,
pub created_at: String,
pub updated_at: String,
#[serde(default)]
pub archived_at: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct CreateAgentRequest {
pub name: String,
pub model: AgentModel,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub mcp_servers: Vec<AgentMcpServer>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub skills: Vec<Skill>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<AgentTool>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub callable_agents: Vec<CallableAgent>,
}
impl CreateAgentRequest {
#[must_use]
pub fn builder() -> CreateAgentRequestBuilder {
CreateAgentRequestBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct CreateAgentRequestBuilder {
name: Option<String>,
model: Option<AgentModel>,
description: Option<String>,
system: Option<String>,
mcp_servers: Vec<AgentMcpServer>,
skills: Vec<Skill>,
tools: Vec<AgentTool>,
metadata: HashMap<String, String>,
callable_agents: Vec<CallableAgent>,
}
impl CreateAgentRequestBuilder {
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn model(mut self, model: impl Into<AgentModel>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn system(mut self, system: impl Into<String>) -> Self {
self.system = Some(system.into());
self
}
#[must_use]
pub fn mcp_server(mut self, server: AgentMcpServer) -> Self {
self.mcp_servers.push(server);
self
}
#[must_use]
pub fn skill(mut self, skill: Skill) -> Self {
self.skills.push(skill);
self
}
#[must_use]
pub fn tool(mut self, tool: AgentTool) -> Self {
self.tools.push(tool);
self
}
#[must_use]
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
#[must_use]
pub fn callable_agent(mut self, callable: CallableAgent) -> Self {
self.callable_agents.push(callable);
self
}
pub fn build(self) -> Result<CreateAgentRequest> {
let name = self
.name
.ok_or_else(|| crate::Error::InvalidConfig("name is required".into()))?;
let model = self
.model
.ok_or_else(|| crate::Error::InvalidConfig("model is required".into()))?;
Ok(CreateAgentRequest {
name,
model,
description: self.description,
system: self.system,
mcp_servers: self.mcp_servers,
skills: self.skills,
tools: self.tools,
metadata: self.metadata,
callable_agents: self.callable_agents,
})
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[non_exhaustive]
pub struct UpdateAgentRequest {
pub version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<AgentModel>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcp_servers: Option<Vec<AgentMcpServer>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skills: Option<Vec<Skill>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<AgentTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<MetadataPatch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callable_agents: Option<Vec<CallableAgent>>,
}
impl UpdateAgentRequest {
#[must_use]
pub fn at_version(version: u32) -> Self {
Self {
version,
..Self::default()
}
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn model(mut self, model: impl Into<AgentModel>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn system(mut self, system: impl Into<String>) -> Self {
self.system = Some(system.into());
self
}
#[must_use]
pub fn mcp_servers(mut self, servers: Vec<AgentMcpServer>) -> Self {
self.mcp_servers = Some(servers);
self
}
#[must_use]
pub fn skills(mut self, skills: Vec<Skill>) -> Self {
self.skills = Some(skills);
self
}
#[must_use]
pub fn tools(mut self, tools: Vec<AgentTool>) -> Self {
self.tools = Some(tools);
self
}
#[must_use]
pub fn metadata(mut self, patch: MetadataPatch) -> Self {
self.metadata = Some(patch);
self
}
#[must_use]
pub fn callable_agents(mut self, callable: Vec<CallableAgent>) -> Self {
self.callable_agents = Some(callable);
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct MetadataPatch(pub HashMap<String, Option<String>>);
impl MetadataPatch {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn set(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.0.insert(key.into(), Some(value.into()));
self
}
#[must_use]
pub fn delete(mut self, key: impl Into<String>) -> Self {
self.0.insert(key.into(), None);
self
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ListAgentsParams {
pub created_at_gte: Option<String>,
pub created_at_lte: Option<String>,
pub include_archived: Option<bool>,
pub limit: Option<u32>,
pub page: Option<String>,
}
impl ListAgentsParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
if let Some(t) = &self.created_at_gte {
q.push(("created_at[gte]", t.clone()));
}
if let Some(t) = &self.created_at_lte {
q.push(("created_at[lte]", t.clone()));
}
if let Some(b) = self.include_archived {
q.push(("include_archived", b.to_string()));
}
if let Some(l) = self.limit {
q.push(("limit", l.to_string()));
}
if let Some(p) = &self.page {
q.push(("page", p.clone()));
}
q
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ListAgentVersionsParams {
pub limit: Option<u32>,
pub page: Option<String>,
}
impl ListAgentVersionsParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
if let Some(l) = self.limit {
q.push(("limit", l.to_string()));
}
if let Some(p) = &self.page {
q.push(("page", p.clone()));
}
q
}
}
pub struct Agents<'a> {
client: &'a Client,
}
impl<'a> Agents<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn create(&self, request: CreateAgentRequest) -> Result<Agent> {
let body = &request;
self.client
.execute_with_retry(
|| {
self.client
.request_builder(reqwest::Method::POST, "/v1/agents")
.json(body)
},
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn retrieve(&self, agent_id: &str, version: Option<u32>) -> Result<Agent> {
let path = format!("/v1/agents/{agent_id}");
let v = version;
self.client
.execute_with_retry(
|| {
let mut req = self.client.request_builder(reqwest::Method::GET, &path);
if let Some(version) = v {
req = req.query(&[("version", version.to_string())]);
}
req
},
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn list(&self, params: ListAgentsParams) -> Result<Paginated<Agent>> {
let query = params.to_query();
self.client
.execute_with_retry(
|| {
let mut req = self
.client
.request_builder(reqwest::Method::GET, "/v1/agents");
for (k, v) in &query {
req = req.query(&[(k, v)]);
}
req
},
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn update(&self, agent_id: &str, request: UpdateAgentRequest) -> Result<Agent> {
let path = format!("/v1/agents/{agent_id}");
let body = &request;
self.client
.execute_with_retry(
|| {
self.client
.request_builder(reqwest::Method::POST, &path)
.json(body)
},
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn archive(&self, agent_id: &str) -> Result<Agent> {
let path = format!("/v1/agents/{agent_id}/archive");
self.client
.execute_with_retry(
|| self.client.request_builder(reqwest::Method::POST, &path),
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn list_versions(
&self,
agent_id: &str,
params: ListAgentVersionsParams,
) -> Result<Paginated<Agent>> {
let path = format!("/v1/agents/{agent_id}/versions");
let query = params.to_query();
self.client
.execute_with_retry(
|| {
let mut req = self.client.request_builder(reqwest::Method::GET, &path);
for (k, v) in &query {
req = req.query(&[(k, v)]);
}
req
},
&[MANAGED_AGENTS_BETA],
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
use wiremock::matchers::{body_partial_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn client_for(mock: &MockServer) -> Client {
Client::builder()
.api_key("sk-ant-test")
.base_url(mock.uri())
.build()
.unwrap()
}
fn fake_agent_response() -> serde_json::Value {
json!({
"id": "agent_01",
"type": "agent",
"name": "Reviewer",
"description": "",
"model": "claude-opus-4-7",
"system": "",
"mcp_servers": [],
"skills": [],
"tools": [],
"metadata": {},
"version": 1,
"created_at": "2026-04-30T12:00:00Z",
"updated_at": "2026-04-30T12:00:00Z"
})
}
#[test]
fn agent_model_serializes_string_form_untagged() {
let m = AgentModel::id("claude-opus-4-7");
let v = serde_json::to_value(&m).unwrap();
assert_eq!(v, json!("claude-opus-4-7"));
}
#[test]
fn agent_model_serializes_config_form_with_speed() {
let m = AgentModel::config("claude-opus-4-7", ModelSpeed::Fast);
let v = serde_json::to_value(&m).unwrap();
assert_eq!(v, json!({"id": "claude-opus-4-7", "speed": "fast"}));
let parsed: AgentModel = serde_json::from_value(v).unwrap();
assert_eq!(parsed, m);
}
#[test]
fn permission_policy_round_trips_known_variants() {
assert_eq!(
serde_json::to_value(PermissionPolicy::AlwaysAllow).unwrap(),
json!({"type": "always_allow"})
);
assert_eq!(
serde_json::to_value(PermissionPolicy::AlwaysAsk).unwrap(),
json!({"type": "always_ask"})
);
}
#[test]
fn permission_policy_unknown_variant_falls_to_other() {
let raw = json!({"type": "future_policy", "x": 1});
let parsed: PermissionPolicy = serde_json::from_value(raw.clone()).unwrap();
match parsed {
PermissionPolicy::Other(v) => assert_eq!(v, raw),
PermissionPolicy::AlwaysAllow | PermissionPolicy::AlwaysAsk => panic!("expected Other"),
}
}
#[test]
fn agent_tool_builtin_toolset_serializes_with_configs() {
let tool = AgentTool::BuiltinToolset(BuiltinToolset {
configs: vec![BuiltinToolConfig {
name: BuiltinToolName::Bash,
enabled: Some(true),
permission_policy: Some(PermissionPolicy::AlwaysAsk),
}],
default_config: Some(ToolsetDefaultConfig {
enabled: Some(true),
permission_policy: Some(PermissionPolicy::AlwaysAllow),
}),
});
let v = serde_json::to_value(&tool).unwrap();
assert_eq!(v["type"], "agent_toolset_20260401");
assert_eq!(v["configs"][0]["name"], "bash");
assert_eq!(v["configs"][0]["permission_policy"]["type"], "always_ask");
assert_eq!(v["default_config"]["enabled"], true);
}
#[test]
fn agent_tool_mcp_toolset_round_trips_with_server_name() {
let tool = AgentTool::mcp_toolset("github");
let v = serde_json::to_value(&tool).unwrap();
assert_eq!(
v,
json!({"type": "mcp_toolset", "mcp_server_name": "github"})
);
let parsed: AgentTool = serde_json::from_value(v).unwrap();
assert_eq!(parsed, tool);
}
#[test]
fn agent_tool_custom_round_trips_with_input_schema() {
let tool = AgentTool::custom(
"lookup",
"Find a record by id",
CustomToolInputSchema {
properties: Some(json!({"id": {"type": "string"}})),
required: vec!["id".into()],
ty: Some("object".into()),
},
);
let v = serde_json::to_value(&tool).unwrap();
assert_eq!(v["type"], "custom");
assert_eq!(v["name"], "lookup");
assert_eq!(v["input_schema"]["required"], json!(["id"]));
let parsed: AgentTool = serde_json::from_value(v).unwrap();
assert_eq!(parsed, tool);
}
#[test]
fn agent_tool_unknown_kind_falls_to_other() {
let raw = json!({"type": "future_tool", "x": 1});
let parsed: AgentTool = serde_json::from_value(raw.clone()).unwrap();
match parsed {
AgentTool::Other(v) => assert_eq!(v, raw),
AgentTool::BuiltinToolset(_) | AgentTool::McpToolset(_) | AgentTool::Custom(_) => {
panic!("expected Other")
}
}
}
#[test]
fn skill_round_trips_anthropic_and_custom_with_version() {
let a = Skill::anthropic_pinned("xlsx", "1.2.3");
let v = serde_json::to_value(&a).unwrap();
assert_eq!(
v,
json!({"type": "anthropic", "skill_id": "xlsx", "version": "1.2.3"})
);
let parsed: Skill = serde_json::from_value(v).unwrap();
assert_eq!(parsed, a);
let c = Skill::custom("skill_01XJ5");
let v = serde_json::to_value(&c).unwrap();
assert_eq!(v, json!({"type": "custom", "skill_id": "skill_01XJ5"}));
}
#[test]
fn skill_unknown_type_falls_to_other() {
let raw = json!({"type": "future_skill", "blob": 1});
let parsed: Skill = serde_json::from_value(raw.clone()).unwrap();
match parsed {
Skill::Other(v) => assert_eq!(v, raw),
Skill::Anthropic(_) | Skill::Custom(_) => panic!("expected Other"),
}
}
#[test]
fn metadata_patch_serializes_set_and_delete() {
let p = MetadataPatch::new().set("env", "prod").delete("legacy_key");
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v["env"], "prod");
assert_eq!(v["legacy_key"], serde_json::Value::Null);
}
#[tokio::test]
async fn create_agent_posts_minimal_payload() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/agents"))
.and(body_partial_json(json!({
"name": "Reviewer",
"model": "claude-opus-4-7"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(fake_agent_response()))
.mount(&mock)
.await;
let client = client_for(&mock);
let req = CreateAgentRequest::builder()
.name("Reviewer")
.model("claude-opus-4-7")
.build()
.unwrap();
let agent = client.managed_agents().agents().create(req).await.unwrap();
assert_eq!(agent.id, "agent_01");
assert_eq!(agent.version, 1);
}
#[tokio::test]
async fn create_coordinator_agent_includes_callable_agents() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/agents"))
.and(body_partial_json(json!({
"name": "Engineering Lead",
"model": "claude-opus-4-7",
"callable_agents": [
{"type": "agent", "id": "agent_reviewer", "version": 2},
{"type": "agent", "id": "agent_test_writer", "version": 5}
]
})))
.respond_with(ResponseTemplate::new(200).set_body_json({
let mut r = fake_agent_response();
r["callable_agents"] = json!([
{"type": "agent", "id": "agent_reviewer", "version": 2},
{"type": "agent", "id": "agent_test_writer", "version": 5}
]);
r
}))
.mount(&mock)
.await;
let client = client_for(&mock);
let req = CreateAgentRequest::builder()
.name("Engineering Lead")
.model("claude-opus-4-7")
.callable_agent(CallableAgent::new("agent_reviewer", 2))
.callable_agent(CallableAgent::new("agent_test_writer", 5))
.build()
.unwrap();
let agent = client.managed_agents().agents().create(req).await.unwrap();
assert_eq!(agent.callable_agents.len(), 2);
assert_eq!(agent.callable_agents[0].id, "agent_reviewer");
assert_eq!(agent.callable_agents[0].version, 2);
}
#[test]
fn callable_agent_serializes_with_type_tag() {
let c = CallableAgent::new("agent_x", 3);
let v = serde_json::to_value(&c).unwrap();
assert_eq!(v, json!({"type": "agent", "id": "agent_x", "version": 3}));
}
#[tokio::test]
async fn create_agent_full_payload_round_trips() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/agents"))
.and(body_partial_json(json!({
"name": "Reviewer",
"model": {"id": "claude-opus-4-7", "speed": "fast"},
"system": "Be helpful.",
"description": "Code review assistant.",
"mcp_servers": [
{"type": "url", "name": "github", "url": "https://api.githubcopilot.com/mcp/"}
],
"tools": [
{"type": "agent_toolset_20260401"},
{"type": "mcp_toolset", "mcp_server_name": "github"}
],
"skills": [{"type": "anthropic", "skill_id": "xlsx"}],
"metadata": {"env": "prod"}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(fake_agent_response()))
.mount(&mock)
.await;
let client = client_for(&mock);
let req = CreateAgentRequest::builder()
.name("Reviewer")
.model(AgentModel::config("claude-opus-4-7", ModelSpeed::Fast))
.system("Be helpful.")
.description("Code review assistant.")
.mcp_server(AgentMcpServer::url(
"github",
"https://api.githubcopilot.com/mcp/",
))
.tool(AgentTool::builtin_toolset())
.tool(AgentTool::mcp_toolset("github"))
.skill(Skill::anthropic("xlsx"))
.metadata("env", "prod")
.build()
.unwrap();
client.managed_agents().agents().create(req).await.unwrap();
}
#[tokio::test]
async fn retrieve_agent_passes_version_query_when_supplied() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/agents/agent_01"))
.and(wiremock::matchers::query_param("version", "3"))
.respond_with(ResponseTemplate::new(200).set_body_json(fake_agent_response()))
.mount(&mock)
.await;
let client = client_for(&mock);
let _ = client
.managed_agents()
.agents()
.retrieve("agent_01", Some(3))
.await
.unwrap();
}
#[tokio::test]
async fn list_agents_passes_created_at_brackets_in_query() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/agents"))
.and(wiremock::matchers::query_param(
"created_at[gte]",
"2026-04-01T00:00:00Z",
))
.and(wiremock::matchers::query_param("include_archived", "true"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [fake_agent_response()],
"next_page": null
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let page = client
.managed_agents()
.agents()
.list(ListAgentsParams {
created_at_gte: Some("2026-04-01T00:00:00Z".into()),
include_archived: Some(true),
..Default::default()
})
.await
.unwrap();
assert_eq!(page.data.len(), 1);
}
#[tokio::test]
async fn update_agent_sends_version_for_optimistic_concurrency() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/agents/agent_01"))
.and(body_partial_json(json!({
"version": 1,
"name": "Reviewer v2",
"metadata": {"env": "staging", "old_key": null}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(fake_agent_response()))
.mount(&mock)
.await;
let client = client_for(&mock);
let req = UpdateAgentRequest::at_version(1)
.name("Reviewer v2")
.metadata(MetadataPatch::new().set("env", "staging").delete("old_key"));
client
.managed_agents()
.agents()
.update("agent_01", req)
.await
.unwrap();
}
#[tokio::test]
async fn archive_agent_posts_to_archive_subpath() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/agents/agent_01/archive"))
.respond_with(ResponseTemplate::new(200).set_body_json({
let mut a = fake_agent_response();
a["archived_at"] = json!("2026-04-30T12:00:00Z");
a
}))
.mount(&mock)
.await;
let client = client_for(&mock);
let agent = client
.managed_agents()
.agents()
.archive("agent_01")
.await
.unwrap();
assert!(agent.archived_at.is_some());
}
#[tokio::test]
async fn list_versions_returns_paginated_history() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/agents/agent_01/versions"))
.and(wiremock::matchers::query_param("limit", "5"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [fake_agent_response()],
"next_page": "cursor_x"
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let page = client
.managed_agents()
.agents()
.list_versions(
"agent_01",
ListAgentVersionsParams {
limit: Some(5),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(page.data.len(), 1);
}
}