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        for agent in self.spec.agents.iter() {
134            if agent.disabled {
135                continue;
136            }
137            ma.start_agent(&agent.id).await.unwrap_or_else(|e| {
138                log::error!("Failed to start agent {}: {}", agent.id, e);
139            });
140        }
141
142        Ok(())
143    }
144
145    /// Stops all agents in this preset.
146    pub async fn stop(&mut self, ma: &ModularAgent) -> Result<(), AgentError> {
147        for agent in self.spec.agents.iter() {
148            ma.stop_agent(&agent.id).await.unwrap_or_else(|e| {
149                log::error!("Failed to stop agent {}: {}", agent.id, e);
150            });
151        }
152        self.running = false;
153        Ok(())
154    }
155}
156
157/// Summary information about a preset.
158///
159/// A lightweight struct containing only essential preset metadata,
160/// useful for listing presets without loading full specifications.
161#[derive(Clone, Debug, Serialize, Deserialize)]
162pub struct PresetInfo {
163    /// Unique identifier of the preset.
164    pub id: String,
165
166    /// User-defined name of the preset, if set.
167    pub name: Option<String>,
168
169    /// Whether the preset is currently running.
170    pub running: bool,
171}
172
173impl From<&Preset> for PresetInfo {
174    fn from(preset: &Preset) -> Self {
175        Self {
176            id: preset.id.clone(),
177            name: preset.name.clone(),
178            running: preset.running,
179        }
180    }
181}