Skip to main content

everruns_core/
agent_definition.rs

1// Portable authored agent execution configuration (EVE-877).
2//
3// Decision: the stored `Agent`/`AgentVersion` persistence records — lifecycle
4// status, versioning and publication metadata, fork lineage, timestamps,
5// usage — live in `everruns-platform`. Core keeps only this portable,
6// execution-facing projection: the authored configuration the runtime folds
7// into the harness → agent → session overlay chain. The platform loading seam
8// (server repositories, worker adapters, hosted stores) projects stored
9// records into this value and enforces lifecycle validation (archived or
10// deleted records fail) before host execution begins.
11
12use 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/// Portable authored execution configuration for an agent.
22///
23/// Carries exactly what turn execution consumes: the agent's identity for
24/// correlation plus the authored configuration layer merged between the
25/// harness chain and the session overlay. It is not a persistence record —
26/// stored lifecycle/versioning metadata stays in `everruns-platform`.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AgentDefinition {
29    /// Public agent identifier (`agent_<32-hex>`), used for correlation and
30    /// session/agent mismatch validation during snapshot projection.
31    pub id: AgentId,
32    /// Addressable name, unique per org (e.g. "customer-support").
33    pub name: String,
34    /// Human-readable display name; falls back to `name` when absent.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub display_name: Option<String>,
37    /// Human-readable description of what the agent does.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub description: Option<String>,
40    /// System prompt contributed by the agent layer.
41    pub system_prompt: String,
42    /// Default LLM model; overridable at the session layer.
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub default_model_id: Option<ModelId>,
45    /// Capabilities enabled for this agent with per-agent configuration.
46    #[serde(default)]
47    pub capabilities: Vec<AgentCapabilityConfig>,
48    /// Starter files copied into each new session for this agent.
49    #[serde(default, skip_serializing_if = "Vec::is_empty")]
50    pub initial_files: Vec<InitialFile>,
51    /// Network access list merged with harness and session layers.
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub network_access: Option<NetworkAccessList>,
54    /// Maximum number of LLM iterations per turn for this agent.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub max_iterations: Option<usize>,
57    /// Request-level parallel tool calling preference (EVE-598).
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub parallel_tool_calls: Option<bool>,
60    /// Client-side tools registered for this agent.
61    #[serde(default, skip_serializing_if = "Vec::is_empty")]
62    pub tools: Vec<ToolDefinition>,
63    /// Remote MCP servers scoped to this agent and inherited by its sessions.
64    #[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    /// Create a definition with the given identity and prompt; all other
75    /// configuration starts empty.
76    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        // Exact shape also excludes product persistence metadata.
111        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}