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}