Skip to main content

systemprompt_agent/services/config_authoring/
edit.rs

1//! Application of [`AgentEditRequest`] mutations to an in-memory
2//! [`AgentConfig`].
3//!
4//! Each `apply_*` function mirrors one group of editable fields and records a
5//! human-readable change entry per mutation. Removal targets that are absent
6//! from the config are returned to the caller (not logged or printed) so the
7//! presentation layer owns how skips are reported.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use std::fs;
13
14use systemprompt_models::services::{AgentConfig, ServicesConfig};
15
16use super::{AgentConfigAuthoringService, ConfigAuthoringError};
17
18#[derive(Debug, Clone, Default)]
19pub struct AgentEditRequest {
20    pub enable: bool,
21    pub disable: bool,
22    pub port: Option<u16>,
23    pub endpoint: Option<String>,
24    pub dev_only: bool,
25    pub is_primary: bool,
26    pub default: bool,
27    pub display_name: Option<String>,
28    pub description: Option<String>,
29    pub version: Option<String>,
30    pub icon_url: Option<String>,
31    pub documentation_url: Option<String>,
32    pub streaming: Option<bool>,
33    pub push_notifications: Option<bool>,
34    pub state_transition_history: Option<bool>,
35    pub provider: Option<String>,
36    pub model: Option<String>,
37    pub system_prompt: Option<String>,
38    pub system_prompt_file: Option<String>,
39    pub mcp_servers: Vec<String>,
40    pub remove_mcp_servers: Vec<String>,
41    pub skills: Vec<String>,
42    pub remove_skills: Vec<String>,
43    pub set_values: Vec<String>,
44}
45
46impl AgentConfigAuthoringService {
47    pub fn apply_enabled_flags(
48        agent: &mut AgentConfig,
49        request: &AgentEditRequest,
50        changes: &mut Vec<String>,
51    ) {
52        if request.enable {
53            agent.enabled = true;
54            changes.push("enabled: true".to_owned());
55        }
56        if request.disable {
57            agent.enabled = false;
58            changes.push("enabled: false".to_owned());
59        }
60    }
61
62    pub fn apply_runtime_fields(
63        agent: &mut AgentConfig,
64        request: &AgentEditRequest,
65        changes: &mut Vec<String>,
66    ) -> Result<(), ConfigAuthoringError> {
67        if let Some(port) = request.port {
68            Self::validate_port(port)?;
69            agent.port = port;
70            changes.push(format!("port: {port}"));
71        }
72        if let Some(endpoint) = &request.endpoint {
73            agent.endpoint.clone_from(endpoint);
74            changes.push(format!("endpoint: {endpoint}"));
75        }
76        if request.dev_only {
77            agent.dev_only = true;
78            changes.push("dev_only: true".to_owned());
79        }
80        if request.is_primary {
81            agent.is_primary = true;
82            changes.push("is_primary: true".to_owned());
83        }
84        if request.default {
85            agent.default = true;
86            changes.push("default: true".to_owned());
87        }
88        Ok(())
89    }
90
91    pub fn apply_card_fields(
92        agent: &mut AgentConfig,
93        request: &AgentEditRequest,
94        changes: &mut Vec<String>,
95    ) {
96        if let Some(display_name) = &request.display_name {
97            agent.card.display_name.clone_from(display_name);
98            changes.push(format!("card.display_name: {display_name}"));
99        }
100        if let Some(description) = &request.description {
101            agent.card.description.clone_from(description);
102            changes.push(format!("card.description: {description}"));
103        }
104        if let Some(version) = &request.version {
105            agent.card.version.clone_from(version);
106            changes.push(format!("card.version: {version}"));
107        }
108        if let Some(icon_url) = &request.icon_url {
109            agent.card.icon_url = Some(icon_url.clone());
110            changes.push(format!("card.icon_url: {icon_url}"));
111        }
112        if let Some(documentation_url) = &request.documentation_url {
113            agent.card.documentation_url = Some(documentation_url.clone());
114            changes.push(format!("card.documentation_url: {documentation_url}"));
115        }
116    }
117
118    pub fn apply_capability_fields(
119        agent: &mut AgentConfig,
120        request: &AgentEditRequest,
121        changes: &mut Vec<String>,
122    ) {
123        if let Some(streaming) = request.streaming {
124            agent.card.capabilities.streaming = streaming;
125            changes.push(format!("card.capabilities.streaming: {streaming}"));
126        }
127        if let Some(push_notifications) = request.push_notifications {
128            agent.card.capabilities.push_notifications = push_notifications;
129            changes.push(format!(
130                "card.capabilities.push_notifications: {push_notifications}"
131            ));
132        }
133        if let Some(state_transition_history) = request.state_transition_history {
134            agent.card.capabilities.state_transition_history = state_transition_history;
135            changes.push(format!(
136                "card.capabilities.state_transition_history: {state_transition_history}"
137            ));
138        }
139    }
140
141    pub fn apply_metadata_fields(
142        agent: &mut AgentConfig,
143        request: &AgentEditRequest,
144        changes: &mut Vec<String>,
145    ) -> Result<(), ConfigAuthoringError> {
146        if let Some(provider) = &request.provider {
147            agent.metadata.provider = Some(provider.clone());
148            changes.push(format!("metadata.provider: {provider}"));
149        }
150        if let Some(model) = &request.model {
151            agent.metadata.model = Some(model.clone());
152            changes.push(format!("metadata.model: {model}"));
153        }
154        if let Some(file_path) = &request.system_prompt_file {
155            let content = fs::read_to_string(file_path).map_err(|source| {
156                ConfigAuthoringError::SystemPromptFile {
157                    path: file_path.clone(),
158                    source,
159                }
160            })?;
161            agent.metadata.system_prompt = Some(content.clone());
162            changes.push(format!(
163                "system_prompt: loaded from {} ({} chars)",
164                file_path,
165                content.len()
166            ));
167        } else if let Some(prompt) = &request.system_prompt {
168            agent.metadata.system_prompt = Some(prompt.clone());
169            changes.push(format!("system_prompt: {} chars", prompt.len()));
170        }
171        Ok(())
172    }
173
174    pub fn apply_mcp_server_changes(
175        agent: &mut AgentConfig,
176        request: &AgentEditRequest,
177        services_config: &ServicesConfig,
178        changes: &mut Vec<String>,
179    ) -> Result<Vec<String>, ConfigAuthoringError> {
180        for mcp_server in &request.mcp_servers {
181            if agent.metadata.mcp_servers.include.contains(mcp_server) {
182                continue;
183            }
184            if !services_config.mcp_servers.contains_key(mcp_server) {
185                return Err(ConfigAuthoringError::UnknownMcpServer {
186                    name: mcp_server.clone(),
187                    available: services_config
188                        .mcp_servers
189                        .keys()
190                        .cloned()
191                        .collect::<Vec<_>>()
192                        .join(", "),
193                });
194            }
195            agent.metadata.mcp_servers.include.push(mcp_server.clone());
196            changes.push(format!("added mcp_server: {mcp_server}"));
197        }
198        let mut skipped = Vec::new();
199        for mcp_server in &request.remove_mcp_servers {
200            if let Some(pos) = agent
201                .metadata
202                .mcp_servers
203                .include
204                .iter()
205                .position(|s| s == mcp_server)
206            {
207                agent.metadata.mcp_servers.include.remove(pos);
208                changes.push(format!("removed mcp_server: {mcp_server}"));
209            } else {
210                skipped.push(mcp_server.clone());
211            }
212        }
213        Ok(skipped)
214    }
215
216    pub fn apply_skill_changes(
217        agent: &mut AgentConfig,
218        request: &AgentEditRequest,
219        changes: &mut Vec<String>,
220    ) -> Vec<String> {
221        for skill in &request.skills {
222            if !agent.metadata.skills.include.contains(skill) {
223                agent.metadata.skills.include.push(skill.clone());
224                changes.push(format!("added skill: {skill}"));
225            }
226        }
227        let mut skipped = Vec::new();
228        for skill in &request.remove_skills {
229            if let Some(pos) = agent
230                .metadata
231                .skills
232                .include
233                .iter()
234                .position(|s| s == skill)
235            {
236                let removed = agent.metadata.skills.include.remove(pos);
237                changes.push(format!("removed skill: {removed}"));
238            } else {
239                skipped.push(skill.clone());
240            }
241        }
242        skipped
243    }
244
245    pub fn apply_set_value_changes(
246        agent: &mut AgentConfig,
247        request: &AgentEditRequest,
248        changes: &mut Vec<String>,
249    ) -> Result<(), ConfigAuthoringError> {
250        for set_value in &request.set_values {
251            let Some((key, value)) = set_value.split_once('=') else {
252                return Err(ConfigAuthoringError::InvalidSetFormat(set_value.clone()));
253            };
254            apply_set_value(agent, key, value)?;
255            changes.push(format!("{key}: {value}"));
256        }
257        Ok(())
258    }
259}
260
261fn apply_set_value(
262    agent: &mut AgentConfig,
263    key: &str,
264    value: &str,
265) -> Result<(), ConfigAuthoringError> {
266    match key {
267        "card.displayName" | "card.display_name" => {
268            value.clone_into(&mut agent.card.display_name);
269        },
270        "card.description" => {
271            value.clone_into(&mut agent.card.description);
272        },
273        "card.version" => {
274            value.clone_into(&mut agent.card.version);
275        },
276        "endpoint" => {
277            value.clone_into(&mut agent.endpoint);
278        },
279        "is_primary" => {
280            agent.is_primary = parse_bool(key, value)?;
281        },
282        "default" => {
283            agent.default = parse_bool(key, value)?;
284        },
285        "dev_only" => {
286            agent.dev_only = parse_bool(key, value)?;
287        },
288        _ => {
289            return Err(ConfigAuthoringError::UnknownSetKey(key.to_owned()));
290        },
291    }
292    Ok(())
293}
294
295fn parse_bool(key: &str, value: &str) -> Result<bool, ConfigAuthoringError> {
296    value
297        .parse()
298        .map_err(|_e| ConfigAuthoringError::InvalidBoolean {
299            key: key.to_owned(),
300            value: value.to_owned(),
301        })
302}