diff --git a/src/agent/channel.rs b/src/agent/channel.rs index 4daddff19..6589e4420 100644 --- a/src/agent/channel.rs +++ b/src/agent/channel.rs @@ -516,12 +516,13 @@ impl Channel { } /// Get the agent's display name (falls back to agent ID). - fn agent_display_name(&self) -> &str { + fn agent_display_name(&self) -> String { self.deps .agent_names + .load() .get(self.deps.agent_id.as_ref()) - .map(String::as_str) - .unwrap_or(self.deps.agent_id.as_ref()) + .cloned() + .unwrap_or_else(|| self.deps.agent_id.to_string()) } fn current_adapter(&self) -> Option<&str> { @@ -793,10 +794,11 @@ impl Channel { .with_label_values(&[&self.deps.agent_id, channel_type]) .inc(); } + let display_name = self.agent_display_name(); self.state.conversation_logger.log_bot_message_with_name( &self.state.channel_id, &text, - Some(self.agent_display_name()), + Some(&display_name), ); } Err(error) => { @@ -1944,6 +1946,7 @@ impl Channel { let name = self .deps .agent_names + .load() .get(other_id.as_str()) .cloned() .unwrap_or_else(|| other_id.clone()); @@ -2541,10 +2544,11 @@ impl Channel { if extracted.is_some() { tracing::warn!(channel_id = %self.id, "extracted reply from malformed tool syntax in LLM text output"); } + let display_name = self.agent_display_name(); self.state.conversation_logger.log_bot_message_with_name( &self.state.channel_id, &final_text, - Some(self.agent_display_name()), + Some(&display_name), ); self.send_outbound_text(final_text, "failed to send fallback reply") .await; diff --git a/src/agent/cortex.rs b/src/agent/cortex.rs index 8e350e6e1..328045729 100644 --- a/src/agent/cortex.rs +++ b/src/agent/cortex.rs @@ -2519,7 +2519,8 @@ async fn pickup_one_ready_task(deps: &AgentDeps, logger: &CortexLogger) -> anyho let logger = logger.clone(); let injection_tx = deps.injection_tx.clone(); let links = deps.links.clone(); - let agent_names = deps.agent_names.clone(); + let agent_names: Arc> = + deps.agent_names.load_full(); let sqlite_pool = deps.sqlite_pool.clone(); let secrets_snapshot = deps.runtime_config.secrets.load().clone(); let process_control_registry = deps.process_control_registry.clone(); diff --git a/src/api/agents.rs b/src/api/agents.rs index ce75842c1..8d71b4823 100644 --- a/src/api/agents.rs +++ b/src/api/agents.rs @@ -413,6 +413,7 @@ pub(super) async fn trigger_warmup( let task_store_registry = state.task_store_registry.clone(); let injection_tx = state.injection_tx.clone(); let humans = (**state.agent_humans.load()).clone(); + let agent_names = state.agent_names.clone(); tokio::spawn(async move { let (event_tx, memory_event_tx) = crate::create_process_event_buses(); let project_store = @@ -432,7 +433,7 @@ pub(super) async fn trigger_warmup( task_store, project_store, links: Arc::new(arc_swap::ArcSwap::from_pointee(Vec::new())), - agent_names: Arc::new(std::collections::HashMap::new()), + agent_names, humans: Arc::new(arc_swap::ArcSwap::from_pointee(humans)), task_store_registry, process_control_registry: Arc::new( @@ -764,6 +765,20 @@ pub async fn create_agent_internal( // Inject active project root paths into the sandbox allowlist. crate::projects::refresh_sandbox_project_paths(&project_store, &arc_agent_id, &sandbox).await; + // Update the shared agent name registry to include this new agent so all + // existing agents can resolve it immediately via their Arc>. + { + let new_agent_name = request + .display_name + .as_deref() + .filter(|s| !s.is_empty()) + .unwrap_or(&agent_id) + .to_string(); + let mut names = (**state.agent_names.load()).clone(); + names.insert(agent_id.clone(), new_agent_name); + state.agent_names.store(std::sync::Arc::new(names)); + } + let deps = crate::AgentDeps { agent_id: arc_agent_id.clone(), memory_search: memory_search.clone(), @@ -789,26 +804,7 @@ pub async fn create_agent_internal( crate::agent::process_control::ProcessControlRegistry::new(), ), injection_tx: state.injection_tx.clone(), - agent_names: { - let configs = state.agent_configs.load(); - let mut names: std::collections::HashMap = configs - .iter() - .map(|c| { - ( - c.id.clone(), - c.display_name.clone().unwrap_or_else(|| c.id.clone()), - ) - }) - .collect(); - names.entry(agent_id.clone()).or_insert_with(|| { - request - .display_name - .clone() - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| agent_id.clone()) - }); - Arc::new(names) - }, + agent_names: state.agent_names.clone(), humans: Arc::new(arc_swap::ArcSwap::from_pointee( (**state.agent_humans.load()).clone(), )), diff --git a/src/api/state.rs b/src/api/state.rs index 9500617e1..30fb5826c 100644 --- a/src/api/state.rs +++ b/src/api/state.rs @@ -123,6 +123,8 @@ pub struct ApiState { /// Cross-agent task store registry for delegation. pub task_store_registry: Arc>>>, + /// Shared agent name map for resolving agent IDs to display names (hot-reloadable). + pub agent_names: Arc>>, /// Sender for cross-agent message injection. pub injection_tx: mpsc::Sender, /// Instance-level agent links for the communication graph. @@ -289,6 +291,7 @@ impl ApiState { task_store_registry: Arc< ArcSwap>>, >, + agent_names: Arc>>, ) -> Self { let (event_tx, _) = broadcast::channel(512); Self { @@ -328,6 +331,7 @@ impl ApiState { agent_tx, agent_remove_tx, task_store_registry, + agent_names, injection_tx, webchat_adapter: ArcSwap::from_pointee(None), agent_links: ArcSwap::from_pointee(Vec::new()), diff --git a/src/hooks/loop_guard.rs b/src/hooks/loop_guard.rs index 0e28c4a31..bc61f8f4a 100644 --- a/src/hooks/loop_guard.rs +++ b/src/hooks/loop_guard.rs @@ -448,12 +448,8 @@ impl LoopGuard { hasher.update(b"|"); hasher.update(args.as_bytes()); hasher.update(b"|"); - let truncated = if result.len() > RESULT_HASH_TRUNCATION { - &result[..RESULT_HASH_TRUNCATION] - } else { - result - }; - hasher.update(truncated.as_bytes()); + let result_bytes = result.as_bytes(); + hasher.update(&result_bytes[..result_bytes.len().min(RESULT_HASH_TRUNCATION)]); hex::encode(hasher.finalize()) } } diff --git a/src/lib.rs b/src/lib.rs index fba359745..8a79be1a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -393,7 +393,8 @@ pub struct AgentDeps { pub sandbox: Arc, pub links: Arc>>, /// Map of all agent IDs to display names, for inter-agent message routing. - pub agent_names: Arc>, + /// Hot-reloadable: updated when agents are added or reconfigured. + pub agent_names: Arc>>, /// Org-level human definitions (hot-reloadable). Used by `build_org_context()` /// to surface human display names, roles, and descriptions in agent prompts. pub humans: Arc>>, diff --git a/src/main.rs b/src/main.rs index 713802d78..d11708b9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1444,6 +1444,10 @@ async fn run( ArcSwap>>, > = Arc::new(ArcSwap::from_pointee(std::collections::HashMap::new())); + let agent_names_registry: Arc< + ArcSwap>, + > = Arc::new(ArcSwap::from_pointee(std::collections::HashMap::new())); + // Start HTTP API server if enabled let mut api_state = spacebot::api::ApiState::new_with_provider_sender( provider_tx, @@ -1451,6 +1455,7 @@ async fn run( agent_remove_tx, injection_tx.clone(), task_store_registry.clone(), + agent_names_registry.clone(), ); api_state.auth_token = config.api.auth_token.clone(); let api_state = Arc::new(api_state); @@ -1608,6 +1613,7 @@ async fn run( agent_humans.clone(), injection_tx.clone(), task_store_registry.clone(), + agent_names_registry.clone(), &bootstrapped_store, ) .await?; @@ -2337,6 +2343,7 @@ async fn run( agent_humans.clone(), injection_tx.clone(), task_store_registry.clone(), + agent_names_registry.clone(), &bootstrapped_store, ).await { Ok(()) => { @@ -2478,20 +2485,21 @@ async fn initialize_agents( task_store_registry: Arc< ArcSwap>>, >, + agent_names_registry: Arc>>, bootstrapped_store: &Option>, ) -> anyhow::Result<()> { let resolved_agents = config.resolve_agents(); - // Build agent name map for inter-agent message routing - let agent_name_map: Arc> = Arc::new( - resolved_agents - .iter() - .map(|a| { - let name = a.display_name.clone().unwrap_or_else(|| a.id.clone()); - (a.id.clone(), name) - }) - .collect(), - ); + // Build agent name map and publish to the shared registry so all agents + // (including those already running) see the updated names immediately. + let agent_name_map: std::collections::HashMap = resolved_agents + .iter() + .map(|a| { + let name = a.display_name.clone().unwrap_or_else(|| a.id.clone()); + (a.id.clone(), name) + }) + .collect(); + agent_names_registry.store(Arc::new(agent_name_map)); for agent_config in &resolved_agents { tracing::info!(agent_id = %agent_config.id, "initializing agent"); @@ -2713,7 +2721,7 @@ async fn initialize_agents( messaging_manager: None, sandbox, links: agent_links.clone(), - agent_names: agent_name_map.clone(), + agent_names: agent_names_registry.clone(), humans: agent_humans.clone(), task_store_registry: task_store_registry.clone(), process_control_registry: Arc::new( diff --git a/src/tools.rs b/src/tools.rs index 4ed3bd7f5..0d79bf977 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -348,6 +348,7 @@ pub async fn add_channel_tools( let agent_display_name = state .deps .agent_names + .load() .get(state.deps.agent_id.as_ref()) .cloned() .unwrap_or_else(|| state.deps.agent_id.to_string()); @@ -369,6 +370,7 @@ pub async fn add_channel_tools( let send_message_display_name = state .deps .agent_names + .load() .get(state.deps.agent_id.as_ref()) .cloned() .unwrap_or_else(|| state.deps.agent_id.to_string()); diff --git a/src/tools/send_agent_message.rs b/src/tools/send_agent_message.rs index 213f1da31..dec8343ad 100644 --- a/src/tools/send_agent_message.rs +++ b/src/tools/send_agent_message.rs @@ -30,7 +30,7 @@ pub struct SendAgentMessageTool { agent_id: crate::AgentId, links: Arc>>, /// Map of known agent IDs to display names, for resolving targets. - agent_names: Arc>, + agent_names: Arc>>, /// Cross-agent task store registry for creating tasks on target agents. task_store_registry: Arc>>>, /// Per-agent conversation logger for writing link channel audit records. @@ -54,7 +54,7 @@ impl SendAgentMessageTool { pub fn new( agent_id: crate::AgentId, links: Arc>>, - agent_names: Arc>, + agent_names: Arc>>, task_store_registry: Arc>>>, conversation_logger: ConversationLogger, ) -> Self { @@ -85,14 +85,16 @@ impl SendAgentMessageTool { /// Resolve an agent target string to an agent ID. /// Checks both IDs and display names (case-insensitive). fn resolve_agent_id(&self, target: &str) -> Option { + let names = self.agent_names.load(); + // Direct ID match - if self.agent_names.contains_key(target) { + if names.contains_key(target) { return Some(target.to_string()); } // Name match (case-insensitive) let target_lower = target.to_lowercase(); - for (agent_id, name) in self.agent_names.iter() { + for (agent_id, name) in names.iter() { if name.to_lowercase() == target_lower { return Some(agent_id.clone()); } @@ -199,6 +201,7 @@ impl Tool for SendAgentMessageTool { let target_display = self .agent_names + .load() .get(receiving_agent_id) .cloned() .unwrap_or_else(|| receiving_agent_id.to_string()); @@ -249,6 +252,7 @@ impl Tool for SendAgentMessageTool { // Log delegation record in the link channel (system message). let sender_display = self .agent_names + .load() .get(sending_agent_id) .cloned() .unwrap_or_else(|| sending_agent_id.to_string());