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_serde_round_trips() {
101        let definition = AgentDefinition::new(
102            "agent_01933b5a000070008000000000000001".parse().unwrap(),
103            "test",
104            "You are helpful.",
105        );
106        let json = serde_json::to_value(&definition).unwrap();
107        assert_eq!(json["id"], "agent_01933b5a000070008000000000000001");
108        let round_tripped: AgentDefinition = serde_json::from_value(json.clone()).unwrap();
109        assert_eq!(serde_json::to_value(&round_tripped).unwrap(), json);
110    }
111
112    #[test]
113    fn definition_carries_no_persistence_metadata() {
114        let definition = AgentDefinition::new(
115            "agent_01933b5a000070008000000000000001".parse().unwrap(),
116            "test",
117            "prompt",
118        );
119        let json = serde_json::to_value(&definition).unwrap();
120        for persistence_field in [
121            "status",
122            "created_at",
123            "updated_at",
124            "archived_at",
125            "deleted_at",
126            "default_version_id",
127            "forked_from_agent_id",
128            "usage",
129        ] {
130            assert!(
131                json.get(persistence_field).is_none(),
132                "portable definition must not expose {persistence_field}"
133            );
134        }
135    }
136}