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}