use serde::{Deserialize, Serialize};
use crate::capability_types::AgentCapabilityConfig;
use crate::mcp_server::{ScopedMcpServers, scoped_mcp_servers_is_empty};
use crate::network_access::NetworkAccessList;
use crate::session_file::InitialFile;
use crate::tool_types::ToolDefinition;
use crate::typed_id::{AgentId, ModelId};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
pub id: AgentId,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub system_prompt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_model_id: Option<ModelId>,
#[serde(default)]
pub capabilities: Vec<AgentCapabilityConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub initial_files: Vec<InitialFile>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network_access: Option<NetworkAccessList>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_iterations: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<ToolDefinition>,
#[serde(
default,
rename = "mcpServers",
alias = "mcp_servers",
skip_serializing_if = "scoped_mcp_servers_is_empty"
)]
pub mcp_servers: ScopedMcpServers,
}
impl AgentDefinition {
pub fn new(id: AgentId, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
Self {
id,
name: name.into(),
display_name: None,
description: None,
system_prompt: system_prompt.into(),
default_model_id: None,
capabilities: vec![],
initial_files: vec![],
network_access: None,
max_iterations: None,
parallel_tool_calls: None,
tools: vec![],
mcp_servers: ScopedMcpServers::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn definition_preserves_portable_wire_contract() {
let mut definition = AgentDefinition::new(
"agent_01933b5a000070008000000000000001".parse().unwrap(),
"test",
"You are helpful.",
);
let mut expected = serde_json::json!({
"id": "agent_01933b5a000070008000000000000001", "name": "test",
"system_prompt": "You are helpful.", "capabilities": []
});
assert_eq!(serde_json::to_value(&definition).unwrap(), expected);
definition.capabilities = vec![AgentCapabilityConfig::with_config(
"web_fetch",
serde_json::json!({"timeout_ms": 30000}),
)];
definition.max_iterations = Some(7);
definition.parallel_tool_calls = Some(false);
definition.mcp_servers.insert(
"docs".into(),
crate::mcp_server::ScopedMcpServer {
url: "https://docs.example.test/mcp".into(),
..Default::default()
},
);
expected["capabilities"] =
serde_json::json!([{"ref": "web_fetch", "config": {"timeout_ms": 30000}}]);
expected["max_iterations"] = serde_json::json!(7);
expected["parallel_tool_calls"] = serde_json::json!(false);
expected["mcpServers"] =
serde_json::json!({"docs": {"type": "http", "url": "https://docs.example.test/mcp"}});
assert_eq!(serde_json::to_value(&definition).unwrap(), expected);
let mut legacy = expected.clone();
let servers = legacy
.as_object_mut()
.unwrap()
.remove("mcpServers")
.unwrap();
legacy["mcp_servers"] = servers;
for input in [expected.clone(), legacy] {
let parsed: AgentDefinition = serde_json::from_value(input).unwrap();
assert_eq!(serde_json::to_value(parsed).unwrap(), expected);
}
}
}