everruns_core/
agent_definition.rs1use serde::{Deserialize, Serialize};
13
14use crate::capability_types::AgentCapabilityConfig;
15use crate::mcp_server::{ScopedMcpServers, scoped_mcp_servers_is_empty};
16use crate::network_access::NetworkAccessList;
17use crate::session_file::InitialFile;
18use crate::tool_types::ToolDefinition;
19use crate::typed_id::{AgentId, ModelId};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AgentDefinition {
29 pub id: AgentId,
32 pub name: String,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub display_name: Option<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub description: Option<String>,
40 pub system_prompt: String,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub default_model_id: Option<ModelId>,
45 #[serde(default)]
47 pub capabilities: Vec<AgentCapabilityConfig>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
50 pub initial_files: Vec<InitialFile>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub network_access: Option<NetworkAccessList>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub max_iterations: Option<usize>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub parallel_tool_calls: Option<bool>,
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub tools: Vec<ToolDefinition>,
63 #[serde(
65 default,
66 rename = "mcpServers",
67 alias = "mcp_servers",
68 skip_serializing_if = "scoped_mcp_servers_is_empty"
69 )]
70 pub mcp_servers: ScopedMcpServers,
71}
72
73impl AgentDefinition {
74 pub fn new(id: AgentId, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
77 Self {
78 id,
79 name: name.into(),
80 display_name: None,
81 description: None,
82 system_prompt: system_prompt.into(),
83 default_model_id: None,
84 capabilities: vec![],
85 initial_files: vec![],
86 network_access: None,
87 max_iterations: None,
88 parallel_tool_calls: None,
89 tools: vec![],
90 mcp_servers: ScopedMcpServers::default(),
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn definition_preserves_portable_wire_contract() {
101 let mut definition = AgentDefinition::new(
102 "agent_01933b5a000070008000000000000001".parse().unwrap(),
103 "test",
104 "You are helpful.",
105 );
106 let mut expected = serde_json::json!({
107 "id": "agent_01933b5a000070008000000000000001", "name": "test",
108 "system_prompt": "You are helpful.", "capabilities": []
109 });
110 assert_eq!(serde_json::to_value(&definition).unwrap(), expected);
112 definition.capabilities = vec![AgentCapabilityConfig::with_config(
113 "web_fetch",
114 serde_json::json!({"timeout_ms": 30000}),
115 )];
116 definition.max_iterations = Some(7);
117 definition.parallel_tool_calls = Some(false);
118 definition.mcp_servers.insert(
119 "docs".into(),
120 crate::mcp_server::ScopedMcpServer {
121 url: "https://docs.example.test/mcp".into(),
122 ..Default::default()
123 },
124 );
125 expected["capabilities"] =
126 serde_json::json!([{"ref": "web_fetch", "config": {"timeout_ms": 30000}}]);
127 expected["max_iterations"] = serde_json::json!(7);
128 expected["parallel_tool_calls"] = serde_json::json!(false);
129 expected["mcpServers"] =
130 serde_json::json!({"docs": {"type": "http", "url": "https://docs.example.test/mcp"}});
131 assert_eq!(serde_json::to_value(&definition).unwrap(), expected);
132 let mut legacy = expected.clone();
133 let servers = legacy
134 .as_object_mut()
135 .unwrap()
136 .remove("mcpServers")
137 .unwrap();
138 legacy["mcp_servers"] = servers;
139 for input in [expected.clone(), legacy] {
140 let parsed: AgentDefinition = serde_json::from_value(input).unwrap();
141 assert_eq!(serde_json::to_value(parsed).unwrap(), expected);
142 }
143 }
144}