Skip to main content

modular_agent_core/
modular_agent.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::{Arc, Mutex, Weak};
3
4use serde_json::Value;
5use tokio::sync::{Mutex as AsyncMutex, broadcast, broadcast::error::RecvError, mpsc};
6use tokio_util::sync::CancellationToken;
7
8use crate::FnvIndexMap;
9use crate::agent::{Agent, AgentMessage, AgentStatus, agent_new};
10use crate::config::{AgentConfigs, AgentConfigsMap};
11use crate::context::AgentContext;
12use crate::definition::{AgentConfigSpecs, AgentDefinition, AgentDefinitions};
13use crate::error::AgentError;
14use crate::id::{new_id, update_ids};
15use crate::message::{self, AgentEventMessage};
16use crate::preset::{Preset, PresetInfo};
17use crate::registry;
18use crate::spec::{AgentSpec, ConnectionSpec, PresetSpec};
19use crate::value::AgentValue;
20
21const MESSAGE_LIMIT: usize = 1024;
22const EVENT_CHANNEL_CAPACITY: usize = 256;
23
24/// Registry size at which dead context-token entries are pruned. Entries are
25/// `Weak` and die with their flow (contexts hold the only strong references).
26/// The registry may exceed this threshold when more flows are genuinely live:
27/// live entries must remain tracked so every flow stays abortable.
28const CONTEXT_TOKEN_PRUNE_THRESHOLD: usize = 1024;
29
30/// Distinguishes which agent-loop incarnation owns the `agent_tokens` slot,
31/// so a draining old loop cannot clobber the token installed for a restarted
32/// agent's new loop (tokens themselves have no identity to compare).
33static AGENT_TOKEN_GENERATION: AtomicU64 = AtomicU64::new(1);
34
35/// The central orchestrator for the modular agent system.
36///
37/// `ModularAgent` manages agent lifecycle, connections, and message routing.
38/// It maintains agent instances, connection maps, and handles [`ModularAgentEvent`]s.
39///
40/// # Lifecycle
41///
42/// 1. [`init()`](Self::init) - Create instance and register agent definitions
43/// 2. [`ready()`](Self::ready) - Start the internal message loop
44/// 3. Load presets with [`open_preset_from_file()`](Self::open_preset_from_file) or [`add_preset()`](Self::add_preset)
45/// 4. [`start_preset()`](Self::start_preset) - Start agents in a preset
46/// 5. Interact via [`write_external_input()`](Self::write_external_input) and [`subscribe()`](Self::subscribe)
47/// 6. [`stop_preset()`](Self::stop_preset) - Stop agents
48/// 7. [`quit()`](Self::quit) - Shut down
49///
50/// # Example
51///
52#[cfg_attr(feature = "file", doc = "```rust,no_run")]
53#[cfg_attr(not(feature = "file"), doc = "```rust,no_run,ignore")]
54/// use modular_agent_core::{ModularAgent, AgentValue, ModularAgentEvent};
55///
56/// #[tokio::main]
57/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
58///     // Initialize and start
59///     let ma = ModularAgent::init()?;
60///     ma.ready().await?;
61///
62///     // Load a preset
63///     let preset_id = ma.open_preset_from_file("my_preset.json", None).await?;
64///     ma.start_preset(&preset_id).await?;
65///
66///     // Send external input
67///     ma.write_external_input("input".to_string(), AgentValue::string("hello")).await?;
68///
69///     // Cleanup
70///     ma.stop_preset(&preset_id).await?;
71///     ma.quit();
72///     Ok(())
73/// }
74/// ```
75/// Shared, lockable handle to a running agent instance.
76pub type SharedAgent = Arc<AsyncMutex<Box<dyn Agent>>>;
77
78// target agent id / source handle / target handle
79pub(crate) type ConnectionTarget = (String, String, String);
80
81#[derive(Clone)]
82pub struct ModularAgent {
83    // agent id -> agent
84    pub(crate) agents: Arc<Mutex<FnvIndexMap<String, SharedAgent>>>,
85
86    // agent id -> sender
87    pub(crate) agent_txs: Arc<Mutex<FnvIndexMap<String, mpsc::Sender<AgentMessage>>>>,
88
89    // channel name -> [external input agent id]
90    pub(crate) external_input_agents: Arc<Mutex<FnvIndexMap<String, Vec<String>>>>,
91
92    // channel name -> value
93    pub(crate) external_values: Arc<Mutex<FnvIndexMap<String, AgentValue>>>,
94
95    // source agent id -> [connection targets]
96    pub(crate) connections: Arc<Mutex<FnvIndexMap<String, Vec<ConnectionTarget>>>>,
97
98    // agent def name -> agent definition
99    pub(crate) defs: Arc<Mutex<AgentDefinitions>>,
100
101    // presets (preset id -> preset)
102    pub(crate) presets: Arc<Mutex<FnvIndexMap<String, Arc<AsyncMutex<Preset>>>>>,
103
104    /// name -> preset id: the single source of truth for preset name lookup
105    /// and uniqueness. Mutated only by `add_preset_raw`, `rename_preset`,
106    /// and `remove_preset`.
107    ///
108    /// Lock order: never acquire `presets` or a preset's async mutex while
109    /// holding this lock.
110    pub(crate) preset_names: Arc<Mutex<FnvIndexMap<String, String>>>,
111
112    // agent def name -> config
113    pub(crate) global_configs_map: Arc<Mutex<FnvIndexMap<String, AgentConfigs>>>,
114
115    // preset id -> parent cancellation token for the preset's agents
116    pub(crate) preset_tokens: Arc<Mutex<FnvIndexMap<String, CancellationToken>>>,
117
118    // agent id -> (loop generation, current cancellation token of that loop)
119    pub(crate) agent_tokens: Arc<Mutex<FnvIndexMap<String, (u64, CancellationToken)>>>,
120
121    // context id -> cancellation token (weak: dies with the flow's contexts)
122    pub(crate) context_tokens: Arc<Mutex<FnvIndexMap<usize, Weak<CancellationToken>>>>,
123
124    // message sender
125    pub(crate) tx: Arc<Mutex<Option<mpsc::Sender<AgentEventMessage>>>>,
126
127    // observers
128    pub(crate) observers: broadcast::Sender<EventEnvelope>,
129
130    /// Origin tag stamped onto the [`EventEnvelope`] of every event emitted
131    /// through this handle. Carried per clone (not shared) so tagged entry
132    /// points can coexist with the untagged handles produced by `base()`.
133    pub(crate) origin: Option<Arc<str>>,
134}
135
136impl Default for ModularAgent {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142impl ModularAgent {
143    /// Create a new `ModularAgent` instance without registering agents.
144    ///
145    /// For most use cases, prefer [`init()`](Self::init) which also registers
146    /// all agent definitions from the inventory.
147    pub fn new() -> Self {
148        let (tx, _rx) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
149        Self {
150            agents: Default::default(),
151            agent_txs: Default::default(),
152            external_input_agents: Default::default(),
153            external_values: Default::default(),
154            connections: Default::default(),
155            defs: Default::default(),
156            presets: Default::default(),
157            preset_names: Default::default(),
158            global_configs_map: Default::default(),
159            preset_tokens: Default::default(),
160            agent_tokens: Default::default(),
161            context_tokens: Default::default(),
162            tx: Arc::new(Mutex::new(None)),
163            observers: tx,
164            origin: None,
165        }
166    }
167
168    /// Returns a clone of this handle that stamps `origin` onto the
169    /// [`EventEnvelope`] of every event emitted through it.
170    ///
171    /// Use this to attribute changes made through a specific entry point
172    /// (e.g. a host UI or an external editing server) so subscribers can
173    /// distinguish them from runtime-originated events, which carry `None`.
174    pub fn with_origin(&self, origin: impl Into<Arc<str>>) -> Self {
175        Self {
176            origin: Some(origin.into()),
177            ..self.clone()
178        }
179    }
180
181    /// Returns a clone of this handle with no origin tag.
182    ///
183    /// Invariant: every handle stored beyond the current call (agent data,
184    /// spawned loops) must be created through this method. Otherwise runtime
185    /// events emitted later would be attributed to whichever tagged entry
186    /// point happened to create the agent or loop.
187    pub(crate) fn base(&self) -> Self {
188        Self {
189            origin: None,
190            ..self.clone()
191        }
192    }
193
194    pub(crate) fn tx(&self) -> Result<mpsc::Sender<AgentEventMessage>, AgentError> {
195        self.tx
196            .lock()
197            .unwrap()
198            .clone()
199            .ok_or(AgentError::TxNotInitialized)
200    }
201
202    /// Initialize a new `ModularAgent` instance.
203    ///
204    /// This creates a new `ModularAgent` and registers all available agent definitions
205    /// from the inventory. Call [`ready`](Self::ready) after this to start the message loop.
206    ///
207    /// # Example
208    ///
209    /// ```rust,no_run
210    /// use modular_agent_core::ModularAgent;
211    ///
212    /// let ma = ModularAgent::init().unwrap();
213    /// ```
214    pub fn init() -> Result<Self, AgentError> {
215        let ma = Self::new();
216        ma.register_agents();
217        Ok(ma)
218    }
219
220    fn register_agents(&self) {
221        registry::register_inventory_agents(self);
222    }
223
224    /// Start the internal message loop.
225    ///
226    /// This must be called after [`init`](Self::init) before loading presets or sending messages.
227    /// The message loop handles routing between agents and external output events.
228    ///
229    /// # Example
230    ///
231    /// ```rust,no_run
232    /// use modular_agent_core::ModularAgent;
233    ///
234    /// #[tokio::main]
235    /// async fn main() {
236    ///     let ma = ModularAgent::init().unwrap();
237    ///     ma.ready().await.unwrap(); // Start the message loop
238    /// }
239    /// ```
240    pub async fn ready(&self) -> Result<(), AgentError> {
241        self.spawn_message_loop().await?;
242        Ok(())
243    }
244
245    /// Shut down the `ModularAgent`.
246    ///
247    /// This stops the internal message loop. Call [`stop_preset`](Self::stop_preset)
248    /// for each running preset before calling this method for graceful shutdown.
249    ///
250    /// This does not release external resources such as MCP server child processes.
251    /// Use [`shutdown`](Self::shutdown) instead when full cleanup is required.
252    ///
253    /// # Example
254    ///
255    /// ```rust,no_run
256    /// # use modular_agent_core::ModularAgent;
257    /// # async fn example(ma: ModularAgent, preset_id: &str) {
258    /// // Stop all presets first
259    /// ma.stop_preset(preset_id).await.unwrap();
260    /// // Then quit
261    /// ma.quit();
262    /// # }
263    /// ```
264    pub fn quit(&self) {
265        let mut tx_lock = self.tx.lock().unwrap();
266        *tx_lock = None;
267    }
268
269    /// Shut down the `ModularAgent` and release external resources.
270    ///
271    /// Calls [`quit`](Self::quit) to stop the internal message loop, then closes any
272    /// pooled MCP server connections so their child processes do not leak. Call
273    /// [`stop_preset`](Self::stop_preset) for each running preset before this method.
274    /// An MCP tool call still in flight during shutdown may reconnect and respawn its
275    /// server process afterwards, so quiesce all workflows first.
276    ///
277    /// # Example
278    ///
279    /// ```rust,no_run
280    /// # use modular_agent_core::ModularAgent;
281    /// # async fn example(ma: ModularAgent, preset_id: &str) {
282    /// ma.stop_preset(preset_id).await.unwrap();
283    /// ma.shutdown().await.unwrap();
284    /// # }
285    /// ```
286    pub async fn shutdown(&self) -> Result<(), AgentError> {
287        self.quit();
288        #[cfg(feature = "mcp")]
289        crate::mcp::shutdown_all_mcp_connections().await?;
290        Ok(())
291    }
292
293    // Preset management
294
295    /// Create a new empty preset.
296    ///
297    /// Returns the id of the new preset. The preset is created with default settings
298    /// and contains no agents or connections initially.
299    pub fn new_preset(&self) -> Result<String, AgentError> {
300        let spec = PresetSpec::default();
301        let id = self.add_preset(spec)?;
302        Ok(id)
303    }
304
305    /// Create a new empty preset with the given name.
306    ///
307    /// Returns the id of the new preset.
308    pub fn new_preset_with_name(&self, name: String) -> Result<String, AgentError> {
309        let spec = PresetSpec::default();
310        let id = self.add_preset_with_name(spec, name)?;
311        Ok(id)
312    }
313
314    /// Get a preset by id.
315    ///
316    /// Returns `None` if no preset exists with the given id.
317    pub fn get_preset(&self, id: &str) -> Option<Arc<AsyncMutex<Preset>>> {
318        let presets = self.presets.lock().unwrap();
319        presets.get(id).cloned()
320    }
321
322    /// Find the id of a live preset by its name.
323    ///
324    /// Returns `None` when no preset with the given name is loaded.
325    pub fn find_preset_id_by_name(&self, name: &str) -> Option<String> {
326        let names = self.preset_names.lock().unwrap();
327        names.get(name).cloned()
328    }
329
330    /// Add a new preset with the given spec, and returns the id of the new preset.
331    ///
332    /// The ids of the given spec, including agents and connections, are changed to new unique ids.
333    /// This allows the same spec to be added multiple times without id conflicts.
334    pub fn add_preset(&self, spec: PresetSpec) -> Result<String, AgentError> {
335        self.add_preset_raw(spec, None)
336    }
337
338    /// Add a new preset with the given name and spec, and returns the id of the new preset.
339    ///
340    /// The ids of the given spec, including agents and connections, are changed to new unique ids.
341    pub fn add_preset_with_name(
342        &self,
343        spec: PresetSpec,
344        name: String,
345    ) -> Result<String, AgentError> {
346        self.add_preset_raw(spec, Some(name))
347    }
348
349    fn add_preset_raw(&self, spec: PresetSpec, name: Option<String>) -> Result<String, AgentError> {
350        let mut preset = Preset::new(spec);
351        if let Some(name) = &name {
352            preset.set_name(name.clone());
353        }
354        let id = preset.id().to_string();
355
356        // Reserve the name first so a duplicate fails before any agents are
357        // created; the reservation is rolled back if a later step fails.
358        if let Some(name) = &name {
359            let mut names = self.preset_names.lock().unwrap();
360            if names.contains_key(name) {
361                return Err(AgentError::PresetNameExists(name.clone()));
362            }
363            names.insert(name.clone(), id.clone());
364        }
365
366        // add agents
367        for agent in &preset.spec().agents {
368            if let Err(e) = self.add_agent_internal(id.clone(), agent.clone()) {
369                log::error!("Failed to add_agent {}: {}", agent.id, e);
370            }
371        }
372
373        // add connections
374        for connection in &preset.spec().connections {
375            self.add_connection_internal(connection.clone())
376                .unwrap_or_else(|e| {
377                    log::error!("Failed to add_connection {}: {}", connection.source, e);
378                });
379        }
380
381        // add the given preset into presets
382        let inserted = {
383            let mut presets = self.presets.lock().unwrap();
384            if presets.contains_key(&id) {
385                false
386            } else {
387                presets.insert(id.clone(), Arc::new(AsyncMutex::new(preset)));
388                true
389            }
390        };
391        if !inserted {
392            if let Some(name) = &name {
393                self.preset_names.lock().unwrap().swap_remove(name);
394            }
395            return Err(AgentError::DuplicateId(id));
396        }
397
398        self.emit_preset_added(id.clone(), name);
399
400        Ok(id)
401    }
402
403    /// Rename a preset by id.
404    ///
405    /// Fails with [`AgentError::PresetNameExists`] when another preset
406    /// already uses `new_name`. Renaming a preset to its current name is a
407    /// no-op and succeeds. Emits [`ModularAgentEvent::PresetRenamed`].
408    pub async fn rename_preset(&self, id: &str, new_name: String) -> Result<(), AgentError> {
409        let preset = self
410            .get_preset(id)
411            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
412
413        {
414            let mut names = self.preset_names.lock().unwrap();
415            if let Some(owner) = names.get(&new_name)
416                && owner != id
417            {
418                return Err(AgentError::PresetNameExists(new_name));
419            }
420            // Remove by id so a previously unnamed preset gaining its first
421            // name is handled too.
422            names.retain(|_, v| v != id);
423            names.insert(new_name.clone(), id.to_string());
424        }
425
426        // Re-check liveness after reserving the name: a concurrent
427        // remove_preset may have completed (including its name-index
428        // cleanup) between get_preset above and the insert, which would
429        // leave the new entry pointing at a dead id forever. The lock-order
430        // rule (never take `presets` while holding `preset_names`) forces
431        // this check to come after the insert; either remove_preset's
432        // cleanup runs after our insert and clears it, or we observe the id
433        // gone here and roll the reservation back.
434        if !self.presets.lock().unwrap().contains_key(id) {
435            let mut names = self.preset_names.lock().unwrap();
436            if names.get(&new_name).is_some_and(|owner| owner == id) {
437                names.swap_remove(&new_name);
438            }
439            return Err(AgentError::PresetNotFound(id.to_string()));
440        }
441
442        let old_name = {
443            let mut preset = preset.lock().await;
444            let old_name = preset.name().map(str::to_string);
445            preset.set_name(new_name.clone());
446            old_name
447        };
448        self.emit_preset_renamed(id.to_string(), old_name, new_name);
449        Ok(())
450    }
451
452    /// Remove a preset by id.
453    ///
454    /// Stops the preset if running, then removes all associated agents and connections.
455    /// Emits [`ModularAgentEvent::PresetRemoved`] after teardown.
456    pub async fn remove_preset(&self, id: &str) -> Result<(), AgentError> {
457        let preset = self
458            .get_preset(id)
459            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
460
461        let mut preset = preset.lock().await;
462        let name = preset.name().map(str::to_string);
463        preset.stop(self).await.unwrap_or_else(|e| {
464            log::error!("Failed to stop preset {}: {}", id, e);
465        });
466
467        // Remove all agents and connections associated with the preset
468        for agent in &preset.spec().agents {
469            self.remove_agent_internal(&agent.id)
470                .await
471                .unwrap_or_else(|e| {
472                    log::error!("Failed to remove_agent {}: {}", agent.id, e);
473                });
474        }
475        for connection in &preset.spec().connections {
476            self.remove_connection_internal(connection);
477        }
478
479        // Drop the preset lock before modifying the presets map
480        drop(preset);
481
482        // Remove the preset entry from the map
483        {
484            let mut presets = self.presets.lock().unwrap();
485            presets.swap_remove(id);
486        }
487        self.preset_names.lock().unwrap().retain(|_, v| v != id);
488        self.remove_preset_token(id);
489
490        self.emit_preset_removed(id.to_string(), name);
491
492        Ok(())
493    }
494
495    /// Start a preset by id.
496    ///
497    /// This starts all agents in the preset, enabling message flow between them.
498    /// Each agent's [`start()`](crate::AsAgent::start) method is called.
499    pub async fn start_preset(&self, id: &str) -> Result<(), AgentError> {
500        let preset = self
501            .get_preset(id)
502            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
503        let mut preset = preset.lock().await;
504        preset.start(self).await?;
505
506        Ok(())
507    }
508
509    /// Stop a preset by id.
510    ///
511    /// This stops all agents in the preset, terminating message processing.
512    /// Each agent's [`stop()`](crate::AsAgent::stop) method is called.
513    pub async fn stop_preset(&self, id: &str) -> Result<(), AgentError> {
514        let preset = self
515            .get_preset(id)
516            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
517        let mut preset = preset.lock().await;
518        preset.stop(self).await?;
519
520        Ok(())
521    }
522
523    /// Open a preset from a JSON file.
524    ///
525    /// Reads the file, parses the JSON as a [`PresetSpec`], and adds it to the system.
526    /// Optionally provide a custom name for the preset.
527    ///
528    /// # Arguments
529    ///
530    /// * `path` - Path to the JSON preset file
531    /// * `name` - Optional custom name for the preset
532    #[cfg(feature = "file")]
533    pub async fn open_preset_from_file(
534        &self,
535        path: &str,
536        name: Option<String>,
537    ) -> Result<String, AgentError> {
538        let json_str =
539            std::fs::read_to_string(path).map_err(|e| AgentError::IoError(e.to_string()))?;
540        let spec = PresetSpec::from_json(&json_str)?;
541        let id = self.add_preset_raw(spec, name)?;
542        Ok(id)
543    }
544
545    /// Save a preset to a JSON file.
546    ///
547    /// Serializes the current preset state (including agent configs) to JSON
548    /// and writes it to the specified path. Emits
549    /// [`ModularAgentEvent::PresetSaved`] when the preset has a name; unnamed
550    /// presets have no list entry to refresh, so no event is emitted for them.
551    #[cfg(feature = "file")]
552    pub async fn save_preset(&self, id: &str, path: &str) -> Result<(), AgentError> {
553        let Some(preset_spec) = self.get_preset_spec(id).await else {
554            return Err(AgentError::PresetNotFound(id.to_string()));
555        };
556        let json_str = preset_spec.to_json()?;
557        std::fs::write(path, json_str).map_err(|e| AgentError::IoError(e.to_string()))?;
558        if let Some(name) = self.get_preset_info(id).await.and_then(|info| info.name) {
559            self.emit_preset_saved(id.to_string(), name);
560        }
561        Ok(())
562    }
563
564    // PresetSpec
565
566    /// Get the current preset spec by id.
567    pub async fn get_preset_spec(&self, id: &str) -> Option<PresetSpec> {
568        let preset = self.get_preset(id)?;
569        let mut preset_spec = {
570            let preset = preset.lock().await;
571            preset.spec().clone()
572        };
573
574        // collect current agent specs in the preset
575        let mut agent_specs = Vec::new();
576        for agent in &preset_spec.agents {
577            if let Some(spec) = self.get_agent_spec(&agent.id).await {
578                agent_specs.push(spec);
579            }
580        }
581        preset_spec.agents = agent_specs;
582
583        // No need to change connections
584
585        Some(preset_spec)
586    }
587
588    /// Update the preset spec
589    pub async fn update_preset_spec(&self, id: &str, value: &Value) -> Result<(), AgentError> {
590        let preset = self
591            .get_preset(id)
592            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
593        let mut preset = preset.lock().await;
594        preset.update_spec(value)?;
595        drop(preset);
596        self.emit_preset_structure_changed(id.to_string());
597        Ok(())
598    }
599
600    // PresetInfo
601
602    /// Get info of the preset by id.
603    pub async fn get_preset_info(&self, id: &str) -> Option<PresetInfo> {
604        let preset = self.get_preset(id)?;
605        Some(PresetInfo::from(&*preset.lock().await))
606    }
607
608    /// Get infos of all presets.
609    pub async fn get_preset_infos(&self) -> Vec<PresetInfo> {
610        let presets = {
611            let presets = self.presets.lock().unwrap();
612            presets.values().cloned().collect::<Vec<_>>()
613        };
614        let mut preset_infos = Vec::new();
615        for preset in presets {
616            let preset_guard = preset.lock().await;
617            preset_infos.push(PresetInfo::from(&*preset_guard));
618        }
619        preset_infos
620    }
621
622    // Agents
623
624    /// Register an agent definition.
625    ///
626    /// This makes the agent type available for use in presets. The definition
627    /// includes metadata (title, category), input/output ports, and config specs.
628    ///
629    /// Note: Agents using `#[modular_agent]` macro are registered automatically via inventory.
630    pub fn register_agent_definiton(&self, def: AgentDefinition) {
631        let def_name = def.name.clone();
632        let def_global_configs = def.global_configs.clone();
633
634        let mut defs = self.defs.lock().unwrap();
635        defs.insert(def.name.clone(), def);
636
637        // if there is a global config, set it
638        if let Some(def_global_configs) = def_global_configs {
639            let mut new_configs = AgentConfigs::default();
640            for (key, config_entry) in def_global_configs.iter() {
641                new_configs.set(key.clone(), config_entry.value.clone());
642            }
643            self.set_global_configs(def_name, new_configs);
644        }
645    }
646
647    /// Get all registered agent definitions.
648    ///
649    /// Returns a map of definition name to [`AgentDefinition`].
650    pub fn get_agent_definitions(&self) -> AgentDefinitions {
651        let defs = self.defs.lock().unwrap();
652        defs.clone()
653    }
654
655    /// Get an agent definition by name.
656    ///
657    /// The name is typically in the format `module::path::StructName`.
658    pub fn get_agent_definition(&self, def_name: &str) -> Option<AgentDefinition> {
659        let defs = self.defs.lock().unwrap();
660        defs.get(def_name).cloned()
661    }
662
663    /// Get the config specs of an agent definition by name.
664    pub fn get_agent_config_specs(&self, def_name: &str) -> Option<AgentConfigSpecs> {
665        let defs = self.defs.lock().unwrap();
666        let def = defs.get(def_name)?;
667        def.configs.clone()
668    }
669
670    /// Get the agent spec by id.
671    pub async fn get_agent_spec(&self, agent_id: &str) -> Option<AgentSpec> {
672        let agent = {
673            let agents = self.agents.lock().unwrap();
674            agents.get(agent_id)?.clone()
675        };
676        let agent = agent.lock().await;
677        Some(agent.spec().clone())
678    }
679
680    /// Update the agent spec by id.
681    ///
682    /// Emits [`ModularAgentEvent::AgentSpecUpdated`], and additionally
683    /// [`ModularAgentEvent::PresetStructureChanged`] when the patch contains
684    /// keys other than `configs`.
685    pub async fn update_agent_spec(&self, agent_id: &str, value: &Value) -> Result<(), AgentError> {
686        let agent = {
687            let agents = self.agents.lock().unwrap();
688            let Some(agent) = agents.get(agent_id) else {
689                return Err(AgentError::AgentNotFound(agent_id.to_string()));
690            };
691            agent.clone()
692        };
693        let preset_id = {
694            let mut agent = agent.lock().await;
695            agent.update_spec(value)?;
696            agent.preset_id().to_string()
697        };
698
699        self.emit_agent_spec_updated(agent_id.to_string());
700
701        // Any non-config key (ports, title, layout, ...) may change how hosts
702        // render the preset, so treat those patches as structural. Config-only
703        // patches stay quiet here; they are covered by AgentSpecUpdated.
704        let structural = value
705            .as_object()
706            .is_some_and(|map| map.keys().any(|key| key != "configs"));
707        if structural {
708            self.emit_preset_structure_changed(preset_id);
709        }
710        Ok(())
711    }
712
713    /// Create a new agent spec from the given agent definition name.
714    pub fn new_agent_spec(&self, def_name: &str) -> Result<AgentSpec, AgentError> {
715        let def = self
716            .get_agent_definition(def_name)
717            .ok_or_else(|| AgentError::AgentDefinitionNotFound(def_name.to_string()))?;
718        Ok(def.to_spec())
719    }
720
721    /// Add an agent to the specified preset.
722    ///
723    /// Creates a new agent instance from the given spec and adds it to the preset.
724    /// Returns the id of the newly created agent. The agent is not started automatically;
725    /// call [`start_preset`](Self::start_preset) or [`start_agent`](Self::start_agent) to start it.
726    pub async fn add_agent(
727        &self,
728        preset_id: String,
729        mut spec: AgentSpec,
730    ) -> Result<String, AgentError> {
731        let preset = self
732            .get_preset(&preset_id)
733            .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
734
735        let id = new_id();
736        spec.id = id.clone();
737        self.add_agent_internal(preset_id.clone(), spec.clone())?;
738
739        let mut preset = preset.lock().await;
740        preset.add_agent(spec.clone());
741        drop(preset);
742
743        self.emit_preset_structure_changed(preset_id);
744
745        Ok(id)
746    }
747
748    fn add_agent_internal(&self, preset_id: String, spec: AgentSpec) -> Result<(), AgentError> {
749        let mut agents = self.agents.lock().unwrap();
750        if agents.contains_key(&spec.id) {
751            return Err(AgentError::AgentAlreadyExists(spec.id.to_string()));
752        }
753        let spec_id = spec.id.clone();
754        // base(): the agent keeps this handle for its lifetime, so runtime
755        // events it emits later must not inherit the creator's origin tag.
756        let mut agent = agent_new(self.base(), spec_id.clone(), spec)?;
757        agent.set_preset_id(preset_id);
758        agents.insert(spec_id, Arc::new(AsyncMutex::new(agent)));
759        Ok(())
760    }
761
762    /// Get the agent by id.
763    pub fn get_agent(&self, agent_id: &str) -> Option<SharedAgent> {
764        let agents = self.agents.lock().unwrap();
765        agents.get(agent_id).cloned()
766    }
767
768    /// Add a connection between two agents in the specified preset.
769    ///
770    /// When the source agent outputs a value on the source handle (port),
771    /// it will be delivered to the target agent's target handle (port).
772    pub async fn add_connection(
773        &self,
774        preset_id: &str,
775        connection: ConnectionSpec,
776    ) -> Result<(), AgentError> {
777        // check if the source and target agents exist
778        {
779            let agents = self.agents.lock().unwrap();
780            if !agents.contains_key(&connection.source) {
781                return Err(AgentError::AgentNotFound(connection.source.to_string()));
782            }
783            if !agents.contains_key(&connection.target) {
784                return Err(AgentError::AgentNotFound(connection.target.to_string()));
785            }
786        }
787
788        // check if handles are valid
789        if connection.source_handle.is_empty() {
790            return Err(AgentError::EmptySourceHandle);
791        }
792        if connection.target_handle.is_empty() {
793            return Err(AgentError::EmptyTargetHandle);
794        }
795
796        let preset = self
797            .get_preset(preset_id)
798            .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
799        let mut preset = preset.lock().await;
800        // Register the routing entry first: it is the fallible step
801        // (duplicate detection), and a failure must leave the preset spec
802        // untouched so no spec change ever goes unannounced.
803        self.add_connection_internal(connection.clone())?;
804        preset.add_connection(connection);
805        drop(preset);
806        self.emit_preset_structure_changed(preset_id.to_string());
807        Ok(())
808    }
809
810    fn add_connection_internal(&self, connection: ConnectionSpec) -> Result<(), AgentError> {
811        let mut connections = self.connections.lock().unwrap();
812        if let Some(targets) = connections.get_mut(&connection.source) {
813            if targets
814                .iter()
815                .any(|(target, source_handle, target_handle)| {
816                    *target == connection.target
817                        && *source_handle == connection.source_handle
818                        && *target_handle == connection.target_handle
819                })
820            {
821                return Err(AgentError::ConnectionAlreadyExists);
822            }
823            targets.push((
824                connection.target,
825                connection.source_handle,
826                connection.target_handle,
827            ));
828        } else {
829            connections.insert(
830                connection.source,
831                vec![(
832                    connection.target,
833                    connection.source_handle,
834                    connection.target_handle,
835                )],
836            );
837        }
838        Ok(())
839    }
840
841    /// Returns true if any connection originates from `source_agent`'s `port`.
842    ///
843    /// Producers can use this to skip building expensive values for ports
844    /// nobody listens to; `agent_out` would only drop them after the
845    /// conversion cost has already been paid.
846    pub fn has_connections(&self, source_agent: &str, port: &str) -> bool {
847        let connections = self.connections.lock().unwrap();
848        connections.get(source_agent).is_some_and(|targets| {
849            targets
850                .iter()
851                .any(|(_, source_port, _)| source_port == port)
852        })
853    }
854
855    /// Add agents and connections to the specified preset.
856    ///
857    /// The ids of the given agents and connections are changed to new unique ids.
858    /// The agents are not started automatically, even if the preset is running.
859    pub async fn add_agents_and_connections(
860        &self,
861        preset_id: &str,
862        agents: &Vec<AgentSpec>,
863        connections: &Vec<ConnectionSpec>,
864    ) -> Result<(Vec<AgentSpec>, Vec<ConnectionSpec>), AgentError> {
865        let (agents, connections) = update_ids(agents, connections);
866
867        let preset = self
868            .get_preset(preset_id)
869            .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
870        let mut preset = preset.lock().await;
871
872        // Track progress so a mid-batch failure can be rolled back: a
873        // partial batch must not leave agents in the spec (or the runtime
874        // maps) while returning an error without any event.
875        let mut added_agents = 0;
876        let mut added_connections = 0;
877        let mut result = Ok(());
878
879        for agent in &agents {
880            if let Err(e) = self.add_agent_internal(preset_id.to_string(), agent.clone()) {
881                result = Err(e);
882                break;
883            }
884            preset.add_agent(agent.clone());
885            added_agents += 1;
886        }
887
888        if result.is_ok() {
889            for connection in &connections {
890                if let Err(e) = self.add_connection_internal(connection.clone()) {
891                    result = Err(e);
892                    break;
893                }
894                preset.add_connection(connection.clone());
895                added_connections += 1;
896            }
897        }
898
899        if let Err(e) = result {
900            for connection in connections.iter().take(added_connections) {
901                preset.remove_connection(connection);
902                self.remove_connection_internal(connection);
903            }
904            // The rolled-back agents were never started, so no stop or
905            // channel teardown is needed; dropping the map entries undoes
906            // add_agent_internal completely.
907            let mut agents_map = self.agents.lock().unwrap();
908            for agent in agents.iter().take(added_agents) {
909                preset.remove_agent(&agent.id);
910                agents_map.swap_remove(&agent.id);
911            }
912            return Err(e);
913        }
914        drop(preset);
915
916        self.emit_preset_structure_changed(preset_id.to_string());
917
918        Ok((agents, connections))
919    }
920
921    /// Remove an agent from the specified preset.
922    ///
923    /// If the agent is running, it will be stopped first.
924    pub async fn remove_agent(&self, preset_id: &str, agent_id: &str) -> Result<(), AgentError> {
925        let preset = self
926            .get_preset(preset_id)
927            .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
928
929        // Tear down the runtime instance before touching the spec so a
930        // failure leaves the spec unchanged and no spec change ever goes
931        // unannounced. An agent can exist in the spec without a runtime
932        // instance (its definition was unknown when the preset was added);
933        // such an agent is still removable from the spec.
934        let runtime_removed = match self.remove_agent_internal(agent_id).await {
935            Ok(()) => true,
936            Err(AgentError::AgentNotFound(_)) => false,
937            Err(e) => return Err(e),
938        };
939
940        let spec_removed = {
941            let mut preset = preset.lock().await;
942            let count_before = preset.spec().agents.len();
943            preset.remove_agent(agent_id);
944            preset.spec().agents.len() != count_before
945        };
946
947        if !runtime_removed && !spec_removed {
948            return Err(AgentError::AgentNotFound(agent_id.to_string()));
949        }
950        self.emit_preset_structure_changed(preset_id.to_string());
951        Ok(())
952    }
953
954    async fn remove_agent_internal(&self, agent_id: &str) -> Result<(), AgentError> {
955        self.stop_agent(agent_id).await?;
956
957        // remove from connections
958        {
959            let mut connections = self.connections.lock().unwrap();
960            let mut sources_to_remove = Vec::new();
961            for (source, targets) in connections.iter_mut() {
962                targets.retain(|(target, _, _)| target != agent_id);
963                if targets.is_empty() {
964                    sources_to_remove.push(source.clone());
965                }
966            }
967            for source in sources_to_remove {
968                connections.swap_remove(&source);
969            }
970            connections.swap_remove(agent_id);
971        }
972
973        // remove from agents
974        {
975            let mut agents = self.agents.lock().unwrap();
976            agents.swap_remove(agent_id);
977        }
978
979        Ok(())
980    }
981
982    /// Remove a connection from the specified preset.
983    pub async fn remove_connection(
984        &self,
985        preset_id: &str,
986        connection: &ConnectionSpec,
987    ) -> Result<(), AgentError> {
988        let preset = self
989            .get_preset(preset_id)
990            .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
991        let mut preset = preset.lock().await;
992        let Some(connection) = preset.remove_connection(connection) else {
993            return Err(AgentError::ConnectionNotFound(format!(
994                "{}:{}->{}:{}",
995                connection.source,
996                connection.source_handle,
997                connection.target,
998                connection.target_handle
999            )));
1000        };
1001        self.remove_connection_internal(&connection);
1002        drop(preset);
1003        self.emit_preset_structure_changed(preset_id.to_string());
1004        Ok(())
1005    }
1006
1007    fn remove_connection_internal(&self, connection: &ConnectionSpec) {
1008        let mut connections = self.connections.lock().unwrap();
1009        if let Some(targets) = connections.get_mut(&connection.source) {
1010            targets.retain(|(target, source_handle, target_handle)| {
1011                *target != connection.target
1012                    || *source_handle != connection.source_handle
1013                    || *target_handle != connection.target_handle
1014            });
1015            if targets.is_empty() {
1016                connections.swap_remove(&connection.source);
1017            }
1018        }
1019    }
1020
1021    // Cancellation tokens
1022
1023    /// Returns the parent cancellation token for a preset, creating it if needed.
1024    fn preset_token(&self, preset_id: &str) -> CancellationToken {
1025        let mut tokens = self.preset_tokens.lock().unwrap();
1026        tokens.entry(preset_id.to_string()).or_default().clone()
1027    }
1028
1029    /// Installs a fresh (uncancelled) parent token for a preset.
1030    ///
1031    /// A fired `CancellationToken` cannot be reset, so this is called when a
1032    /// preset starts to replace the token cancelled by a previous stop.
1033    pub(crate) fn reset_preset_token(&self, preset_id: &str) {
1034        let mut tokens = self.preset_tokens.lock().unwrap();
1035        tokens.insert(preset_id.to_string(), CancellationToken::new());
1036    }
1037
1038    /// Cancels the preset's parent token, aborting the in-flight `process()`
1039    /// of every agent in the preset at once.
1040    ///
1041    /// The entry is kept (in its cancelled state) for the duration of the
1042    /// stop sequence so agent tokens renewed while agents are still being
1043    /// stopped are born cancelled and queued inputs are skipped instead of
1044    /// processed. [`Preset::stop`](crate::preset::Preset::stop) removes the
1045    /// entry once every agent has stopped, so a later `start_agent` derives
1046    /// a live token instead of a child of the fired one.
1047    pub(crate) fn cancel_preset_token(&self, preset_id: &str) {
1048        let token = self.preset_tokens.lock().unwrap().get(preset_id).cloned();
1049        if let Some(token) = token {
1050            token.cancel();
1051        }
1052    }
1053
1054    pub(crate) fn remove_preset_token(&self, preset_id: &str) {
1055        self.preset_tokens.lock().unwrap().swap_remove(preset_id);
1056    }
1057
1058    /// Creates and tracks a fresh cancellation token for an agent as a child
1059    /// of its preset's parent token. The returned generation identifies the
1060    /// agent-loop incarnation that owns the slot.
1061    fn create_agent_token(&self, preset_id: &str, agent_id: &str) -> (u64, CancellationToken) {
1062        let generation = AGENT_TOKEN_GENERATION.fetch_add(1, Ordering::Relaxed);
1063        let token = self.preset_token(preset_id).child_token();
1064        self.agent_tokens
1065            .lock()
1066            .unwrap()
1067            .insert(agent_id.to_string(), (generation, token.clone()));
1068        (generation, token)
1069    }
1070
1071    /// Replaces a fired agent token with a fresh child of the preset token.
1072    ///
1073    /// Called by the agent loop after its token fired. Returns `None` when
1074    /// the slot no longer belongs to the calling loop — either
1075    /// [`stop_agent`](Self::stop_agent) removed the entry, a restarted
1076    /// agent's new loop installed its own token (different generation), or
1077    /// the whole preset was removed. The caller then keeps its fired token
1078    /// so queued inputs are skipped until the `Stop` message arrives.
1079    fn renew_agent_token(
1080        &self,
1081        preset_id: &str,
1082        agent_id: &str,
1083        generation: u64,
1084    ) -> Option<CancellationToken> {
1085        // Look up (never create) the parent: a lagging loop must not
1086        // resurrect the token entry of a removed preset.
1087        let parent = self.preset_tokens.lock().unwrap().get(preset_id).cloned()?;
1088        let fresh = parent.child_token();
1089        let mut tokens = self.agent_tokens.lock().unwrap();
1090        let slot = tokens.get_mut(agent_id)?;
1091        if slot.0 != generation {
1092            return None;
1093        }
1094        slot.1 = fresh.clone();
1095        Some(fresh)
1096    }
1097
1098    /// Returns the cancellation token for a context, creating it if needed.
1099    ///
1100    /// The registry holds `Weak` references: an entry dies when the flow's
1101    /// last context clone is dropped, so lookups for finished flows fail and
1102    /// dead entries can be pruned. Pruning starts once the registry reaches
1103    /// [`CONTEXT_TOKEN_PRUNE_THRESHOLD`], but live entries are never evicted.
1104    pub(crate) fn context_token(&self, ctx_id: usize) -> Arc<CancellationToken> {
1105        let mut tokens = self.context_tokens.lock().unwrap();
1106        if let Some(token) = tokens.get(&ctx_id).and_then(Weak::upgrade) {
1107            return token;
1108        }
1109        if tokens.len() >= CONTEXT_TOKEN_PRUNE_THRESHOLD {
1110            tokens.retain(|_, weak| weak.strong_count() > 0);
1111        }
1112        let token = Arc::new(CancellationToken::new());
1113        tokens.insert(ctx_id, Arc::downgrade(&token));
1114        token
1115    }
1116
1117    /// Aborts the flow identified by `ctx_id`.
1118    ///
1119    /// Cancels the context's cancellation token, which every agent handling
1120    /// the flow received via [`AgentContext::cancel_token`]. Cancellation is
1121    /// cooperative for work already in flight: agents that `select!` on the
1122    /// token (LLM streaming loops, [`PresetToolAgent`](crate::tool::PresetToolAgent)
1123    /// result waits) abort promptly with [`AgentError::Cancelled`], while
1124    /// agents that ignore it run to completion. Inputs dispatched after the
1125    /// token fires are skipped before `process()` is called. The cancelled
1126    /// token stays alive as long as any context of the flow does, so queued
1127    /// and cyclic inputs for the flow are skipped too.
1128    ///
1129    /// Returns `false` when no live flow is tracked under `ctx_id` (the flow
1130    /// already finished, or never reached an agent): nothing is cancelled.
1131    pub fn abort_context(&self, ctx_id: usize) -> bool {
1132        let token = self
1133            .context_tokens
1134            .lock()
1135            .unwrap()
1136            .get(&ctx_id)
1137            .and_then(Weak::upgrade);
1138        match token {
1139            Some(token) => {
1140                token.cancel();
1141                true
1142            }
1143            None => {
1144                log::warn!("abort_context: no live flow for context {}", ctx_id);
1145                false
1146            }
1147        }
1148    }
1149
1150    /// Start an agent by id.
1151    ///
1152    /// Creates a message channel for the agent and spawns its event loop.
1153    /// The agent's [`start()`](crate::AsAgent::start) method is called, then
1154    /// the agent begins processing incoming messages.
1155    ///
1156    /// If the agent's definition has `native_thread = true`, the agent runs
1157    /// on a dedicated OS thread instead of the tokio runtime.
1158    pub async fn start_agent(&self, agent_id: &str) -> Result<(), AgentError> {
1159        let agent = {
1160            let agents = self.agents.lock().unwrap();
1161            let Some(a) = agents.get(agent_id) else {
1162                return Err(AgentError::AgentNotFound(agent_id.to_string()));
1163            };
1164            a.clone()
1165        };
1166        let (def_name, preset_id) = {
1167            let agent = agent.lock().await;
1168            (agent.def_name().to_string(), agent.preset_id().to_string())
1169        };
1170        let uses_native_thread = {
1171            let defs = self.defs.lock().unwrap();
1172            let Some(def) = defs.get(&def_name) else {
1173                return Err(AgentError::AgentDefinitionNotFound(agent_id.to_string()));
1174            };
1175            def.native_thread
1176        };
1177        let agent_status = {
1178            // This will not block since the agent is not started yet.
1179            let agent = agent.lock().await;
1180            agent.status().clone()
1181        };
1182        if agent_status == AgentStatus::Init {
1183            log::info!("Starting agent {}", agent_id);
1184
1185            let (tx, mut rx) = mpsc::channel(MESSAGE_LIMIT);
1186
1187            {
1188                let mut agent_txs = self.agent_txs.lock().unwrap();
1189                agent_txs.insert(agent_id.to_string(), tx.clone());
1190            };
1191
1192            let agent_clone = agent.clone();
1193            let agent_id_clone = agent_id.to_string();
1194            // base(): the agent loop outlives this call, so it must not
1195            // stamp runtime events with the caller's origin.
1196            let ma = self.base();
1197            // Created before spawning so stop_agent can cancel it immediately.
1198            let (generation, mut token) = self.create_agent_token(&preset_id, agent_id);
1199
1200            let agent_loop = async move {
1201                // Race start() against the token too: a start() stuck on
1202                // slow I/O holds the agent lock, and without the race
1203                // stop_agent would block on that lock until start() returns
1204                // on its own.
1205                let start = async {
1206                    let mut agent_guard = agent_clone.lock().await;
1207                    agent_guard.start().await
1208                };
1209                tokio::select! {
1210                    biased;
1211                    _ = token.cancelled() => {
1212                        log::info!("Start cancelled: {}", agent_id_clone);
1213                        return;
1214                    }
1215                    r = start => {
1216                        if let Err(e) = r {
1217                            log::error!("Failed to start agent {}: {}", agent_id_clone, e);
1218                            return;
1219                        }
1220                    }
1221                }
1222
1223                while let Some(message) = rx.recv().await {
1224                    match message {
1225                        AgentMessage::Input { ctx, port, value } => {
1226                            // Attach the flow's cancellation token so
1227                            // downstream awaits (tool result waits, LLM
1228                            // streams) can observe per-context aborts.
1229                            let ctx = if ctx.cancel_token().is_none() {
1230                                ctx.with_cancel_token(ma.context_token(ctx.id()))
1231                            } else {
1232                                ctx
1233                            };
1234                            let fut =
1235                                async { agent_clone.lock().await.process(ctx, port, value).await };
1236                            tokio::select! {
1237                                biased;
1238                                _ = token.cancelled() => {
1239                                    log::info!("Process cancelled: {}", agent_id_clone);
1240                                    // Dropping the future aborts any in-flight
1241                                    // I/O and releases the agent lock. A fired
1242                                    // token cannot be reset, so install a fresh
1243                                    // one unless this loop no longer owns the
1244                                    // token slot (agent stopping or restarted).
1245                                    if let Some(fresh) = ma.renew_agent_token(
1246                                        &preset_id,
1247                                        &agent_id_clone,
1248                                        generation,
1249                                    ) {
1250                                        token = fresh;
1251                                    }
1252                                }
1253                                r = fut => r.unwrap_or_else(|e| {
1254                                    log::error!("Process Error {}: {}", agent_id_clone, e);
1255                                }),
1256                            }
1257                        }
1258                        AgentMessage::Config { key, value } => {
1259                            agent_clone
1260                                .lock()
1261                                .await
1262                                .set_config(key, value)
1263                                .unwrap_or_else(|e| {
1264                                    log::error!("Config Error {}: {}", agent_id_clone, e);
1265                                });
1266                        }
1267                        AgentMessage::Configs { configs } => {
1268                            agent_clone
1269                                .lock()
1270                                .await
1271                                .set_configs(configs)
1272                                .unwrap_or_else(|e| {
1273                                    log::error!("Configs Error {}: {}", agent_id_clone, e);
1274                                });
1275                        }
1276                        AgentMessage::Stop => {
1277                            rx.close();
1278                            break;
1279                        }
1280                    }
1281                }
1282            };
1283
1284            if uses_native_thread {
1285                std::thread::spawn(move || {
1286                    let rt = tokio::runtime::Builder::new_current_thread()
1287                        .enable_all()
1288                        .build()
1289                        .unwrap();
1290                    rt.block_on(agent_loop);
1291                });
1292            } else {
1293                tokio::spawn(agent_loop);
1294            }
1295        }
1296        Ok(())
1297    }
1298
1299    /// Stop an agent by id.
1300    ///
1301    /// Sends a stop message to the agent, closes its message channel,
1302    /// and calls the agent's [`stop()`](crate::AsAgent::stop) method.
1303    pub async fn stop_agent(&self, agent_id: &str) -> Result<(), AgentError> {
1304        {
1305            // remove the sender first to prevent new messages being sent
1306            let mut agent_txs = self.agent_txs.lock().unwrap();
1307            if let Some(tx) = agent_txs.swap_remove(agent_id)
1308                && let Err(e) = tx.try_send(AgentMessage::Stop)
1309            {
1310                log::warn!("Failed to send stop message to agent {}: {}", agent_id, e);
1311            }
1312        }
1313
1314        // Cancel BEFORE awaiting the agent lock: a long-running process()
1315        // holds the lock, and cancelling makes the agent loop drop that
1316        // future (releasing the lock) instead of blocking stop until it
1317        // completes. Removing the entry first keeps the fired token in the
1318        // loop so inputs queued ahead of Stop are skipped rather than
1319        // processed with a renewed token.
1320        let token = self.agent_tokens.lock().unwrap().swap_remove(agent_id);
1321        if let Some((_, token)) = token {
1322            token.cancel();
1323        }
1324
1325        let agent = {
1326            let agents = self.agents.lock().unwrap();
1327            let Some(a) = agents.get(agent_id) else {
1328                return Err(AgentError::AgentNotFound(agent_id.to_string()));
1329            };
1330            a.clone()
1331        };
1332        let mut agent_guard = agent.lock().await;
1333        if *agent_guard.status() == AgentStatus::Start {
1334            log::info!("Stopping agent {}", agent_id);
1335            agent_guard.stop().await?;
1336        }
1337
1338        Ok(())
1339    }
1340
1341    /// Set configs for an agent by id.
1342    ///
1343    /// Emits [`ModularAgentEvent::AgentConfigUpdated`] for each key once the
1344    /// configs have been handed to the agent. When the agent is running, the
1345    /// configs travel through its message channel and are applied
1346    /// asynchronously: the events report successful delivery, not completed
1347    /// application. Events are emitted regardless of whether a key's value
1348    /// actually changed.
1349    pub async fn set_agent_configs(
1350        &self,
1351        agent_id: String,
1352        configs: AgentConfigs,
1353    ) -> Result<(), AgentError> {
1354        let tx = {
1355            let agent_txs = self.agent_txs.lock().unwrap();
1356            agent_txs.get(&agent_id).cloned()
1357        };
1358
1359        let Some(tx) = tx else {
1360            // The agent is not running. We can set the configs directly.
1361            let agent = {
1362                let agents = self.agents.lock().unwrap();
1363                let Some(a) = agents.get(&agent_id) else {
1364                    return Err(AgentError::AgentNotFound(agent_id.to_string()));
1365                };
1366                a.clone()
1367            };
1368            agent.lock().await.set_configs(configs.clone())?;
1369            for (key, value) in configs {
1370                self.emit_agent_config_updated(agent_id.clone(), key, value);
1371            }
1372            return Ok(());
1373        };
1374        let message = AgentMessage::Configs {
1375            configs: configs.clone(),
1376        };
1377        tx.send(message).await.map_err(|_| {
1378            AgentError::SendMessageFailed("Failed to send config message".to_string())
1379        })?;
1380        for (key, value) in configs {
1381            self.emit_agent_config_updated(agent_id.clone(), key, value);
1382        }
1383        Ok(())
1384    }
1385
1386    /// Get global configs for the agent definition by name.
1387    pub fn get_global_configs(&self, def_name: &str) -> Option<AgentConfigs> {
1388        let global_configs_map = self.global_configs_map.lock().unwrap();
1389        global_configs_map.get(def_name).cloned()
1390    }
1391
1392    /// Set global configs for the agent definition by name.
1393    pub fn set_global_configs(&self, def_name: String, configs: AgentConfigs) {
1394        let mut global_configs_map = self.global_configs_map.lock().unwrap();
1395
1396        let Some(existing_configs) = global_configs_map.get_mut(&def_name) else {
1397            global_configs_map.insert(def_name, configs);
1398            return;
1399        };
1400
1401        for (key, value) in configs {
1402            existing_configs.set(key, value);
1403        }
1404    }
1405
1406    /// Get the global configs map.
1407    pub fn get_global_configs_map(&self) -> AgentConfigsMap {
1408        let global_configs_map = self.global_configs_map.lock().unwrap();
1409        global_configs_map.clone()
1410    }
1411
1412    /// Set the global configs map.
1413    pub fn set_global_configs_map(&self, new_configs_map: AgentConfigsMap) {
1414        for (agent_name, new_configs) in new_configs_map {
1415            self.set_global_configs(agent_name, new_configs);
1416        }
1417    }
1418
1419    /// Send input to an agent.
1420    pub(crate) async fn agent_input(
1421        &self,
1422        agent_id: String,
1423        ctx: AgentContext,
1424        port: String,
1425        value: AgentValue,
1426    ) -> Result<(), AgentError> {
1427        let message = if let Some(config_key) = port.strip_prefix("config:") {
1428            AgentMessage::Config {
1429                key: config_key.to_string(),
1430                value,
1431            }
1432        } else {
1433            AgentMessage::Input {
1434                ctx,
1435                port: port.clone(),
1436                value,
1437            }
1438        };
1439
1440        let tx = {
1441            let agent_txs = self.agent_txs.lock().unwrap();
1442            agent_txs.get(&agent_id).cloned()
1443        };
1444
1445        let Some(tx) = tx else {
1446            // The agent is not running. If it's a config message, we can set it directly.
1447            let agent: SharedAgent = {
1448                let agents = self.agents.lock().unwrap();
1449                let Some(a) = agents.get(&agent_id) else {
1450                    return Err(AgentError::AgentNotFound(agent_id.to_string()));
1451                };
1452                a.clone()
1453            };
1454            if let AgentMessage::Config { key, value } = message {
1455                agent.lock().await.set_config(key, value)?;
1456            }
1457            return Ok(());
1458        };
1459        tx.send(message).await.map_err(|_| {
1460            AgentError::SendMessageFailed("Failed to send input message".to_string())
1461        })?;
1462
1463        self.emit_agent_input(agent_id.to_string(), port);
1464
1465        Ok(())
1466    }
1467
1468    /// Send output from an agent. (Async version)
1469    pub async fn send_agent_out(
1470        &self,
1471        agent_id: String,
1472        ctx: AgentContext,
1473        port: String,
1474        value: AgentValue,
1475    ) -> Result<(), AgentError> {
1476        message::send_agent_out(self, agent_id, ctx, port, value).await
1477    }
1478
1479    /// Send output from an agent.
1480    pub fn try_send_agent_out(
1481        &self,
1482        agent_id: String,
1483        ctx: AgentContext,
1484        port: String,
1485        value: AgentValue,
1486    ) -> Result<(), AgentError> {
1487        message::try_send_agent_out(self, agent_id, ctx, port, value)
1488    }
1489
1490    /// Write a value to a named channel.
1491    ///
1492    /// This is the primary method for sending external input into the agent network.
1493    /// The value will be delivered to all [`ExternalInputAgent`](crate::external_agent::ExternalInputAgent)
1494    /// instances listening to the specified channel name, which will then forward it to
1495    /// their connected agents.
1496    ///
1497    /// # Arguments
1498    ///
1499    /// * `name` - The channel name to write to. Must match the `name` config of an `ExternalInputAgent`.
1500    /// * `value` - The value to send.
1501    ///
1502    /// # Example
1503    ///
1504    /// ```rust,no_run
1505    /// # use modular_agent_core::{ModularAgent, AgentValue};
1506    /// # async fn example(ma: ModularAgent) {
1507    /// // Send a string to the "input" channel
1508    /// ma.write_external_input("input".to_string(), AgentValue::string("hello")).await.unwrap();
1509    ///
1510    /// // Send an integer
1511    /// ma.write_external_input("numbers".to_string(), AgentValue::integer(42)).await.unwrap();
1512    /// # }
1513    /// ```
1514    pub async fn write_external_input(
1515        &self,
1516        name: String,
1517        value: AgentValue,
1518    ) -> Result<(), AgentError> {
1519        self.send_external_output(name, AgentContext::new(), value)
1520            .await
1521    }
1522
1523    /// Write a value to the local variable channel.
1524    pub async fn write_local_input(
1525        &self,
1526        preset_id: &str,
1527        name: &str,
1528        value: AgentValue,
1529    ) -> Result<(), AgentError> {
1530        let channel_name = format!("%{}/{}", preset_id, name);
1531        self.send_external_output(channel_name, AgentContext::new(), value)
1532            .await
1533    }
1534
1535    pub(crate) async fn send_external_output(
1536        &self,
1537        name: String,
1538        ctx: AgentContext,
1539        value: AgentValue,
1540    ) -> Result<(), AgentError> {
1541        message::send_external_output(self, name, ctx, value).await
1542    }
1543
1544    async fn spawn_message_loop(&self) -> Result<(), AgentError> {
1545        // TODO: settings for the channel size
1546        let (tx, mut rx) = mpsc::channel(4096);
1547        {
1548            let mut tx_lock = self.tx.lock().unwrap();
1549            *tx_lock = Some(tx);
1550        }
1551
1552        // spawn the main loop; base() so events emitted while routing
1553        // messages are never attributed to the caller of ready().
1554        let ma = self.base();
1555        tokio::spawn(async move {
1556            while let Some(message) = rx.recv().await {
1557                use AgentEventMessage::*;
1558
1559                match message {
1560                    AgentOut {
1561                        agent,
1562                        ctx,
1563                        port,
1564                        value,
1565                    } => {
1566                        message::agent_out(&ma, agent, ctx, port, value).await;
1567                    }
1568                    ExternalOutput { name, ctx, value } => {
1569                        message::external_input(&ma, name, ctx, value).await;
1570                    }
1571                }
1572            }
1573        });
1574
1575        tokio::task::yield_now().await;
1576
1577        Ok(())
1578    }
1579
1580    /// Subscribe to all `ModularAgent` events.
1581    ///
1582    /// Returns a broadcast receiver of [`EventEnvelope`]s, each carrying a
1583    /// [`ModularAgentEvent`] together with the origin of the change.
1584    /// For filtered subscriptions, use [`subscribe_to_event`](Self::subscribe_to_event).
1585    ///
1586    /// **Note**: Subscribe before starting presets to avoid missing events.
1587    pub fn subscribe(&self) -> broadcast::Receiver<EventEnvelope> {
1588        self.observers.subscribe()
1589    }
1590
1591    /// Subscribe to filtered [`ModularAgentEvent`]s.
1592    ///
1593    /// This method creates a filtered subscription to events. The provided closure
1594    /// filters and maps events, and only successfully mapped events are forwarded
1595    /// to the returned receiver.
1596    ///
1597    /// **Important**: Subscribe to events BEFORE starting presets to avoid missing
1598    /// events due to race conditions.
1599    ///
1600    /// # Arguments
1601    ///
1602    /// * `filter_map` - A closure that receives each [`EventEnvelope`] and returns
1603    ///   `Some(T)` for events you want to receive, or `None` to skip them.
1604    ///
1605    /// # Returns
1606    ///
1607    /// An unbounded receiver that will receive the filtered and mapped events.
1608    ///
1609    /// # Example
1610    ///
1611    /// ```rust,no_run
1612    /// use modular_agent_core::{ModularAgent, ModularAgentEvent, AgentValue};
1613    ///
1614    /// # async fn example(ma: &ModularAgent) {
1615    /// // Subscribe to a specific channel's output
1616    /// let output_channel = "output".to_string();
1617    /// let mut output_rx = ma.subscribe_to_event(move |envelope| {
1618    ///     if let ModularAgentEvent::ExternalOutput(name, value) = envelope.event {
1619    ///         if name == output_channel {
1620    ///             return Some(value);
1621    ///         }
1622    ///     }
1623    ///     None
1624    /// });
1625    ///
1626    /// // Now start the preset and receive events
1627    /// while let Some(value) = output_rx.recv().await {
1628    ///     println!("Received: {:?}", value);
1629    /// }
1630    /// # }
1631    /// ```
1632    pub fn subscribe_to_event<F, T>(&self, mut filter_map: F) -> mpsc::UnboundedReceiver<T>
1633    where
1634        F: FnMut(EventEnvelope) -> Option<T> + Send + 'static,
1635        T: Send + 'static,
1636    {
1637        let (tx, rx) = mpsc::unbounded_channel();
1638        let mut event_rx = self.subscribe();
1639
1640        tokio::spawn(async move {
1641            loop {
1642                match event_rx.recv().await {
1643                    Ok(envelope) => {
1644                        if let Some(mapped_event) = filter_map(envelope)
1645                            && tx.send(mapped_event).is_err()
1646                        {
1647                            // Receiver dropped, task can exit
1648                            break;
1649                        }
1650                    }
1651                    Err(RecvError::Lagged(n)) => {
1652                        log::warn!("Event subscriber lagged by {} events", n);
1653                    }
1654                    Err(RecvError::Closed) => {
1655                        // Sender dropped, task can exit
1656                        break;
1657                    }
1658                }
1659            }
1660        });
1661        rx
1662    }
1663
1664    pub(crate) fn emit_agent_config_updated(
1665        &self,
1666        agent_id: String,
1667        key: String,
1668        value: AgentValue,
1669    ) {
1670        self.notify_observers(ModularAgentEvent::AgentConfigUpdated(agent_id, key, value));
1671    }
1672
1673    pub(crate) fn emit_agent_error(&self, agent_id: String, message: String) {
1674        self.notify_observers(ModularAgentEvent::AgentError(agent_id, message));
1675    }
1676
1677    pub(crate) fn emit_agent_input(&self, agent_id: String, port: String) {
1678        self.notify_observers(ModularAgentEvent::AgentIn(agent_id, port));
1679    }
1680
1681    pub(crate) fn emit_agent_spec_updated(&self, agent_id: String) {
1682        self.notify_observers(ModularAgentEvent::AgentSpecUpdated(agent_id));
1683    }
1684
1685    pub(crate) fn emit_preset_structure_changed(&self, preset_id: String) {
1686        self.notify_observers(ModularAgentEvent::PresetStructureChanged { preset_id });
1687    }
1688
1689    pub(crate) fn emit_preset_added(&self, preset_id: String, name: Option<String>) {
1690        self.notify_observers(ModularAgentEvent::PresetAdded { preset_id, name });
1691    }
1692
1693    pub(crate) fn emit_preset_removed(&self, preset_id: String, name: Option<String>) {
1694        self.notify_observers(ModularAgentEvent::PresetRemoved { preset_id, name });
1695    }
1696
1697    pub(crate) fn emit_preset_renamed(
1698        &self,
1699        preset_id: String,
1700        old_name: Option<String>,
1701        new_name: String,
1702    ) {
1703        self.notify_observers(ModularAgentEvent::PresetRenamed {
1704            preset_id,
1705            old_name,
1706            new_name,
1707        });
1708    }
1709
1710    #[cfg(feature = "file")]
1711    pub(crate) fn emit_preset_saved(&self, preset_id: String, name: String) {
1712        self.notify_observers(ModularAgentEvent::PresetSaved { preset_id, name });
1713    }
1714
1715    pub(crate) fn emit_external_output(&self, name: String, value: AgentValue) {
1716        // // ignore local variables
1717        // if name.starts_with('%') {
1718        //     return;
1719        // }
1720        self.notify_observers(ModularAgentEvent::ExternalOutput(name, value));
1721    }
1722
1723    /// The single point where events are wrapped into envelopes, so every
1724    /// emitted event carries exactly the origin of the handle it went through.
1725    fn notify_observers(&self, event: ModularAgentEvent) {
1726        let _ = self.observers.send(EventEnvelope {
1727            origin: self.origin.clone(),
1728            event,
1729        });
1730    }
1731}
1732
1733/// Carrier for a [`ModularAgentEvent`] together with the origin of the change.
1734///
1735/// `origin` identifies the entry point that performed the mutation which
1736/// produced the event (see [`ModularAgent::with_origin`]). `None` means the
1737/// event originated inside the agent runtime itself.
1738#[derive(Clone, Debug)]
1739pub struct EventEnvelope {
1740    pub origin: Option<Arc<str>>,
1741    pub event: ModularAgentEvent,
1742}
1743
1744/// Events emitted by [`ModularAgent`] during operation.
1745///
1746/// Subscribe to these events using [`ModularAgent::subscribe`] or
1747/// [`ModularAgent::subscribe_to_event`].
1748///
1749/// # Example
1750///
1751/// ```rust,no_run
1752/// use modular_agent_core::{ModularAgent, ModularAgentEvent};
1753///
1754/// # fn example(ma: &ModularAgent) {
1755/// // Subscribe to all external output events
1756/// let mut rx = ma.subscribe_to_event(|envelope| {
1757///     if let ModularAgentEvent::ExternalOutput(name, value) = envelope.event {
1758///         Some((name, value))
1759///     } else {
1760///         None
1761///     }
1762/// });
1763/// # }
1764/// ```
1765#[derive(Clone, Debug)]
1766#[non_exhaustive]
1767pub enum ModularAgentEvent {
1768    /// An agent's configuration was updated.
1769    ///
1770    /// Fields: `(agent_id, config_key, new_value)`
1771    AgentConfigUpdated(String, String, AgentValue),
1772
1773    /// An agent encountered an error.
1774    ///
1775    /// Fields: `(agent_id, error_message)`
1776    AgentError(String, String),
1777
1778    /// An agent received input on a port.
1779    ///
1780    /// Fields: `(agent_id, port_name)`
1781    AgentIn(String, String),
1782
1783    /// An agent's spec was updated.
1784    ///
1785    /// Fields: `(agent_id)`
1786    AgentSpecUpdated(String),
1787
1788    /// A preset's structure (agents, connections, or non-config spec keys)
1789    /// was changed.
1790    ///
1791    /// Emitted by [`ModularAgent::add_agent`], [`ModularAgent::remove_agent`],
1792    /// [`ModularAgent::add_connection`], [`ModularAgent::remove_connection`],
1793    /// [`ModularAgent::add_agents_and_connections`],
1794    /// [`ModularAgent::update_preset_spec`], and by
1795    /// [`ModularAgent::update_agent_spec`] when the patch contains keys other
1796    /// than `configs`, so hosts can refresh their view of the preset.
1797    PresetStructureChanged { preset_id: String },
1798
1799    /// A preset was added.
1800    ///
1801    /// Emitted whenever a preset is created or loaded
1802    /// ([`ModularAgent::new_preset`], [`ModularAgent::add_preset`], their
1803    /// named variants, and `open_preset_from_file`).
1804    PresetAdded {
1805        preset_id: String,
1806        name: Option<String>,
1807    },
1808
1809    /// A preset was removed.
1810    ///
1811    /// Emitted by [`ModularAgent::remove_preset`] after the preset and its
1812    /// agents have been torn down, so hosts can close any view of it.
1813    PresetRemoved {
1814        preset_id: String,
1815        name: Option<String>,
1816    },
1817
1818    /// A preset was renamed.
1819    ///
1820    /// Emitted by [`ModularAgent::rename_preset`]. `old_name` is `None` when
1821    /// the preset had no name before.
1822    PresetRenamed {
1823        preset_id: String,
1824        old_name: Option<String>,
1825        new_name: String,
1826    },
1827
1828    /// A named preset was saved to disk.
1829    ///
1830    /// Emitted by [`ModularAgent::save_preset`]; unnamed presets produce no
1831    /// event.
1832    #[cfg(feature = "file")]
1833    PresetSaved { preset_id: String, name: String },
1834
1835    /// A value was written to an external output channel.
1836    ///
1837    /// This event is emitted when:
1838    /// - [`ModularAgent::write_external_input`] is called and flows through the network
1839    /// - An [`ExternalOutputAgent`](crate::external_agent::ExternalOutputAgent) receives a value
1840    ///
1841    /// Fields: `(channel_name, value)`
1842    ExternalOutput(String, AgentValue),
1843}
1844
1845#[cfg(test)]
1846mod tests {
1847    use super::*;
1848
1849    #[test]
1850    fn live_context_tokens_are_not_evicted_at_prune_threshold() {
1851        let ma = ModularAgent::new();
1852        let tokens: Vec<_> = (0..=CONTEXT_TOKEN_PRUNE_THRESHOLD)
1853            .map(|ctx_id| ma.context_token(ctx_id))
1854            .collect();
1855
1856        assert_eq!(tokens.len(), CONTEXT_TOKEN_PRUNE_THRESHOLD + 1);
1857        assert!(ma.abort_context(0));
1858        assert!(tokens[0].is_cancelled());
1859    }
1860}