use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, Weak};
use serde_json::Value;
use tokio::sync::{Mutex as AsyncMutex, broadcast, broadcast::error::RecvError, mpsc};
use tokio_util::sync::CancellationToken;
use crate::FnvIndexMap;
use crate::agent::{Agent, AgentMessage, AgentStatus, agent_new};
use crate::config::{AgentConfigs, AgentConfigsMap};
use crate::context::AgentContext;
use crate::definition::{AgentConfigSpecs, AgentDefinition, AgentDefinitions};
use crate::error::AgentError;
use crate::id::{new_id, update_ids};
use crate::message::{self, AgentEventMessage};
use crate::preset::{Preset, PresetInfo};
use crate::registry;
use crate::spec::{AgentSpec, ConnectionSpec, PresetSpec};
use crate::value::AgentValue;
const MESSAGE_LIMIT: usize = 1024;
const EVENT_CHANNEL_CAPACITY: usize = 256;
const CONTEXT_TOKEN_PRUNE_THRESHOLD: usize = 1024;
static AGENT_TOKEN_GENERATION: AtomicU64 = AtomicU64::new(1);
#[cfg_attr(feature = "file", doc = "```rust,no_run")]
#[cfg_attr(not(feature = "file"), doc = "```rust,no_run,ignore")]
pub type SharedAgent = Arc<AsyncMutex<Box<dyn Agent>>>;
pub(crate) type ConnectionTarget = (String, String, String);
#[derive(Clone)]
pub struct ModularAgent {
pub(crate) agents: Arc<Mutex<FnvIndexMap<String, SharedAgent>>>,
pub(crate) agent_txs: Arc<Mutex<FnvIndexMap<String, mpsc::Sender<AgentMessage>>>>,
pub(crate) external_input_agents: Arc<Mutex<FnvIndexMap<String, Vec<String>>>>,
pub(crate) external_values: Arc<Mutex<FnvIndexMap<String, AgentValue>>>,
pub(crate) connections: Arc<Mutex<FnvIndexMap<String, Vec<ConnectionTarget>>>>,
pub(crate) defs: Arc<Mutex<AgentDefinitions>>,
pub(crate) presets: Arc<Mutex<FnvIndexMap<String, Arc<AsyncMutex<Preset>>>>>,
pub(crate) preset_names: Arc<Mutex<FnvIndexMap<String, String>>>,
pub(crate) global_configs_map: Arc<Mutex<FnvIndexMap<String, AgentConfigs>>>,
pub(crate) preset_tokens: Arc<Mutex<FnvIndexMap<String, CancellationToken>>>,
pub(crate) agent_tokens: Arc<Mutex<FnvIndexMap<String, (u64, CancellationToken)>>>,
pub(crate) context_tokens: Arc<Mutex<FnvIndexMap<usize, Weak<CancellationToken>>>>,
pub(crate) tx: Arc<Mutex<Option<mpsc::Sender<AgentEventMessage>>>>,
pub(crate) observers: broadcast::Sender<EventEnvelope>,
pub(crate) origin: Option<Arc<str>>,
}
impl Default for ModularAgent {
fn default() -> Self {
Self::new()
}
}
impl ModularAgent {
pub fn new() -> Self {
let (tx, _rx) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
Self {
agents: Default::default(),
agent_txs: Default::default(),
external_input_agents: Default::default(),
external_values: Default::default(),
connections: Default::default(),
defs: Default::default(),
presets: Default::default(),
preset_names: Default::default(),
global_configs_map: Default::default(),
preset_tokens: Default::default(),
agent_tokens: Default::default(),
context_tokens: Default::default(),
tx: Arc::new(Mutex::new(None)),
observers: tx,
origin: None,
}
}
pub fn with_origin(&self, origin: impl Into<Arc<str>>) -> Self {
Self {
origin: Some(origin.into()),
..self.clone()
}
}
pub(crate) fn base(&self) -> Self {
Self {
origin: None,
..self.clone()
}
}
pub(crate) fn tx(&self) -> Result<mpsc::Sender<AgentEventMessage>, AgentError> {
self.tx
.lock()
.unwrap()
.clone()
.ok_or(AgentError::TxNotInitialized)
}
pub fn init() -> Result<Self, AgentError> {
let ma = Self::new();
ma.register_agents();
Ok(ma)
}
fn register_agents(&self) {
registry::register_inventory_agents(self);
}
pub async fn ready(&self) -> Result<(), AgentError> {
self.spawn_message_loop().await?;
Ok(())
}
pub fn quit(&self) {
let mut tx_lock = self.tx.lock().unwrap();
*tx_lock = None;
}
pub async fn shutdown(&self) -> Result<(), AgentError> {
self.quit();
#[cfg(feature = "mcp")]
crate::mcp::shutdown_all_mcp_connections().await?;
Ok(())
}
pub fn new_preset(&self) -> Result<String, AgentError> {
let spec = PresetSpec::default();
let id = self.add_preset(spec)?;
Ok(id)
}
pub fn new_preset_with_name(&self, name: String) -> Result<String, AgentError> {
let spec = PresetSpec::default();
let id = self.add_preset_with_name(spec, name)?;
Ok(id)
}
pub fn get_preset(&self, id: &str) -> Option<Arc<AsyncMutex<Preset>>> {
let presets = self.presets.lock().unwrap();
presets.get(id).cloned()
}
pub fn find_preset_id_by_name(&self, name: &str) -> Option<String> {
let names = self.preset_names.lock().unwrap();
names.get(name).cloned()
}
pub fn add_preset(&self, spec: PresetSpec) -> Result<String, AgentError> {
self.add_preset_raw(spec, None)
}
pub fn add_preset_with_name(
&self,
spec: PresetSpec,
name: String,
) -> Result<String, AgentError> {
self.add_preset_raw(spec, Some(name))
}
fn add_preset_raw(&self, spec: PresetSpec, name: Option<String>) -> Result<String, AgentError> {
let mut preset = Preset::new(spec);
if let Some(name) = &name {
preset.set_name(name.clone());
}
let id = preset.id().to_string();
if let Some(name) = &name {
let mut names = self.preset_names.lock().unwrap();
if names.contains_key(name) {
return Err(AgentError::PresetNameExists(name.clone()));
}
names.insert(name.clone(), id.clone());
}
for agent in &preset.spec().agents {
if let Err(e) = self.add_agent_internal(id.clone(), agent.clone()) {
log::error!("Failed to add_agent {}: {}", agent.id, e);
}
}
for connection in &preset.spec().connections {
self.add_connection_internal(connection.clone())
.unwrap_or_else(|e| {
log::error!("Failed to add_connection {}: {}", connection.source, e);
});
}
let inserted = {
let mut presets = self.presets.lock().unwrap();
if presets.contains_key(&id) {
false
} else {
presets.insert(id.clone(), Arc::new(AsyncMutex::new(preset)));
true
}
};
if !inserted {
if let Some(name) = &name {
self.preset_names.lock().unwrap().swap_remove(name);
}
return Err(AgentError::DuplicateId(id));
}
self.emit_preset_added(id.clone(), name);
Ok(id)
}
pub async fn rename_preset(&self, id: &str, new_name: String) -> Result<(), AgentError> {
let preset = self
.get_preset(id)
.ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
{
let mut names = self.preset_names.lock().unwrap();
if let Some(owner) = names.get(&new_name)
&& owner != id
{
return Err(AgentError::PresetNameExists(new_name));
}
names.retain(|_, v| v != id);
names.insert(new_name.clone(), id.to_string());
}
if !self.presets.lock().unwrap().contains_key(id) {
let mut names = self.preset_names.lock().unwrap();
if names.get(&new_name).is_some_and(|owner| owner == id) {
names.swap_remove(&new_name);
}
return Err(AgentError::PresetNotFound(id.to_string()));
}
let old_name = {
let mut preset = preset.lock().await;
let old_name = preset.name().map(str::to_string);
preset.set_name(new_name.clone());
old_name
};
self.emit_preset_renamed(id.to_string(), old_name, new_name);
Ok(())
}
pub async fn remove_preset(&self, id: &str) -> Result<(), AgentError> {
let preset = self
.get_preset(id)
.ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
let mut preset = preset.lock().await;
let name = preset.name().map(str::to_string);
preset.stop(self).await.unwrap_or_else(|e| {
log::error!("Failed to stop preset {}: {}", id, e);
});
for agent in &preset.spec().agents {
self.remove_agent_internal(&agent.id)
.await
.unwrap_or_else(|e| {
log::error!("Failed to remove_agent {}: {}", agent.id, e);
});
}
for connection in &preset.spec().connections {
self.remove_connection_internal(connection);
}
drop(preset);
{
let mut presets = self.presets.lock().unwrap();
presets.swap_remove(id);
}
self.preset_names.lock().unwrap().retain(|_, v| v != id);
self.remove_preset_token(id);
self.emit_preset_removed(id.to_string(), name);
Ok(())
}
pub async fn start_preset(&self, id: &str) -> Result<(), AgentError> {
let preset = self
.get_preset(id)
.ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
let mut preset = preset.lock().await;
preset.start(self).await?;
Ok(())
}
pub async fn stop_preset(&self, id: &str) -> Result<(), AgentError> {
let preset = self
.get_preset(id)
.ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
let mut preset = preset.lock().await;
preset.stop(self).await?;
Ok(())
}
#[cfg(feature = "file")]
pub async fn open_preset_from_file(
&self,
path: &str,
name: Option<String>,
) -> Result<String, AgentError> {
let json_str =
std::fs::read_to_string(path).map_err(|e| AgentError::IoError(e.to_string()))?;
let spec = PresetSpec::from_json(&json_str)?;
let id = self.add_preset_raw(spec, name)?;
Ok(id)
}
#[cfg(feature = "file")]
pub async fn save_preset(&self, id: &str, path: &str) -> Result<(), AgentError> {
let Some(preset_spec) = self.get_preset_spec(id).await else {
return Err(AgentError::PresetNotFound(id.to_string()));
};
let json_str = preset_spec.to_json()?;
std::fs::write(path, json_str).map_err(|e| AgentError::IoError(e.to_string()))?;
if let Some(name) = self.get_preset_info(id).await.and_then(|info| info.name) {
self.emit_preset_saved(id.to_string(), name);
}
Ok(())
}
pub async fn get_preset_spec(&self, id: &str) -> Option<PresetSpec> {
let preset = self.get_preset(id)?;
let mut preset_spec = {
let preset = preset.lock().await;
preset.spec().clone()
};
let mut agent_specs = Vec::new();
for agent in &preset_spec.agents {
if let Some(spec) = self.get_agent_spec(&agent.id).await {
agent_specs.push(spec);
}
}
preset_spec.agents = agent_specs;
Some(preset_spec)
}
pub async fn update_preset_spec(&self, id: &str, value: &Value) -> Result<(), AgentError> {
let preset = self
.get_preset(id)
.ok_or_else(|| AgentError::PresetNotFound(id.to_string()))?;
let mut preset = preset.lock().await;
preset.update_spec(value)?;
drop(preset);
self.emit_preset_structure_changed(id.to_string());
Ok(())
}
pub async fn get_preset_info(&self, id: &str) -> Option<PresetInfo> {
let preset = self.get_preset(id)?;
Some(PresetInfo::from(&*preset.lock().await))
}
pub async fn get_preset_infos(&self) -> Vec<PresetInfo> {
let presets = {
let presets = self.presets.lock().unwrap();
presets.values().cloned().collect::<Vec<_>>()
};
let mut preset_infos = Vec::new();
for preset in presets {
let preset_guard = preset.lock().await;
preset_infos.push(PresetInfo::from(&*preset_guard));
}
preset_infos
}
pub fn register_agent_definiton(&self, def: AgentDefinition) {
let def_name = def.name.clone();
let def_global_configs = def.global_configs.clone();
let mut defs = self.defs.lock().unwrap();
defs.insert(def.name.clone(), def);
if let Some(def_global_configs) = def_global_configs {
let mut new_configs = AgentConfigs::default();
for (key, config_entry) in def_global_configs.iter() {
new_configs.set(key.clone(), config_entry.value.clone());
}
self.set_global_configs(def_name, new_configs);
}
}
pub fn get_agent_definitions(&self) -> AgentDefinitions {
let defs = self.defs.lock().unwrap();
defs.clone()
}
pub fn get_agent_definition(&self, def_name: &str) -> Option<AgentDefinition> {
let defs = self.defs.lock().unwrap();
defs.get(def_name).cloned()
}
pub fn get_agent_config_specs(&self, def_name: &str) -> Option<AgentConfigSpecs> {
let defs = self.defs.lock().unwrap();
let def = defs.get(def_name)?;
def.configs.clone()
}
pub async fn get_agent_spec(&self, agent_id: &str) -> Option<AgentSpec> {
let agent = {
let agents = self.agents.lock().unwrap();
agents.get(agent_id)?.clone()
};
let agent = agent.lock().await;
Some(agent.spec().clone())
}
pub async fn update_agent_spec(&self, agent_id: &str, value: &Value) -> Result<(), AgentError> {
let agent = {
let agents = self.agents.lock().unwrap();
let Some(agent) = agents.get(agent_id) else {
return Err(AgentError::AgentNotFound(agent_id.to_string()));
};
agent.clone()
};
let preset_id = {
let mut agent = agent.lock().await;
agent.update_spec(value)?;
agent.preset_id().to_string()
};
self.emit_agent_spec_updated(agent_id.to_string());
let structural = value
.as_object()
.is_some_and(|map| map.keys().any(|key| key != "configs"));
if structural {
self.emit_preset_structure_changed(preset_id);
}
Ok(())
}
pub fn new_agent_spec(&self, def_name: &str) -> Result<AgentSpec, AgentError> {
let def = self
.get_agent_definition(def_name)
.ok_or_else(|| AgentError::AgentDefinitionNotFound(def_name.to_string()))?;
Ok(def.to_spec())
}
pub async fn add_agent(
&self,
preset_id: String,
mut spec: AgentSpec,
) -> Result<String, AgentError> {
let preset = self
.get_preset(&preset_id)
.ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
let id = new_id();
spec.id = id.clone();
self.add_agent_internal(preset_id.clone(), spec.clone())?;
let mut preset = preset.lock().await;
preset.add_agent(spec.clone());
drop(preset);
self.emit_preset_structure_changed(preset_id);
Ok(id)
}
fn add_agent_internal(&self, preset_id: String, spec: AgentSpec) -> Result<(), AgentError> {
let mut agents = self.agents.lock().unwrap();
if agents.contains_key(&spec.id) {
return Err(AgentError::AgentAlreadyExists(spec.id.to_string()));
}
let spec_id = spec.id.clone();
let mut agent = agent_new(self.base(), spec_id.clone(), spec)?;
agent.set_preset_id(preset_id);
agents.insert(spec_id, Arc::new(AsyncMutex::new(agent)));
Ok(())
}
pub fn get_agent(&self, agent_id: &str) -> Option<SharedAgent> {
let agents = self.agents.lock().unwrap();
agents.get(agent_id).cloned()
}
pub async fn add_connection(
&self,
preset_id: &str,
connection: ConnectionSpec,
) -> Result<(), AgentError> {
{
let agents = self.agents.lock().unwrap();
if !agents.contains_key(&connection.source) {
return Err(AgentError::AgentNotFound(connection.source.to_string()));
}
if !agents.contains_key(&connection.target) {
return Err(AgentError::AgentNotFound(connection.target.to_string()));
}
}
if connection.source_handle.is_empty() {
return Err(AgentError::EmptySourceHandle);
}
if connection.target_handle.is_empty() {
return Err(AgentError::EmptyTargetHandle);
}
let preset = self
.get_preset(preset_id)
.ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
let mut preset = preset.lock().await;
self.add_connection_internal(connection.clone())?;
preset.add_connection(connection);
drop(preset);
self.emit_preset_structure_changed(preset_id.to_string());
Ok(())
}
fn add_connection_internal(&self, connection: ConnectionSpec) -> Result<(), AgentError> {
let mut connections = self.connections.lock().unwrap();
if let Some(targets) = connections.get_mut(&connection.source) {
if targets
.iter()
.any(|(target, source_handle, target_handle)| {
*target == connection.target
&& *source_handle == connection.source_handle
&& *target_handle == connection.target_handle
})
{
return Err(AgentError::ConnectionAlreadyExists);
}
targets.push((
connection.target,
connection.source_handle,
connection.target_handle,
));
} else {
connections.insert(
connection.source,
vec![(
connection.target,
connection.source_handle,
connection.target_handle,
)],
);
}
Ok(())
}
pub fn has_connections(&self, source_agent: &str, port: &str) -> bool {
let connections = self.connections.lock().unwrap();
connections.get(source_agent).is_some_and(|targets| {
targets
.iter()
.any(|(_, source_port, _)| source_port == port)
})
}
pub async fn add_agents_and_connections(
&self,
preset_id: &str,
agents: &Vec<AgentSpec>,
connections: &Vec<ConnectionSpec>,
) -> Result<(Vec<AgentSpec>, Vec<ConnectionSpec>), AgentError> {
let (agents, connections) = update_ids(agents, connections);
let preset = self
.get_preset(preset_id)
.ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
let mut preset = preset.lock().await;
let mut added_agents = 0;
let mut added_connections = 0;
let mut result = Ok(());
for agent in &agents {
if let Err(e) = self.add_agent_internal(preset_id.to_string(), agent.clone()) {
result = Err(e);
break;
}
preset.add_agent(agent.clone());
added_agents += 1;
}
if result.is_ok() {
for connection in &connections {
if let Err(e) = self.add_connection_internal(connection.clone()) {
result = Err(e);
break;
}
preset.add_connection(connection.clone());
added_connections += 1;
}
}
if let Err(e) = result {
for connection in connections.iter().take(added_connections) {
preset.remove_connection(connection);
self.remove_connection_internal(connection);
}
let mut agents_map = self.agents.lock().unwrap();
for agent in agents.iter().take(added_agents) {
preset.remove_agent(&agent.id);
agents_map.swap_remove(&agent.id);
}
return Err(e);
}
drop(preset);
self.emit_preset_structure_changed(preset_id.to_string());
Ok((agents, connections))
}
pub async fn remove_agent(&self, preset_id: &str, agent_id: &str) -> Result<(), AgentError> {
let preset = self
.get_preset(preset_id)
.ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
let runtime_removed = match self.remove_agent_internal(agent_id).await {
Ok(()) => true,
Err(AgentError::AgentNotFound(_)) => false,
Err(e) => return Err(e),
};
let spec_removed = {
let mut preset = preset.lock().await;
let count_before = preset.spec().agents.len();
preset.remove_agent(agent_id);
preset.spec().agents.len() != count_before
};
if !runtime_removed && !spec_removed {
return Err(AgentError::AgentNotFound(agent_id.to_string()));
}
self.emit_preset_structure_changed(preset_id.to_string());
Ok(())
}
async fn remove_agent_internal(&self, agent_id: &str) -> Result<(), AgentError> {
self.stop_agent(agent_id).await?;
{
let mut connections = self.connections.lock().unwrap();
let mut sources_to_remove = Vec::new();
for (source, targets) in connections.iter_mut() {
targets.retain(|(target, _, _)| target != agent_id);
if targets.is_empty() {
sources_to_remove.push(source.clone());
}
}
for source in sources_to_remove {
connections.swap_remove(&source);
}
connections.swap_remove(agent_id);
}
{
let mut agents = self.agents.lock().unwrap();
agents.swap_remove(agent_id);
}
Ok(())
}
pub async fn remove_connection(
&self,
preset_id: &str,
connection: &ConnectionSpec,
) -> Result<(), AgentError> {
let preset = self
.get_preset(preset_id)
.ok_or_else(|| AgentError::PresetNotFound(preset_id.to_string()))?;
let mut preset = preset.lock().await;
let Some(connection) = preset.remove_connection(connection) else {
return Err(AgentError::ConnectionNotFound(format!(
"{}:{}->{}:{}",
connection.source,
connection.source_handle,
connection.target,
connection.target_handle
)));
};
self.remove_connection_internal(&connection);
drop(preset);
self.emit_preset_structure_changed(preset_id.to_string());
Ok(())
}
fn remove_connection_internal(&self, connection: &ConnectionSpec) {
let mut connections = self.connections.lock().unwrap();
if let Some(targets) = connections.get_mut(&connection.source) {
targets.retain(|(target, source_handle, target_handle)| {
*target != connection.target
|| *source_handle != connection.source_handle
|| *target_handle != connection.target_handle
});
if targets.is_empty() {
connections.swap_remove(&connection.source);
}
}
}
fn preset_token(&self, preset_id: &str) -> CancellationToken {
let mut tokens = self.preset_tokens.lock().unwrap();
tokens.entry(preset_id.to_string()).or_default().clone()
}
pub(crate) fn reset_preset_token(&self, preset_id: &str) {
let mut tokens = self.preset_tokens.lock().unwrap();
tokens.insert(preset_id.to_string(), CancellationToken::new());
}
pub(crate) fn cancel_preset_token(&self, preset_id: &str) {
let token = self.preset_tokens.lock().unwrap().get(preset_id).cloned();
if let Some(token) = token {
token.cancel();
}
}
pub(crate) fn remove_preset_token(&self, preset_id: &str) {
self.preset_tokens.lock().unwrap().swap_remove(preset_id);
}
fn create_agent_token(&self, preset_id: &str, agent_id: &str) -> (u64, CancellationToken) {
let generation = AGENT_TOKEN_GENERATION.fetch_add(1, Ordering::Relaxed);
let token = self.preset_token(preset_id).child_token();
self.agent_tokens
.lock()
.unwrap()
.insert(agent_id.to_string(), (generation, token.clone()));
(generation, token)
}
fn renew_agent_token(
&self,
preset_id: &str,
agent_id: &str,
generation: u64,
) -> Option<CancellationToken> {
let parent = self.preset_tokens.lock().unwrap().get(preset_id).cloned()?;
let fresh = parent.child_token();
let mut tokens = self.agent_tokens.lock().unwrap();
let slot = tokens.get_mut(agent_id)?;
if slot.0 != generation {
return None;
}
slot.1 = fresh.clone();
Some(fresh)
}
pub(crate) fn context_token(&self, ctx_id: usize) -> Arc<CancellationToken> {
let mut tokens = self.context_tokens.lock().unwrap();
if let Some(token) = tokens.get(&ctx_id).and_then(Weak::upgrade) {
return token;
}
if tokens.len() >= CONTEXT_TOKEN_PRUNE_THRESHOLD {
tokens.retain(|_, weak| weak.strong_count() > 0);
}
let token = Arc::new(CancellationToken::new());
tokens.insert(ctx_id, Arc::downgrade(&token));
token
}
pub fn abort_context(&self, ctx_id: usize) -> bool {
let token = self
.context_tokens
.lock()
.unwrap()
.get(&ctx_id)
.and_then(Weak::upgrade);
match token {
Some(token) => {
token.cancel();
true
}
None => {
log::warn!("abort_context: no live flow for context {}", ctx_id);
false
}
}
}
pub async fn start_agent(&self, agent_id: &str) -> Result<(), AgentError> {
let agent = {
let agents = self.agents.lock().unwrap();
let Some(a) = agents.get(agent_id) else {
return Err(AgentError::AgentNotFound(agent_id.to_string()));
};
a.clone()
};
let (def_name, preset_id) = {
let agent = agent.lock().await;
(agent.def_name().to_string(), agent.preset_id().to_string())
};
let uses_native_thread = {
let defs = self.defs.lock().unwrap();
let Some(def) = defs.get(&def_name) else {
return Err(AgentError::AgentDefinitionNotFound(agent_id.to_string()));
};
def.native_thread
};
let agent_status = {
let agent = agent.lock().await;
agent.status().clone()
};
if agent_status == AgentStatus::Init {
log::info!("Starting agent {}", agent_id);
let (tx, mut rx) = mpsc::channel(MESSAGE_LIMIT);
{
let mut agent_txs = self.agent_txs.lock().unwrap();
agent_txs.insert(agent_id.to_string(), tx.clone());
};
let agent_clone = agent.clone();
let agent_id_clone = agent_id.to_string();
let ma = self.base();
let (generation, mut token) = self.create_agent_token(&preset_id, agent_id);
let agent_loop = async move {
let start = async {
let mut agent_guard = agent_clone.lock().await;
agent_guard.start().await
};
tokio::select! {
biased;
_ = token.cancelled() => {
log::info!("Start cancelled: {}", agent_id_clone);
return;
}
r = start => {
if let Err(e) = r {
log::error!("Failed to start agent {}: {}", agent_id_clone, e);
return;
}
}
}
while let Some(message) = rx.recv().await {
match message {
AgentMessage::Input { ctx, port, value } => {
let ctx = if ctx.cancel_token().is_none() {
ctx.with_cancel_token(ma.context_token(ctx.id()))
} else {
ctx
};
let fut =
async { agent_clone.lock().await.process(ctx, port, value).await };
tokio::select! {
biased;
_ = token.cancelled() => {
log::info!("Process cancelled: {}", agent_id_clone);
if let Some(fresh) = ma.renew_agent_token(
&preset_id,
&agent_id_clone,
generation,
) {
token = fresh;
}
}
r = fut => r.unwrap_or_else(|e| {
log::error!("Process Error {}: {}", agent_id_clone, e);
}),
}
}
AgentMessage::Config { key, value } => {
agent_clone
.lock()
.await
.set_config(key, value)
.unwrap_or_else(|e| {
log::error!("Config Error {}: {}", agent_id_clone, e);
});
}
AgentMessage::Configs { configs } => {
agent_clone
.lock()
.await
.set_configs(configs)
.unwrap_or_else(|e| {
log::error!("Configs Error {}: {}", agent_id_clone, e);
});
}
AgentMessage::Stop => {
rx.close();
break;
}
}
}
};
if uses_native_thread {
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(agent_loop);
});
} else {
tokio::spawn(agent_loop);
}
}
Ok(())
}
pub async fn stop_agent(&self, agent_id: &str) -> Result<(), AgentError> {
{
let mut agent_txs = self.agent_txs.lock().unwrap();
if let Some(tx) = agent_txs.swap_remove(agent_id)
&& let Err(e) = tx.try_send(AgentMessage::Stop)
{
log::warn!("Failed to send stop message to agent {}: {}", agent_id, e);
}
}
let token = self.agent_tokens.lock().unwrap().swap_remove(agent_id);
if let Some((_, token)) = token {
token.cancel();
}
let agent = {
let agents = self.agents.lock().unwrap();
let Some(a) = agents.get(agent_id) else {
return Err(AgentError::AgentNotFound(agent_id.to_string()));
};
a.clone()
};
let mut agent_guard = agent.lock().await;
if *agent_guard.status() == AgentStatus::Start {
log::info!("Stopping agent {}", agent_id);
agent_guard.stop().await?;
}
Ok(())
}
pub async fn set_agent_configs(
&self,
agent_id: String,
configs: AgentConfigs,
) -> Result<(), AgentError> {
let tx = {
let agent_txs = self.agent_txs.lock().unwrap();
agent_txs.get(&agent_id).cloned()
};
let Some(tx) = tx else {
let agent = {
let agents = self.agents.lock().unwrap();
let Some(a) = agents.get(&agent_id) else {
return Err(AgentError::AgentNotFound(agent_id.to_string()));
};
a.clone()
};
agent.lock().await.set_configs(configs.clone())?;
for (key, value) in configs {
self.emit_agent_config_updated(agent_id.clone(), key, value);
}
return Ok(());
};
let message = AgentMessage::Configs {
configs: configs.clone(),
};
tx.send(message).await.map_err(|_| {
AgentError::SendMessageFailed("Failed to send config message".to_string())
})?;
for (key, value) in configs {
self.emit_agent_config_updated(agent_id.clone(), key, value);
}
Ok(())
}
pub fn get_global_configs(&self, def_name: &str) -> Option<AgentConfigs> {
let global_configs_map = self.global_configs_map.lock().unwrap();
global_configs_map.get(def_name).cloned()
}
pub fn set_global_configs(&self, def_name: String, configs: AgentConfigs) {
let mut global_configs_map = self.global_configs_map.lock().unwrap();
let Some(existing_configs) = global_configs_map.get_mut(&def_name) else {
global_configs_map.insert(def_name, configs);
return;
};
for (key, value) in configs {
existing_configs.set(key, value);
}
}
pub fn get_global_configs_map(&self) -> AgentConfigsMap {
let global_configs_map = self.global_configs_map.lock().unwrap();
global_configs_map.clone()
}
pub fn set_global_configs_map(&self, new_configs_map: AgentConfigsMap) {
for (agent_name, new_configs) in new_configs_map {
self.set_global_configs(agent_name, new_configs);
}
}
pub(crate) async fn agent_input(
&self,
agent_id: String,
ctx: AgentContext,
port: String,
value: AgentValue,
) -> Result<(), AgentError> {
let message = if let Some(config_key) = port.strip_prefix("config:") {
AgentMessage::Config {
key: config_key.to_string(),
value,
}
} else {
AgentMessage::Input {
ctx,
port: port.clone(),
value,
}
};
let tx = {
let agent_txs = self.agent_txs.lock().unwrap();
agent_txs.get(&agent_id).cloned()
};
let Some(tx) = tx else {
let agent: SharedAgent = {
let agents = self.agents.lock().unwrap();
let Some(a) = agents.get(&agent_id) else {
return Err(AgentError::AgentNotFound(agent_id.to_string()));
};
a.clone()
};
if let AgentMessage::Config { key, value } = message {
agent.lock().await.set_config(key, value)?;
}
return Ok(());
};
tx.send(message).await.map_err(|_| {
AgentError::SendMessageFailed("Failed to send input message".to_string())
})?;
self.emit_agent_input(agent_id.to_string(), port);
Ok(())
}
pub async fn send_agent_out(
&self,
agent_id: String,
ctx: AgentContext,
port: String,
value: AgentValue,
) -> Result<(), AgentError> {
message::send_agent_out(self, agent_id, ctx, port, value).await
}
pub fn try_send_agent_out(
&self,
agent_id: String,
ctx: AgentContext,
port: String,
value: AgentValue,
) -> Result<(), AgentError> {
message::try_send_agent_out(self, agent_id, ctx, port, value)
}
pub async fn write_external_input(
&self,
name: String,
value: AgentValue,
) -> Result<(), AgentError> {
self.send_external_output(name, AgentContext::new(), value)
.await
}
pub async fn write_local_input(
&self,
preset_id: &str,
name: &str,
value: AgentValue,
) -> Result<(), AgentError> {
let channel_name = format!("%{}/{}", preset_id, name);
self.send_external_output(channel_name, AgentContext::new(), value)
.await
}
pub(crate) async fn send_external_output(
&self,
name: String,
ctx: AgentContext,
value: AgentValue,
) -> Result<(), AgentError> {
message::send_external_output(self, name, ctx, value).await
}
async fn spawn_message_loop(&self) -> Result<(), AgentError> {
let (tx, mut rx) = mpsc::channel(4096);
{
let mut tx_lock = self.tx.lock().unwrap();
*tx_lock = Some(tx);
}
let ma = self.base();
tokio::spawn(async move {
while let Some(message) = rx.recv().await {
use AgentEventMessage::*;
match message {
AgentOut {
agent,
ctx,
port,
value,
} => {
message::agent_out(&ma, agent, ctx, port, value).await;
}
ExternalOutput { name, ctx, value } => {
message::external_input(&ma, name, ctx, value).await;
}
}
}
});
tokio::task::yield_now().await;
Ok(())
}
pub fn subscribe(&self) -> broadcast::Receiver<EventEnvelope> {
self.observers.subscribe()
}
pub fn subscribe_to_event<F, T>(&self, mut filter_map: F) -> mpsc::UnboundedReceiver<T>
where
F: FnMut(EventEnvelope) -> Option<T> + Send + 'static,
T: Send + 'static,
{
let (tx, rx) = mpsc::unbounded_channel();
let mut event_rx = self.subscribe();
tokio::spawn(async move {
loop {
match event_rx.recv().await {
Ok(envelope) => {
if let Some(mapped_event) = filter_map(envelope)
&& tx.send(mapped_event).is_err()
{
break;
}
}
Err(RecvError::Lagged(n)) => {
log::warn!("Event subscriber lagged by {} events", n);
}
Err(RecvError::Closed) => {
break;
}
}
}
});
rx
}
pub(crate) fn emit_agent_config_updated(
&self,
agent_id: String,
key: String,
value: AgentValue,
) {
self.notify_observers(ModularAgentEvent::AgentConfigUpdated(agent_id, key, value));
}
pub(crate) fn emit_agent_error(&self, agent_id: String, message: String) {
self.notify_observers(ModularAgentEvent::AgentError(agent_id, message));
}
pub(crate) fn emit_agent_input(&self, agent_id: String, port: String) {
self.notify_observers(ModularAgentEvent::AgentIn(agent_id, port));
}
pub(crate) fn emit_agent_spec_updated(&self, agent_id: String) {
self.notify_observers(ModularAgentEvent::AgentSpecUpdated(agent_id));
}
pub(crate) fn emit_preset_structure_changed(&self, preset_id: String) {
self.notify_observers(ModularAgentEvent::PresetStructureChanged { preset_id });
}
pub(crate) fn emit_preset_added(&self, preset_id: String, name: Option<String>) {
self.notify_observers(ModularAgentEvent::PresetAdded { preset_id, name });
}
pub(crate) fn emit_preset_removed(&self, preset_id: String, name: Option<String>) {
self.notify_observers(ModularAgentEvent::PresetRemoved { preset_id, name });
}
pub(crate) fn emit_preset_renamed(
&self,
preset_id: String,
old_name: Option<String>,
new_name: String,
) {
self.notify_observers(ModularAgentEvent::PresetRenamed {
preset_id,
old_name,
new_name,
});
}
#[cfg(feature = "file")]
pub(crate) fn emit_preset_saved(&self, preset_id: String, name: String) {
self.notify_observers(ModularAgentEvent::PresetSaved { preset_id, name });
}
pub(crate) fn emit_external_output(&self, name: String, value: AgentValue) {
self.notify_observers(ModularAgentEvent::ExternalOutput(name, value));
}
fn notify_observers(&self, event: ModularAgentEvent) {
let _ = self.observers.send(EventEnvelope {
origin: self.origin.clone(),
event,
});
}
}
#[derive(Clone, Debug)]
pub struct EventEnvelope {
pub origin: Option<Arc<str>>,
pub event: ModularAgentEvent,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ModularAgentEvent {
AgentConfigUpdated(String, String, AgentValue),
AgentError(String, String),
AgentIn(String, String),
AgentSpecUpdated(String),
PresetStructureChanged { preset_id: String },
PresetAdded {
preset_id: String,
name: Option<String>,
},
PresetRemoved {
preset_id: String,
name: Option<String>,
},
PresetRenamed {
preset_id: String,
old_name: Option<String>,
new_name: String,
},
#[cfg(feature = "file")]
PresetSaved { preset_id: String, name: String },
ExternalOutput(String, AgentValue),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn live_context_tokens_are_not_evicted_at_prune_threshold() {
let ma = ModularAgent::new();
let tokens: Vec<_> = (0..=CONTEXT_TOKEN_PRUNE_THRESHOLD)
.map(|ctx_id| ma.context_token(ctx_id))
.collect();
assert_eq!(tokens.len(), CONTEXT_TOKEN_PRUNE_THRESHOLD + 1);
assert!(ma.abort_context(0));
assert!(tokens[0].is_cancelled());
}
}