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