Skip to main content

modular_agent_core/
preset.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::error::AgentError;
5use crate::id::{new_id, update_ids};
6use crate::modular_agent::ModularAgent;
7use crate::spec::PresetSpec;
8use crate::{AgentSpec, ConnectionSpec};
9
10/// A runtime instance of a workflow preset.
11///
12/// A preset represents a running or runnable workflow, containing agents
13/// and their connections. It manages the lifecycle (start/stop) of all
14/// agents within the workflow.
15pub struct Preset {
16    /// Unique identifier for this preset instance.
17    id: String,
18
19    /// Optional user-defined name for this preset.
20    name: Option<String>,
21
22    /// Whether this preset is currently running.
23    running: bool,
24
25    /// The specification containing agents and connections.
26    spec: PresetSpec,
27}
28
29impl Preset {
30    /// Creates a new preset with the given specification.
31    ///
32    /// All IDs in the spec (agents and connections) are regenerated to ensure uniqueness.
33    pub fn new(mut spec: PresetSpec) -> Self {
34        let (agents, connections) = update_ids(&spec.agents, &spec.connections);
35        spec.agents = agents;
36        spec.connections = connections;
37
38        Self {
39            id: new_id(),
40            name: None,
41            running: false,
42            spec,
43        }
44    }
45
46    /// Returns the unique identifier of this preset.
47    pub fn id(&self) -> &str {
48        &self.id
49    }
50
51    /// Returns a reference to the preset specification.
52    pub fn spec(&self) -> &PresetSpec {
53        &self.spec
54    }
55
56    /// Updates the preset specification from a JSON value.
57    ///
58    /// Note: The "agents" and "connections" fields are ignored;
59    /// only extension fields are updated.
60    pub fn update_spec(&mut self, value: &Value) -> Result<(), AgentError> {
61        let update_map = value
62            .as_object()
63            .ok_or_else(|| AgentError::SerializationError("Expected JSON object".to_string()))?;
64
65        for (k, v) in update_map {
66            match k.as_str() {
67                "agents" => {
68                    // just ignore
69                }
70                "connections" => {
71                    // just ignore
72                }
73                _ => {
74                    // Update extensions
75                    self.spec.extensions.insert(k.clone(), v.clone());
76                }
77            }
78        }
79        Ok(())
80    }
81
82    /// Returns whether this preset is currently running.
83    pub fn running(&self) -> bool {
84        self.running
85    }
86
87    /// Returns the user-defined name of this preset, if set.
88    pub fn name(&self) -> Option<&str> {
89        self.name.as_deref()
90    }
91
92    /// Sets the user-defined name of this preset.
93    pub fn set_name(&mut self, name: String) {
94        self.name = Some(name);
95    }
96
97    /// Clears the user-defined name of this preset.
98    pub fn clear_name(&mut self) {
99        self.name = None;
100    }
101
102    /// Adds an agent to this preset.
103    pub fn add_agent(&mut self, agent: AgentSpec) {
104        self.spec.add_agent(agent);
105    }
106
107    /// Removes an agent from this preset by its ID.
108    pub fn remove_agent(&mut self, agent_id: &str) {
109        self.spec.remove_agent(agent_id);
110    }
111
112    /// Adds a connection to this preset.
113    pub fn add_connection(&mut self, connection: ConnectionSpec) {
114        self.spec.add_connection(connection);
115    }
116
117    /// Removes a connection from this preset.
118    pub fn remove_connection(&mut self, connection: &ConnectionSpec) -> Option<ConnectionSpec> {
119        self.spec.remove_connection(connection)
120    }
121
122    /// Starts all enabled agents in this preset.
123    ///
124    /// If the preset is already running, this method returns immediately.
125    /// Disabled agents are skipped.
126    pub async fn start(&mut self, ma: &ModularAgent) -> Result<(), AgentError> {
127        if self.running {
128            // Already running
129            return Ok(());
130        }
131        self.running = true;
132
133        // A previous stop left the preset's parent cancellation token fired;
134        // install a fresh one before agents derive their child tokens.
135        ma.reset_preset_token(&self.id);
136
137        for agent in self.spec.agents.iter() {
138            if agent.disabled {
139                continue;
140            }
141            ma.start_agent(&agent.id).await.unwrap_or_else(|e| {
142                log::error!("Failed to start agent {}: {}", agent.id, e);
143            });
144        }
145
146        Ok(())
147    }
148
149    /// Stops all agents in this preset.
150    pub async fn stop(&mut self, ma: &ModularAgent) -> Result<(), AgentError> {
151        // Cancel every agent's in-flight process() up front so the
152        // per-agent stops below are not serialized behind long-running work.
153        ma.cancel_preset_token(&self.id);
154
155        for agent in self.spec.agents.iter() {
156            ma.stop_agent(&agent.id).await.unwrap_or_else(|e| {
157                log::error!("Failed to stop agent {}: {}", agent.id, e);
158            });
159        }
160        // Every agent has stopped; drop the fired parent token so a later
161        // start_agent derives a live token instead of a born-cancelled child
162        // that would silently skip all inputs.
163        ma.remove_preset_token(&self.id);
164        self.running = false;
165        Ok(())
166    }
167}
168
169/// Summary information about a preset.
170///
171/// A lightweight struct containing only essential preset metadata,
172/// useful for listing presets without loading full specifications.
173#[derive(Clone, Debug, Serialize, Deserialize)]
174pub struct PresetInfo {
175    /// Unique identifier of the preset.
176    pub id: String,
177
178    /// User-defined name of the preset, if set.
179    pub name: Option<String>,
180
181    /// Whether the preset is currently running.
182    pub running: bool,
183}
184
185impl From<&Preset> for PresetInfo {
186    fn from(preset: &Preset) -> Self {
187        Self {
188            id: preset.id.clone(),
189            name: preset.name.clone(),
190            running: preset.running,
191        }
192    }
193}