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