Skip to main content

systemprompt_models/services/agent_config/
disk.rs

1//! On-disk YAML shape of an agent's `config.yaml` and its projection
2//! into the runtime [`super::AgentConfig`] shape.
3
4use serde::Deserialize;
5use systemprompt_identifiers::AgentId;
6
7use super::card::{AgentCardConfig, AgentMetadataConfig, OAuthConfig, default_true};
8use super::{AgentConfig, DEFAULT_AGENT_SYSTEM_PROMPT_FILE};
9use crate::errors::ConfigValidationError;
10
11fn default_version() -> String {
12    "1.0.0".to_string()
13}
14
15#[derive(Debug, Clone, Deserialize)]
16pub struct DiskAgentConfig {
17    #[serde(default)]
18    pub id: Option<AgentId>,
19    pub name: String,
20    pub display_name: String,
21    pub description: String,
22    #[serde(default = "default_version")]
23    pub version: String,
24    #[serde(default = "default_true")]
25    pub enabled: bool,
26    pub port: u16,
27    #[serde(default)]
28    pub endpoint: Option<String>,
29    #[serde(default)]
30    pub dev_only: bool,
31    #[serde(default)]
32    pub is_primary: bool,
33    #[serde(default)]
34    pub default: bool,
35    #[serde(default)]
36    pub system_prompt_file: Option<String>,
37    #[serde(default)]
38    pub tags: Vec<String>,
39    #[serde(default)]
40    pub category: Option<String>,
41    #[serde(default)]
42    pub mcp_servers: Vec<String>,
43    #[serde(default)]
44    pub skills: Vec<String>,
45    #[serde(default)]
46    pub provider: Option<String>,
47    #[serde(default)]
48    pub model: Option<String>,
49    pub card: AgentCardConfig,
50    #[serde(default)]
51    pub oauth: OAuthConfig,
52}
53
54impl DiskAgentConfig {
55    #[must_use]
56    pub fn system_prompt_file(&self) -> &str {
57        self.system_prompt_file
58            .as_deref()
59            .filter(|s| !s.is_empty())
60            .unwrap_or(DEFAULT_AGENT_SYSTEM_PROMPT_FILE)
61    }
62
63    #[must_use]
64    pub fn to_agent_config(&self, base_url: &str, system_prompt: Option<String>) -> AgentConfig {
65        let endpoint = self.endpoint.clone().unwrap_or_else(|| {
66            format!(
67                "{}/api/v1/agents/{}",
68                base_url.trim_end_matches('/'),
69                self.name
70            )
71        });
72
73        let card_name = self
74            .card
75            .name
76            .clone()
77            .unwrap_or_else(|| self.display_name.clone());
78
79        AgentConfig {
80            name: self.name.clone(),
81            port: self.port,
82            endpoint,
83            tags: self.tags.clone(),
84            enabled: self.enabled,
85            dev_only: self.dev_only,
86            is_primary: self.is_primary,
87            default: self.default,
88            card: AgentCardConfig {
89                name: Some(card_name),
90                ..self.card.clone()
91            },
92            metadata: AgentMetadataConfig {
93                system_prompt,
94                mcp_servers: self.mcp_servers.clone(),
95                skills: self.skills.clone(),
96                provider: self.provider.clone(),
97                model: self.model.clone(),
98                ..Default::default()
99            },
100            oauth: self.oauth.clone(),
101        }
102    }
103
104    pub fn validate(&self, dir_name: &str) -> Result<(), ConfigValidationError> {
105        if let Some(id) = &self.id
106            && id.as_str() != dir_name
107        {
108            return Err(ConfigValidationError::invalid_field(format!(
109                "Agent config id '{id}' does not match directory name '{dir_name}'"
110            )));
111        }
112
113        if !self
114            .name
115            .chars()
116            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
117        {
118            return Err(ConfigValidationError::invalid_field(format!(
119                "Agent name '{}' must be lowercase alphanumeric with underscores only",
120                self.name
121            )));
122        }
123
124        if self.name.len() < 3 || self.name.len() > 50 {
125            return Err(ConfigValidationError::invalid_field(format!(
126                "Agent name '{}' must be between 3 and 50 characters",
127                self.name
128            )));
129        }
130
131        if self.port == 0 {
132            return Err(ConfigValidationError::invalid_field(format!(
133                "Agent '{}' has invalid port {}",
134                self.name, self.port
135            )));
136        }
137
138        if self.display_name.is_empty() {
139            return Err(ConfigValidationError::required(format!(
140                "Agent '{}' display_name must not be empty",
141                self.name
142            )));
143        }
144
145        Ok(())
146    }
147}