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    // agent def name -> config
105    pub(crate) global_configs_map: Arc<Mutex<FnvIndexMap<String, AgentConfigs>>>,
106
107    // preset id -> parent cancellation token for the preset's agents
108    pub(crate) preset_tokens: Arc<Mutex<FnvIndexMap<String, CancellationToken>>>,
109
110    // agent id -> (loop generation, current cancellation token of that loop)
111    pub(crate) agent_tokens: Arc<Mutex<FnvIndexMap<String, (u64, CancellationToken)>>>,
112
113    // context id -> cancellation token (weak: dies with the flow's contexts)
114    pub(crate) context_tokens: Arc<Mutex<FnvIndexMap<usize, Weak<CancellationToken>>>>,
115
116    // message sender
117    pub(crate) tx: Arc<Mutex<Option<mpsc::Sender<AgentEventMessage>>>>,
118
119    // observers
120    pub(crate) observers: broadcast::Sender<ModularAgentEvent>,
121}
122
123impl Default for ModularAgent {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl ModularAgent {
130    /// Create a new `ModularAgent` instance without registering agents.
131    ///
132    /// For most use cases, prefer [`init()`](Self::init) which also registers
133    /// all agent definitions from the inventory.
134    pub fn new() -> Self {
135        let (tx, _rx) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
136        Self {
137            agents: Default::default(),
138            agent_txs: Default::default(),
139            external_input_agents: Default::default(),
140            external_values: Default::default(),
141            connections: Default::default(),
142            defs: Default::default(),
143            presets: Default::default(),
144            global_configs_map: Default::default(),
145            preset_tokens: Default::default(),
146            agent_tokens: Default::default(),
147            context_tokens: Default::default(),
148            tx: Arc::new(Mutex::new(None)),
149            observers: tx,
150        }
151    }
152
153    pub(crate) fn tx(&self) -> Result<mpsc::Sender<AgentEventMessage>, AgentError> {
154        self.tx
155            .lock()
156            .unwrap()
157            .clone()
158            .ok_or(AgentError::TxNotInitialized)
159    }
160
161    /// Initialize a new `ModularAgent` instance.
162    ///
163    /// This creates a new `ModularAgent` and registers all available agent definitions
164    /// from the inventory. Call [`ready`](Self::ready) after this to start the message loop.
165    ///
166    /// # Example
167    ///
168    /// ```rust,no_run
169    /// use modular_agent_core::ModularAgent;
170    ///
171    /// let ma = ModularAgent::init().unwrap();
172    /// ```
173    pub fn init() -> Result<Self, AgentError> {
174        let ma = Self::new();
175        ma.register_agents();
176        Ok(ma)
177    }
178
179    fn register_agents(&self) {
180        registry::register_inventory_agents(self);
181    }
182
183    /// Start the internal message loop.
184    ///
185    /// This must be called after [`init`](Self::init) before loading presets or sending messages.
186    /// The message loop handles routing between agents and external output events.
187    ///
188    /// # Example
189    ///
190    /// ```rust,no_run
191    /// use modular_agent_core::ModularAgent;
192    ///
193    /// #[tokio::main]
194    /// async fn main() {
195    ///     let ma = ModularAgent::init().unwrap();
196    ///     ma.ready().await.unwrap(); // Start the message loop
197    /// }
198    /// ```
199    pub async fn ready(&self) -> Result<(), AgentError> {
200        self.spawn_message_loop().await?;
201        Ok(())
202    }
203
204    /// Shut down the `ModularAgent`.
205    ///
206    /// This stops the internal message loop. Call [`stop_preset`](Self::stop_preset)
207    /// for each running preset before calling this method for graceful shutdown.
208    ///
209    /// This does not release external resources such as MCP server child processes.
210    /// Use [`shutdown`](Self::shutdown) instead when full cleanup is required.
211    ///
212    /// # Example
213    ///
214    /// ```rust,no_run
215    /// # use modular_agent_core::ModularAgent;
216    /// # async fn example(ma: ModularAgent, preset_id: &str) {
217    /// // Stop all presets first
218    /// ma.stop_preset(preset_id).await.unwrap();
219    /// // Then quit
220    /// ma.quit();
221    /// # }
222    /// ```
223    pub fn quit(&self) {
224        let mut tx_lock = self.tx.lock().unwrap();
225        *tx_lock = None;
226    }
227
228    /// Shut down the `ModularAgent` and release external resources.
229    ///
230    /// Calls [`quit`](Self::quit) to stop the internal message loop, then closes any
231    /// pooled MCP server connections so their child processes do not leak. Call
232    /// [`stop_preset`](Self::stop_preset) for each running preset before this method.
233    /// An MCP tool call still in flight during shutdown may reconnect and respawn its
234    /// server process afterwards, so quiesce all workflows first.
235    ///
236    /// # Example
237    ///
238    /// ```rust,no_run
239    /// # use modular_agent_core::ModularAgent;
240    /// # async fn example(ma: ModularAgent, preset_id: &str) {
241    /// ma.stop_preset(preset_id).await.unwrap();
242    /// ma.shutdown().await.unwrap();
243    /// # }
244    /// ```
245    pub async fn shutdown(&self) -> Result<(), AgentError> {
246        self.quit();
247        #[cfg(feature = "mcp")]
248        crate::mcp::shutdown_all_mcp_connections().await?;
249        Ok(())
250    }
251
252    // Preset management
253
254    /// Create a new empty preset.
255    ///
256    /// Returns the id of the new preset. The preset is created with default settings
257    /// and contains no agents or connections initially.
258    pub fn new_preset(&self) -> Result<String, AgentError> {
259        let spec = PresetSpec::default();
260        let id = self.add_preset(spec)?;
261        Ok(id)
262    }
263
264    /// Create a new empty preset with the given name.
265    ///
266    /// Returns the id of the new preset.
267    pub fn new_preset_with_name(&self, name: String) -> Result<String, AgentError> {
268        let spec = PresetSpec::default();
269        let id = self.add_preset_with_name(spec, name)?;
270        Ok(id)
271    }
272
273    /// Get a preset by id.
274    ///
275    /// Returns `None` if no preset exists with the given id.
276    pub fn get_preset(&self, id: &str) -> Option<Arc<AsyncMutex<Preset>>> {
277        let presets = self.presets.lock().unwrap();
278        presets.get(id).cloned()
279    }
280
281    /// Add a new preset with the given spec, and returns the id of the new preset.
282    ///
283    /// The ids of the given spec, including agents and connections, are changed to new unique ids.
284    /// This allows the same spec to be added multiple times without id conflicts.
285    pub fn add_preset(&self, spec: PresetSpec) -> Result<String, AgentError> {
286        self.add_preset_raw(spec, None)
287    }
288
289    /// Add a new preset with the given name and spec, and returns the id of the new preset.
290    ///
291    /// The ids of the given spec, including agents and connections, are changed to new unique ids.
292    pub fn add_preset_with_name(
293        &self,
294        spec: PresetSpec,
295        name: String,
296    ) -> Result<String, AgentError> {
297        self.add_preset_raw(spec, Some(name))
298    }
299
300    fn add_preset_raw(&self, spec: PresetSpec, name: Option<String>) -> Result<String, AgentError> {
301        let mut preset = Preset::new(spec);
302        if let Some(name) = name {
303            preset.set_name(name);
304        }
305        let id = preset.id().to_string();
306
307        // add agents
308        for agent in &preset.spec().agents {
309            if let Err(e) = self.add_agent_internal(id.clone(), agent.clone()) {
310                log::error!("Failed to add_agent {}: {}", agent.id, e);
311            }
312        }
313
314        // add connections
315        for connection in &preset.spec().connections {
316            self.add_connection_internal(connection.clone())
317                .unwrap_or_else(|e| {
318                    log::error!("Failed to add_connection {}: {}", connection.source, e);
319                });
320        }
321
322        // add the given preset into presets
323        let mut presets = self.presets.lock().unwrap();
324        if presets.contains_key(&id) {
325            return Err(AgentError::DuplicateId(id));
326        }
327        presets.insert(id.to_string(), Arc::new(AsyncMutex::new(preset)));
328
329        Ok(id)
330    }
331
332    /// Rename a preset by id.
333    pub async fn rename_preset(&self, id: &str, new_name: String) -> Result<(), AgentError> {
334        let preset = self
335            .get_preset(id)
336            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
337        let mut preset = preset.lock().await;
338        preset.set_name(new_name);
339        Ok(())
340    }
341
342    /// Remove a preset by id.
343    ///
344    /// Stops the preset if running, then removes all associated agents and connections.
345    pub async fn remove_preset(&self, id: &str) -> Result<(), AgentError> {
346        let preset = self
347            .get_preset(id)
348            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
349
350        let mut preset = preset.lock().await;
351        preset.stop(self).await.unwrap_or_else(|e| {
352            log::error!("Failed to stop preset {}: {}", id, e);
353        });
354
355        // Remove all agents and connections associated with the preset
356        for agent in &preset.spec().agents {
357            self.remove_agent_internal(&agent.id)
358                .await
359                .unwrap_or_else(|e| {
360                    log::error!("Failed to remove_agent {}: {}", agent.id, e);
361                });
362        }
363        for connection in &preset.spec().connections {
364            self.remove_connection_internal(connection);
365        }
366
367        // Drop the preset lock before modifying the presets map
368        drop(preset);
369
370        // Remove the preset entry from the map
371        {
372            let mut presets = self.presets.lock().unwrap();
373            presets.swap_remove(id);
374        }
375        self.remove_preset_token(id);
376
377        Ok(())
378    }
379
380    /// Start a preset by id.
381    ///
382    /// This starts all agents in the preset, enabling message flow between them.
383    /// Each agent's [`start()`](crate::AsAgent::start) method is called.
384    pub async fn start_preset(&self, id: &str) -> Result<(), AgentError> {
385        let preset = self
386            .get_preset(id)
387            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
388        let mut preset = preset.lock().await;
389        preset.start(self).await?;
390
391        Ok(())
392    }
393
394    /// Stop a preset by id.
395    ///
396    /// This stops all agents in the preset, terminating message processing.
397    /// Each agent's [`stop()`](crate::AsAgent::stop) method is called.
398    pub async fn stop_preset(&self, id: &str) -> Result<(), AgentError> {
399        let preset = self
400            .get_preset(id)
401            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
402        let mut preset = preset.lock().await;
403        preset.stop(self).await?;
404
405        Ok(())
406    }
407
408    /// Open a preset from a JSON file.
409    ///
410    /// Reads the file, parses the JSON as a [`PresetSpec`], and adds it to the system.
411    /// Optionally provide a custom name for the preset.
412    ///
413    /// # Arguments
414    ///
415    /// * `path` - Path to the JSON preset file
416    /// * `name` - Optional custom name for the preset
417    #[cfg(feature = "file")]
418    pub async fn open_preset_from_file(
419        &self,
420        path: &str,
421        name: Option<String>,
422    ) -> Result<String, AgentError> {
423        let json_str =
424            std::fs::read_to_string(path).map_err(|e| AgentError::IoError(e.to_string()))?;
425        let spec = PresetSpec::from_json(&json_str)?;
426        let id = self.add_preset_raw(spec, name)?;
427        Ok(id)
428    }
429
430    /// Save a preset to a JSON file.
431    ///
432    /// Serializes the current preset state (including agent configs) to JSON
433    /// and writes it to the specified path.
434    #[cfg(feature = "file")]
435    pub async fn save_preset(&self, id: &str, path: &str) -> Result<(), AgentError> {
436        let Some(preset_spec) = self.get_preset_spec(id).await else {
437            return Err(AgentError::PresetNotFound(id.to_string()));
438        };
439        let json_str = preset_spec.to_json()?;
440        std::fs::write(path, json_str).map_err(|e| AgentError::IoError(e.to_string()))?;
441        Ok(())
442    }
443
444    // PresetSpec
445
446    /// Get the current preset spec by id.
447    pub async fn get_preset_spec(&self, id: &str) -> Option<PresetSpec> {
448        let preset = self.get_preset(id)?;
449        let mut preset_spec = {
450            let preset = preset.lock().await;
451            preset.spec().clone()
452        };
453
454        // collect current agent specs in the preset
455        let mut agent_specs = Vec::new();
456        for agent in &preset_spec.agents {
457            if let Some(spec) = self.get_agent_spec(&agent.id).await {
458                agent_specs.push(spec);
459            }
460        }
461        preset_spec.agents = agent_specs;
462
463        // No need to change connections
464
465        Some(preset_spec)
466    }
467
468    /// Update the preset spec
469    pub async fn update_preset_spec(&self, id: &str, value: &Value) -> Result<(), AgentError> {
470        let preset = self
471            .get_preset(id)
472            .ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
473        let mut preset = preset.lock().await;
474        preset.update_spec(value)?;
475        Ok(())
476    }
477
478    // PresetInfo
479
480    /// Get info of the preset by id.
481    pub async fn get_preset_info(&self, id: &str) -> Option<PresetInfo> {
482        let preset = self.get_preset(id)?;
483        Some(PresetInfo::from(&*preset.lock().await))
484    }
485
486    /// Get infos of all presets.
487    pub async fn get_preset_infos(&self) -> Vec<PresetInfo> {
488        let presets = {
489            let presets = self.presets.lock().unwrap();
490            presets.values().cloned().collect::<Vec<_>>()
491        };
492        let mut preset_infos = Vec::new();
493        for preset in presets {
494            let preset_guard = preset.lock().await;
495            preset_infos.push(PresetInfo::from(&*preset_guard));
496        }
497        preset_infos
498    }
499
500    // Agents
501
502    /// Register an agent definition.
503    ///
504    /// This makes the agent type available for use in presets. The definition
505    /// includes metadata (title, category), input/output ports, and config specs.
506    ///
507    /// Note: Agents using `#[modular_agent]` macro are registered automatically via inventory.
508    pub fn register_agent_definiton(&self, def: AgentDefinition) {
509        let def_name = def.name.clone();
510        let def_global_configs = def.global_configs.clone();
511
512        let mut defs = self.defs.lock().unwrap();
513        defs.insert(def.name.clone(), def);
514
515        // if there is a global config, set it
516        if let Some(def_global_configs) = def_global_configs {
517            let mut new_configs = AgentConfigs::default();
518            for (key, config_entry) in def_global_configs.iter() {
519                new_configs.set(key.clone(), config_entry.value.clone());
520            }
521            self.set_global_configs(def_name, new_configs);
522        }
523    }
524
525    /// Get all registered agent definitions.
526    ///
527    /// Returns a map of definition name to [`AgentDefinition`].
528    pub fn get_agent_definitions(&self) -> AgentDefinitions {
529        let defs = self.defs.lock().unwrap();
530        defs.clone()
531    }
532
533    /// Get an agent definition by name.
534    ///
535    /// The name is typically in the format `module::path::StructName`.
536    pub fn get_agent_definition(&self, def_name: &str) -> Option<AgentDefinition> {
537        let defs = self.defs.lock().unwrap();
538        defs.get(def_name).cloned()
539    }
540
541    /// Get the config specs of an agent definition by name.
542    pub fn get_agent_config_specs(&self, def_name: &str) -> Option<AgentConfigSpecs> {
543        let defs = self.defs.lock().unwrap();
544        let def = defs.get(def_name)?;
545        def.configs.clone()
546    }
547
548    /// Get the agent spec by id.
549    pub async fn get_agent_spec(&self, agent_id: &str) -> Option<AgentSpec> {
550        let agent = {
551            let agents = self.agents.lock().unwrap();
552            agents.get(agent_id)?.clone()
553        };
554        let agent = agent.lock().await;
555        Some(agent.spec().clone())
556    }
557
558    /// Update the agent spec by id.
559    pub async fn update_agent_spec(&self, agent_id: &str, value: &Value) -> Result<(), AgentError> {
560        let agent = {
561            let agents = self.agents.lock().unwrap();
562            let Some(agent) = agents.get(agent_id) else {
563                return Err(AgentError::AgentNotFound(agent_id.to_string()));
564            };
565            agent.clone()
566        };
567        let mut agent = agent.lock().await;
568        agent.update_spec(value)?;
569        Ok(())
570    }
571
572    /// Create a new agent spec from the given agent definition name.
573    pub fn new_agent_spec(&self, def_name: &str) -> Result<AgentSpec, AgentError> {
574        let def = self
575            .get_agent_definition(def_name)
576            .ok_or_else(|| AgentError::AgentDefinitionNotFound(def_name.to_string()))?;
577        Ok(def.to_spec())
578    }
579
580    /// Add an agent to the specified preset.
581    ///
582    /// Creates a new agent instance from the given spec and adds it to the preset.
583    /// Returns the id of the newly created agent. The agent is not started automatically;
584    /// call [`start_preset`](Self::start_preset) or [`start_agent`](Self::start_agent) to start it.
585    pub async fn add_agent(
586        &self,
587        preset_id: String,
588        mut spec: AgentSpec,
589    ) -> Result<String, AgentError> {
590        let preset = self
591            .get_preset(&preset_id)
592            .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
593
594        let id = new_id();
595        spec.id = id.clone();
596        self.add_agent_internal(preset_id, spec.clone())?;
597
598        let mut preset = preset.lock().await;
599        preset.add_agent(spec.clone());
600
601        Ok(id)
602    }
603
604    fn add_agent_internal(&self, preset_id: String, spec: AgentSpec) -> Result<(), AgentError> {
605        let mut agents = self.agents.lock().unwrap();
606        if agents.contains_key(&spec.id) {
607            return Err(AgentError::AgentAlreadyExists(spec.id.to_string()));
608        }
609        let spec_id = spec.id.clone();
610        let mut agent = agent_new(self.clone(), spec_id.clone(), spec)?;
611        agent.set_preset_id(preset_id);
612        agents.insert(spec_id, Arc::new(AsyncMutex::new(agent)));
613        Ok(())
614    }
615
616    /// Get the agent by id.
617    pub fn get_agent(&self, agent_id: &str) -> Option<SharedAgent> {
618        let agents = self.agents.lock().unwrap();
619        agents.get(agent_id).cloned()
620    }
621
622    /// Add a connection between two agents in the specified preset.
623    ///
624    /// When the source agent outputs a value on the source handle (port),
625    /// it will be delivered to the target agent's target handle (port).
626    pub async fn add_connection(
627        &self,
628        preset_id: &str,
629        connection: ConnectionSpec,
630    ) -> Result<(), AgentError> {
631        // check if the source and target agents exist
632        {
633            let agents = self.agents.lock().unwrap();
634            if !agents.contains_key(&connection.source) {
635                return Err(AgentError::AgentNotFound(connection.source.to_string()));
636            }
637            if !agents.contains_key(&connection.target) {
638                return Err(AgentError::AgentNotFound(connection.target.to_string()));
639            }
640        }
641
642        // check if handles are valid
643        if connection.source_handle.is_empty() {
644            return Err(AgentError::EmptySourceHandle);
645        }
646        if connection.target_handle.is_empty() {
647            return Err(AgentError::EmptyTargetHandle);
648        }
649
650        let preset = self
651            .get_preset(preset_id)
652            .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
653        let mut preset = preset.lock().await;
654        preset.add_connection(connection.clone());
655        self.add_connection_internal(connection)?;
656        Ok(())
657    }
658
659    fn add_connection_internal(&self, connection: ConnectionSpec) -> Result<(), AgentError> {
660        let mut connections = self.connections.lock().unwrap();
661        if let Some(targets) = connections.get_mut(&connection.source) {
662            if targets
663                .iter()
664                .any(|(target, source_handle, target_handle)| {
665                    *target == connection.target
666                        && *source_handle == connection.source_handle
667                        && *target_handle == connection.target_handle
668                })
669            {
670                return Err(AgentError::ConnectionAlreadyExists);
671            }
672            targets.push((
673                connection.target,
674                connection.source_handle,
675                connection.target_handle,
676            ));
677        } else {
678            connections.insert(
679                connection.source,
680                vec![(
681                    connection.target,
682                    connection.source_handle,
683                    connection.target_handle,
684                )],
685            );
686        }
687        Ok(())
688    }
689
690    /// Returns true if any connection originates from `source_agent`'s `port`.
691    ///
692    /// Producers can use this to skip building expensive values for ports
693    /// nobody listens to; `agent_out` would only drop them after the
694    /// conversion cost has already been paid.
695    pub fn has_connections(&self, source_agent: &str, port: &str) -> bool {
696        let connections = self.connections.lock().unwrap();
697        connections.get(source_agent).is_some_and(|targets| {
698            targets
699                .iter()
700                .any(|(_, source_port, _)| source_port == port)
701        })
702    }
703
704    /// Add agents and connections to the specified preset.
705    ///
706    /// The ids of the given agents and connections are changed to new unique ids.
707    /// The agents are not started automatically, even if the preset is running.
708    pub async fn add_agents_and_connections(
709        &self,
710        preset_id: &str,
711        agents: &Vec<AgentSpec>,
712        connections: &Vec<ConnectionSpec>,
713    ) -> Result<(Vec<AgentSpec>, Vec<ConnectionSpec>), AgentError> {
714        let (agents, connections) = update_ids(agents, connections);
715
716        let preset = self
717            .get_preset(preset_id)
718            .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
719        let mut preset = preset.lock().await;
720
721        for agent in &agents {
722            self.add_agent_internal(preset_id.to_string(), agent.clone())?;
723            preset.add_agent(agent.clone());
724        }
725
726        for connection in &connections {
727            self.add_connection_internal(connection.clone())?;
728            preset.add_connection(connection.clone());
729        }
730
731        Ok((agents, connections))
732    }
733
734    /// Remove an agent from the specified preset.
735    ///
736    /// If the agent is running, it will be stopped first.
737    pub async fn remove_agent(&self, preset_id: &str, agent_id: &str) -> Result<(), AgentError> {
738        {
739            let preset = self
740                .get_preset(preset_id)
741                .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
742            let mut preset = preset.lock().await;
743            preset.remove_agent(agent_id);
744        }
745        self.remove_agent_internal(agent_id).await?;
746        Ok(())
747    }
748
749    async fn remove_agent_internal(&self, agent_id: &str) -> Result<(), AgentError> {
750        self.stop_agent(agent_id).await?;
751
752        // remove from connections
753        {
754            let mut connections = self.connections.lock().unwrap();
755            let mut sources_to_remove = Vec::new();
756            for (source, targets) in connections.iter_mut() {
757                targets.retain(|(target, _, _)| target != agent_id);
758                if targets.is_empty() {
759                    sources_to_remove.push(source.clone());
760                }
761            }
762            for source in sources_to_remove {
763                connections.swap_remove(&source);
764            }
765            connections.swap_remove(agent_id);
766        }
767
768        // remove from agents
769        {
770            let mut agents = self.agents.lock().unwrap();
771            agents.swap_remove(agent_id);
772        }
773
774        Ok(())
775    }
776
777    /// Remove a connection from the specified preset.
778    pub async fn remove_connection(
779        &self,
780        preset_id: &str,
781        connection: &ConnectionSpec,
782    ) -> Result<(), AgentError> {
783        let preset = self
784            .get_preset(preset_id)
785            .ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
786        let mut preset = preset.lock().await;
787        let Some(connection) = preset.remove_connection(connection) else {
788            return Err(AgentError::ConnectionNotFound(format!(
789                "{}:{}->{}:{}",
790                connection.source,
791                connection.source_handle,
792                connection.target,
793                connection.target_handle
794            )));
795        };
796        self.remove_connection_internal(&connection);
797        Ok(())
798    }
799
800    fn remove_connection_internal(&self, connection: &ConnectionSpec) {
801        let mut connections = self.connections.lock().unwrap();
802        if let Some(targets) = connections.get_mut(&connection.source) {
803            targets.retain(|(target, source_handle, target_handle)| {
804                *target != connection.target
805                    || *source_handle != connection.source_handle
806                    || *target_handle != connection.target_handle
807            });
808            if targets.is_empty() {
809                connections.swap_remove(&connection.source);
810            }
811        }
812    }
813
814    // Cancellation tokens
815
816    /// Returns the parent cancellation token for a preset, creating it if needed.
817    fn preset_token(&self, preset_id: &str) -> CancellationToken {
818        let mut tokens = self.preset_tokens.lock().unwrap();
819        tokens.entry(preset_id.to_string()).or_default().clone()
820    }
821
822    /// Installs a fresh (uncancelled) parent token for a preset.
823    ///
824    /// A fired `CancellationToken` cannot be reset, so this is called when a
825    /// preset starts to replace the token cancelled by a previous stop.
826    pub(crate) fn reset_preset_token(&self, preset_id: &str) {
827        let mut tokens = self.preset_tokens.lock().unwrap();
828        tokens.insert(preset_id.to_string(), CancellationToken::new());
829    }
830
831    /// Cancels the preset's parent token, aborting the in-flight `process()`
832    /// of every agent in the preset at once.
833    ///
834    /// The entry is kept (in its cancelled state) for the duration of the
835    /// stop sequence so agent tokens renewed while agents are still being
836    /// stopped are born cancelled and queued inputs are skipped instead of
837    /// processed. [`Preset::stop`](crate::preset::Preset::stop) removes the
838    /// entry once every agent has stopped, so a later `start_agent` derives
839    /// a live token instead of a child of the fired one.
840    pub(crate) fn cancel_preset_token(&self, preset_id: &str) {
841        let token = self.preset_tokens.lock().unwrap().get(preset_id).cloned();
842        if let Some(token) = token {
843            token.cancel();
844        }
845    }
846
847    pub(crate) fn remove_preset_token(&self, preset_id: &str) {
848        self.preset_tokens.lock().unwrap().swap_remove(preset_id);
849    }
850
851    /// Creates and tracks a fresh cancellation token for an agent as a child
852    /// of its preset's parent token. The returned generation identifies the
853    /// agent-loop incarnation that owns the slot.
854    fn create_agent_token(&self, preset_id: &str, agent_id: &str) -> (u64, CancellationToken) {
855        let generation = AGENT_TOKEN_GENERATION.fetch_add(1, Ordering::Relaxed);
856        let token = self.preset_token(preset_id).child_token();
857        self.agent_tokens
858            .lock()
859            .unwrap()
860            .insert(agent_id.to_string(), (generation, token.clone()));
861        (generation, token)
862    }
863
864    /// Replaces a fired agent token with a fresh child of the preset token.
865    ///
866    /// Called by the agent loop after its token fired. Returns `None` when
867    /// the slot no longer belongs to the calling loop — either
868    /// [`stop_agent`](Self::stop_agent) removed the entry, a restarted
869    /// agent's new loop installed its own token (different generation), or
870    /// the whole preset was removed. The caller then keeps its fired token
871    /// so queued inputs are skipped until the `Stop` message arrives.
872    fn renew_agent_token(
873        &self,
874        preset_id: &str,
875        agent_id: &str,
876        generation: u64,
877    ) -> Option<CancellationToken> {
878        // Look up (never create) the parent: a lagging loop must not
879        // resurrect the token entry of a removed preset.
880        let parent = self.preset_tokens.lock().unwrap().get(preset_id).cloned()?;
881        let fresh = parent.child_token();
882        let mut tokens = self.agent_tokens.lock().unwrap();
883        let slot = tokens.get_mut(agent_id)?;
884        if slot.0 != generation {
885            return None;
886        }
887        slot.1 = fresh.clone();
888        Some(fresh)
889    }
890
891    /// Returns the cancellation token for a context, creating it if needed.
892    ///
893    /// The registry holds `Weak` references: an entry dies when the flow's
894    /// last context clone is dropped, so lookups for finished flows fail and
895    /// dead entries can be pruned. Pruning starts once the registry reaches
896    /// [`CONTEXT_TOKEN_PRUNE_THRESHOLD`], but live entries are never evicted.
897    pub(crate) fn context_token(&self, ctx_id: usize) -> Arc<CancellationToken> {
898        let mut tokens = self.context_tokens.lock().unwrap();
899        if let Some(token) = tokens.get(&ctx_id).and_then(Weak::upgrade) {
900            return token;
901        }
902        if tokens.len() >= CONTEXT_TOKEN_PRUNE_THRESHOLD {
903            tokens.retain(|_, weak| weak.strong_count() > 0);
904        }
905        let token = Arc::new(CancellationToken::new());
906        tokens.insert(ctx_id, Arc::downgrade(&token));
907        token
908    }
909
910    /// Aborts the flow identified by `ctx_id`.
911    ///
912    /// Cancels the context's cancellation token, which every agent handling
913    /// the flow received via [`AgentContext::cancel_token`]. Cancellation is
914    /// cooperative for work already in flight: agents that `select!` on the
915    /// token (LLM streaming loops, [`PresetToolAgent`](crate::tool::PresetToolAgent)
916    /// result waits) abort promptly with [`AgentError::Cancelled`], while
917    /// agents that ignore it run to completion. Inputs dispatched after the
918    /// token fires are skipped before `process()` is called. The cancelled
919    /// token stays alive as long as any context of the flow does, so queued
920    /// and cyclic inputs for the flow are skipped too.
921    ///
922    /// Returns `false` when no live flow is tracked under `ctx_id` (the flow
923    /// already finished, or never reached an agent): nothing is cancelled.
924    pub fn abort_context(&self, ctx_id: usize) -> bool {
925        let token = self
926            .context_tokens
927            .lock()
928            .unwrap()
929            .get(&ctx_id)
930            .and_then(Weak::upgrade);
931        match token {
932            Some(token) => {
933                token.cancel();
934                true
935            }
936            None => {
937                log::warn!("abort_context: no live flow for context {}", ctx_id);
938                false
939            }
940        }
941    }
942
943    /// Start an agent by id.
944    ///
945    /// Creates a message channel for the agent and spawns its event loop.
946    /// The agent's [`start()`](crate::AsAgent::start) method is called, then
947    /// the agent begins processing incoming messages.
948    ///
949    /// If the agent's definition has `native_thread = true`, the agent runs
950    /// on a dedicated OS thread instead of the tokio runtime.
951    pub async fn start_agent(&self, agent_id: &str) -> Result<(), AgentError> {
952        let agent = {
953            let agents = self.agents.lock().unwrap();
954            let Some(a) = agents.get(agent_id) else {
955                return Err(AgentError::AgentNotFound(agent_id.to_string()));
956            };
957            a.clone()
958        };
959        let (def_name, preset_id) = {
960            let agent = agent.lock().await;
961            (agent.def_name().to_string(), agent.preset_id().to_string())
962        };
963        let uses_native_thread = {
964            let defs = self.defs.lock().unwrap();
965            let Some(def) = defs.get(&def_name) else {
966                return Err(AgentError::AgentDefinitionNotFound(agent_id.to_string()));
967            };
968            def.native_thread
969        };
970        let agent_status = {
971            // This will not block since the agent is not started yet.
972            let agent = agent.lock().await;
973            agent.status().clone()
974        };
975        if agent_status == AgentStatus::Init {
976            log::info!("Starting agent {}", agent_id);
977
978            let (tx, mut rx) = mpsc::channel(MESSAGE_LIMIT);
979
980            {
981                let mut agent_txs = self.agent_txs.lock().unwrap();
982                agent_txs.insert(agent_id.to_string(), tx.clone());
983            };
984
985            let agent_clone = agent.clone();
986            let agent_id_clone = agent_id.to_string();
987            let ma = self.clone();
988            // Created before spawning so stop_agent can cancel it immediately.
989            let (generation, mut token) = self.create_agent_token(&preset_id, agent_id);
990
991            let agent_loop = async move {
992                // Race start() against the token too: a start() stuck on
993                // slow I/O holds the agent lock, and without the race
994                // stop_agent would block on that lock until start() returns
995                // on its own.
996                let start = async {
997                    let mut agent_guard = agent_clone.lock().await;
998                    agent_guard.start().await
999                };
1000                tokio::select! {
1001                    biased;
1002                    _ = token.cancelled() => {
1003                        log::info!("Start cancelled: {}", agent_id_clone);
1004                        return;
1005                    }
1006                    r = start => {
1007                        if let Err(e) = r {
1008                            log::error!("Failed to start agent {}: {}", agent_id_clone, e);
1009                            return;
1010                        }
1011                    }
1012                }
1013
1014                while let Some(message) = rx.recv().await {
1015                    match message {
1016                        AgentMessage::Input { ctx, port, value } => {
1017                            // Attach the flow's cancellation token so
1018                            // downstream awaits (tool result waits, LLM
1019                            // streams) can observe per-context aborts.
1020                            let ctx = if ctx.cancel_token().is_none() {
1021                                ctx.with_cancel_token(ma.context_token(ctx.id()))
1022                            } else {
1023                                ctx
1024                            };
1025                            let fut =
1026                                async { agent_clone.lock().await.process(ctx, port, value).await };
1027                            tokio::select! {
1028                                biased;
1029                                _ = token.cancelled() => {
1030                                    log::info!("Process cancelled: {}", agent_id_clone);
1031                                    // Dropping the future aborts any in-flight
1032                                    // I/O and releases the agent lock. A fired
1033                                    // token cannot be reset, so install a fresh
1034                                    // one unless this loop no longer owns the
1035                                    // token slot (agent stopping or restarted).
1036                                    if let Some(fresh) = ma.renew_agent_token(
1037                                        &preset_id,
1038                                        &agent_id_clone,
1039                                        generation,
1040                                    ) {
1041                                        token = fresh;
1042                                    }
1043                                }
1044                                r = fut => r.unwrap_or_else(|e| {
1045                                    log::error!("Process Error {}: {}", agent_id_clone, e);
1046                                }),
1047                            }
1048                        }
1049                        AgentMessage::Config { key, value } => {
1050                            agent_clone
1051                                .lock()
1052                                .await
1053                                .set_config(key, value)
1054                                .unwrap_or_else(|e| {
1055                                    log::error!("Config Error {}: {}", agent_id_clone, e);
1056                                });
1057                        }
1058                        AgentMessage::Configs { configs } => {
1059                            agent_clone
1060                                .lock()
1061                                .await
1062                                .set_configs(configs)
1063                                .unwrap_or_else(|e| {
1064                                    log::error!("Configs Error {}: {}", agent_id_clone, e);
1065                                });
1066                        }
1067                        AgentMessage::Stop => {
1068                            rx.close();
1069                            break;
1070                        }
1071                    }
1072                }
1073            };
1074
1075            if uses_native_thread {
1076                std::thread::spawn(move || {
1077                    let rt = tokio::runtime::Builder::new_current_thread()
1078                        .enable_all()
1079                        .build()
1080                        .unwrap();
1081                    rt.block_on(agent_loop);
1082                });
1083            } else {
1084                tokio::spawn(agent_loop);
1085            }
1086        }
1087        Ok(())
1088    }
1089
1090    /// Stop an agent by id.
1091    ///
1092    /// Sends a stop message to the agent, closes its message channel,
1093    /// and calls the agent's [`stop()`](crate::AsAgent::stop) method.
1094    pub async fn stop_agent(&self, agent_id: &str) -> Result<(), AgentError> {
1095        {
1096            // remove the sender first to prevent new messages being sent
1097            let mut agent_txs = self.agent_txs.lock().unwrap();
1098            if let Some(tx) = agent_txs.swap_remove(agent_id)
1099                && let Err(e) = tx.try_send(AgentMessage::Stop)
1100            {
1101                log::warn!("Failed to send stop message to agent {}: {}", agent_id, e);
1102            }
1103        }
1104
1105        // Cancel BEFORE awaiting the agent lock: a long-running process()
1106        // holds the lock, and cancelling makes the agent loop drop that
1107        // future (releasing the lock) instead of blocking stop until it
1108        // completes. Removing the entry first keeps the fired token in the
1109        // loop so inputs queued ahead of Stop are skipped rather than
1110        // processed with a renewed token.
1111        let token = self.agent_tokens.lock().unwrap().swap_remove(agent_id);
1112        if let Some((_, token)) = token {
1113            token.cancel();
1114        }
1115
1116        let agent = {
1117            let agents = self.agents.lock().unwrap();
1118            let Some(a) = agents.get(agent_id) else {
1119                return Err(AgentError::AgentNotFound(agent_id.to_string()));
1120            };
1121            a.clone()
1122        };
1123        let mut agent_guard = agent.lock().await;
1124        if *agent_guard.status() == AgentStatus::Start {
1125            log::info!("Stopping agent {}", agent_id);
1126            agent_guard.stop().await?;
1127        }
1128
1129        Ok(())
1130    }
1131
1132    /// Set configs for an agent by id.
1133    pub async fn set_agent_configs(
1134        &self,
1135        agent_id: String,
1136        configs: AgentConfigs,
1137    ) -> Result<(), AgentError> {
1138        let tx = {
1139            let agent_txs = self.agent_txs.lock().unwrap();
1140            agent_txs.get(&agent_id).cloned()
1141        };
1142
1143        let Some(tx) = tx else {
1144            // The agent is not running. We can set the configs directly.
1145            let agent = {
1146                let agents = self.agents.lock().unwrap();
1147                let Some(a) = agents.get(&agent_id) else {
1148                    return Err(AgentError::AgentNotFound(agent_id.to_string()));
1149                };
1150                a.clone()
1151            };
1152            agent.lock().await.set_configs(configs.clone())?;
1153            return Ok(());
1154        };
1155        let message = AgentMessage::Configs { configs };
1156        tx.send(message).await.map_err(|_| {
1157            AgentError::SendMessageFailed("Failed to send config message".to_string())
1158        })?;
1159        Ok(())
1160    }
1161
1162    /// Get global configs for the agent definition by name.
1163    pub fn get_global_configs(&self, def_name: &str) -> Option<AgentConfigs> {
1164        let global_configs_map = self.global_configs_map.lock().unwrap();
1165        global_configs_map.get(def_name).cloned()
1166    }
1167
1168    /// Set global configs for the agent definition by name.
1169    pub fn set_global_configs(&self, def_name: String, configs: AgentConfigs) {
1170        let mut global_configs_map = self.global_configs_map.lock().unwrap();
1171
1172        let Some(existing_configs) = global_configs_map.get_mut(&def_name) else {
1173            global_configs_map.insert(def_name, configs);
1174            return;
1175        };
1176
1177        for (key, value) in configs {
1178            existing_configs.set(key, value);
1179        }
1180    }
1181
1182    /// Get the global configs map.
1183    pub fn get_global_configs_map(&self) -> AgentConfigsMap {
1184        let global_configs_map = self.global_configs_map.lock().unwrap();
1185        global_configs_map.clone()
1186    }
1187
1188    /// Set the global configs map.
1189    pub fn set_global_configs_map(&self, new_configs_map: AgentConfigsMap) {
1190        for (agent_name, new_configs) in new_configs_map {
1191            self.set_global_configs(agent_name, new_configs);
1192        }
1193    }
1194
1195    /// Send input to an agent.
1196    pub(crate) async fn agent_input(
1197        &self,
1198        agent_id: String,
1199        ctx: AgentContext,
1200        port: String,
1201        value: AgentValue,
1202    ) -> Result<(), AgentError> {
1203        let message = if let Some(config_key) = port.strip_prefix("config:") {
1204            AgentMessage::Config {
1205                key: config_key.to_string(),
1206                value,
1207            }
1208        } else {
1209            AgentMessage::Input {
1210                ctx,
1211                port: port.clone(),
1212                value,
1213            }
1214        };
1215
1216        let tx = {
1217            let agent_txs = self.agent_txs.lock().unwrap();
1218            agent_txs.get(&agent_id).cloned()
1219        };
1220
1221        let Some(tx) = tx else {
1222            // The agent is not running. If it's a config message, we can set it directly.
1223            let agent: SharedAgent = {
1224                let agents = self.agents.lock().unwrap();
1225                let Some(a) = agents.get(&agent_id) else {
1226                    return Err(AgentError::AgentNotFound(agent_id.to_string()));
1227                };
1228                a.clone()
1229            };
1230            if let AgentMessage::Config { key, value } = message {
1231                agent.lock().await.set_config(key, value)?;
1232            }
1233            return Ok(());
1234        };
1235        tx.send(message).await.map_err(|_| {
1236            AgentError::SendMessageFailed("Failed to send input message".to_string())
1237        })?;
1238
1239        self.emit_agent_input(agent_id.to_string(), port);
1240
1241        Ok(())
1242    }
1243
1244    /// Send output from an agent. (Async version)
1245    pub async fn send_agent_out(
1246        &self,
1247        agent_id: String,
1248        ctx: AgentContext,
1249        port: String,
1250        value: AgentValue,
1251    ) -> Result<(), AgentError> {
1252        message::send_agent_out(self, agent_id, ctx, port, value).await
1253    }
1254
1255    /// Send output from an agent.
1256    pub fn try_send_agent_out(
1257        &self,
1258        agent_id: String,
1259        ctx: AgentContext,
1260        port: String,
1261        value: AgentValue,
1262    ) -> Result<(), AgentError> {
1263        message::try_send_agent_out(self, agent_id, ctx, port, value)
1264    }
1265
1266    /// Write a value to a named channel.
1267    ///
1268    /// This is the primary method for sending external input into the agent network.
1269    /// The value will be delivered to all [`ExternalInputAgent`](crate::external_agent::ExternalInputAgent)
1270    /// instances listening to the specified channel name, which will then forward it to
1271    /// their connected agents.
1272    ///
1273    /// # Arguments
1274    ///
1275    /// * `name` - The channel name to write to. Must match the `name` config of an `ExternalInputAgent`.
1276    /// * `value` - The value to send.
1277    ///
1278    /// # Example
1279    ///
1280    /// ```rust,no_run
1281    /// # use modular_agent_core::{ModularAgent, AgentValue};
1282    /// # async fn example(ma: ModularAgent) {
1283    /// // Send a string to the "input" channel
1284    /// ma.write_external_input("input".to_string(), AgentValue::string("hello")).await.unwrap();
1285    ///
1286    /// // Send an integer
1287    /// ma.write_external_input("numbers".to_string(), AgentValue::integer(42)).await.unwrap();
1288    /// # }
1289    /// ```
1290    pub async fn write_external_input(
1291        &self,
1292        name: String,
1293        value: AgentValue,
1294    ) -> Result<(), AgentError> {
1295        self.send_external_output(name, AgentContext::new(), value)
1296            .await
1297    }
1298
1299    /// Write a value to the local variable channel.
1300    pub async fn write_local_input(
1301        &self,
1302        preset_id: &str,
1303        name: &str,
1304        value: AgentValue,
1305    ) -> Result<(), AgentError> {
1306        let channel_name = format!("%{}/{}", preset_id, name);
1307        self.send_external_output(channel_name, AgentContext::new(), value)
1308            .await
1309    }
1310
1311    pub(crate) async fn send_external_output(
1312        &self,
1313        name: String,
1314        ctx: AgentContext,
1315        value: AgentValue,
1316    ) -> Result<(), AgentError> {
1317        message::send_external_output(self, name, ctx, value).await
1318    }
1319
1320    async fn spawn_message_loop(&self) -> Result<(), AgentError> {
1321        // TODO: settings for the channel size
1322        let (tx, mut rx) = mpsc::channel(4096);
1323        {
1324            let mut tx_lock = self.tx.lock().unwrap();
1325            *tx_lock = Some(tx);
1326        }
1327
1328        // spawn the main loop
1329        let ma = self.clone();
1330        tokio::spawn(async move {
1331            while let Some(message) = rx.recv().await {
1332                use AgentEventMessage::*;
1333
1334                match message {
1335                    AgentOut {
1336                        agent,
1337                        ctx,
1338                        port,
1339                        value,
1340                    } => {
1341                        message::agent_out(&ma, agent, ctx, port, value).await;
1342                    }
1343                    ExternalOutput { name, ctx, value } => {
1344                        message::external_input(&ma, name, ctx, value).await;
1345                    }
1346                }
1347            }
1348        });
1349
1350        tokio::task::yield_now().await;
1351
1352        Ok(())
1353    }
1354
1355    /// Subscribe to all `ModularAgent` events.
1356    ///
1357    /// Returns a broadcast receiver that receives all [`ModularAgentEvent`]s.
1358    /// For filtered subscriptions, use [`subscribe_to_event`](Self::subscribe_to_event).
1359    ///
1360    /// **Note**: Subscribe before starting presets to avoid missing events.
1361    pub fn subscribe(&self) -> broadcast::Receiver<ModularAgentEvent> {
1362        self.observers.subscribe()
1363    }
1364
1365    /// Subscribe to filtered [`ModularAgentEvent`]s.
1366    ///
1367    /// This method creates a filtered subscription to events. The provided closure
1368    /// filters and maps events, and only successfully mapped events are forwarded
1369    /// to the returned receiver.
1370    ///
1371    /// **Important**: Subscribe to events BEFORE starting presets to avoid missing
1372    /// events due to race conditions.
1373    ///
1374    /// # Arguments
1375    ///
1376    /// * `filter_map` - A closure that receives each event and returns `Some(T)` for
1377    ///   events you want to receive, or `None` to skip them.
1378    ///
1379    /// # Returns
1380    ///
1381    /// An unbounded receiver that will receive the filtered and mapped events.
1382    ///
1383    /// # Example
1384    ///
1385    /// ```rust,no_run
1386    /// use modular_agent_core::{ModularAgent, ModularAgentEvent, AgentValue};
1387    ///
1388    /// # async fn example(ma: &ModularAgent) {
1389    /// // Subscribe to a specific channel's output
1390    /// let output_channel = "output".to_string();
1391    /// let mut output_rx = ma.subscribe_to_event(move |event| {
1392    ///     if let ModularAgentEvent::ExternalOutput(name, value) = event {
1393    ///         if name == output_channel {
1394    ///             return Some(value);
1395    ///         }
1396    ///     }
1397    ///     None
1398    /// });
1399    ///
1400    /// // Now start the preset and receive events
1401    /// while let Some(value) = output_rx.recv().await {
1402    ///     println!("Received: {:?}", value);
1403    /// }
1404    /// # }
1405    /// ```
1406    pub fn subscribe_to_event<F, T>(&self, mut filter_map: F) -> mpsc::UnboundedReceiver<T>
1407    where
1408        F: FnMut(ModularAgentEvent) -> Option<T> + Send + 'static,
1409        T: Send + 'static,
1410    {
1411        let (tx, rx) = mpsc::unbounded_channel();
1412        let mut event_rx = self.subscribe();
1413
1414        tokio::spawn(async move {
1415            loop {
1416                match event_rx.recv().await {
1417                    Ok(event) => {
1418                        if let Some(mapped_event) = filter_map(event)
1419                            && tx.send(mapped_event).is_err()
1420                        {
1421                            // Receiver dropped, task can exit
1422                            break;
1423                        }
1424                    }
1425                    Err(RecvError::Lagged(n)) => {
1426                        log::warn!("Event subscriber lagged by {} events", n);
1427                    }
1428                    Err(RecvError::Closed) => {
1429                        // Sender dropped, task can exit
1430                        break;
1431                    }
1432                }
1433            }
1434        });
1435        rx
1436    }
1437
1438    pub(crate) fn emit_agent_config_updated(
1439        &self,
1440        agent_id: String,
1441        key: String,
1442        value: AgentValue,
1443    ) {
1444        self.notify_observers(ModularAgentEvent::AgentConfigUpdated(agent_id, key, value));
1445    }
1446
1447    pub(crate) fn emit_agent_error(&self, agent_id: String, message: String) {
1448        self.notify_observers(ModularAgentEvent::AgentError(agent_id, message));
1449    }
1450
1451    pub(crate) fn emit_agent_input(&self, agent_id: String, port: String) {
1452        self.notify_observers(ModularAgentEvent::AgentIn(agent_id, port));
1453    }
1454
1455    pub(crate) fn emit_agent_spec_updated(&self, agent_id: String) {
1456        self.notify_observers(ModularAgentEvent::AgentSpecUpdated(agent_id));
1457    }
1458
1459    pub(crate) fn emit_external_output(&self, name: String, value: AgentValue) {
1460        // // ignore local variables
1461        // if name.starts_with('%') {
1462        //     return;
1463        // }
1464        self.notify_observers(ModularAgentEvent::ExternalOutput(name, value));
1465    }
1466
1467    fn notify_observers(&self, event: ModularAgentEvent) {
1468        let _ = self.observers.send(event);
1469    }
1470}
1471
1472/// Events emitted by [`ModularAgent`] during operation.
1473///
1474/// Subscribe to these events using [`ModularAgent::subscribe`] or
1475/// [`ModularAgent::subscribe_to_event`].
1476///
1477/// # Example
1478///
1479/// ```rust,no_run
1480/// use modular_agent_core::{ModularAgent, ModularAgentEvent};
1481///
1482/// # fn example(ma: &ModularAgent) {
1483/// // Subscribe to all external output events
1484/// let mut rx = ma.subscribe_to_event(|event| {
1485///     if let ModularAgentEvent::ExternalOutput(name, value) = event {
1486///         Some((name, value))
1487///     } else {
1488///         None
1489///     }
1490/// });
1491/// # }
1492/// ```
1493#[derive(Clone, Debug)]
1494pub enum ModularAgentEvent {
1495    /// An agent's configuration was updated.
1496    ///
1497    /// Fields: `(agent_id, config_key, new_value)`
1498    AgentConfigUpdated(String, String, AgentValue),
1499
1500    /// An agent encountered an error.
1501    ///
1502    /// Fields: `(agent_id, error_message)`
1503    AgentError(String, String),
1504
1505    /// An agent received input on a port.
1506    ///
1507    /// Fields: `(agent_id, port_name)`
1508    AgentIn(String, String),
1509
1510    /// An agent's spec was updated.
1511    ///
1512    /// Fields: `(agent_id)`
1513    AgentSpecUpdated(String),
1514
1515    /// A value was written to an external output channel.
1516    ///
1517    /// This event is emitted when:
1518    /// - [`ModularAgent::write_external_input`] is called and flows through the network
1519    /// - An [`ExternalOutputAgent`](crate::external_agent::ExternalOutputAgent) receives a value
1520    ///
1521    /// Fields: `(channel_name, value)`
1522    ExternalOutput(String, AgentValue),
1523}
1524
1525#[cfg(test)]
1526mod tests {
1527    use super::*;
1528
1529    #[test]
1530    fn live_context_tokens_are_not_evicted_at_prune_threshold() {
1531        let ma = ModularAgent::new();
1532        let tokens: Vec<_> = (0..=CONTEXT_TOKEN_PRUNE_THRESHOLD)
1533            .map(|ctx_id| ma.context_token(ctx_id))
1534            .collect();
1535
1536        assert_eq!(tokens.len(), CONTEXT_TOKEN_PRUNE_THRESHOLD + 1);
1537        assert!(ma.abort_context(0));
1538        assert!(tokens[0].is_cancelled());
1539    }
1540}