diff --git a/src/openhuman/agent/debug/mod.rs b/src/openhuman/agent/debug/mod.rs index e1a9bcaac..3abc19cca 100644 --- a/src/openhuman/agent/debug/mod.rs +++ b/src/openhuman/agent/debug/mod.rs @@ -216,6 +216,9 @@ async fn render_via_session(config: &Config, agent_id: &str) -> Result = + self.tools.iter().map(|t| t.name().to_string()).collect(); + let new_tools: Vec> = synthed + .into_iter() + .filter(|t| !existing.contains(t.name())) + .collect(); + if new_tools.is_empty() { + return; + } + + let new_names: Vec = new_tools.iter().map(|t| t.name().to_string()).collect(); + let new_specs: Vec = + new_tools.iter().map(|t| t.spec()).collect(); + + // The tools / tool_specs Arcs are uniquely owned at turn-1 build + // time (no sub-agent has captured a snapshot yet). If a caller + // managed to clone them out before the first turn fired, the + // mutation below would silently no-op — log that case so it's + // visible in diagnostics. + match ( + Arc::get_mut(&mut self.tools), + Arc::get_mut(&mut self.tool_specs), + ) { + (Some(tools_vec), Some(specs_vec)) => { + tools_vec.extend(new_tools); + specs_vec.extend(new_specs); + } + _ => { + log::warn!( + "[agent] refresh_delegation_tools: tools/tool_specs Arc is shared — \ + cannot inject delegation tools ({} would have been added: {:?})", + new_names.len(), + new_names + ); + return; + } + } + + // Definitions with a Named ToolScope carry an explicit visible + // whitelist; the synthesised delegation tools must opt in to that + // whitelist or the LLM never sees them. Wildcard-scope agents + // keep `visible_tool_names` empty ("no filter"), so we skip the + // insert there. + if !self.visible_tool_names.is_empty() { + for name in &new_names { + self.visible_tool_names.insert(name.clone()); + } + } + + // Rebuild the visible-spec cache so the next prompt build picks + // up the new tools. + let visible_specs: Vec = + if self.visible_tool_names.is_empty() { + (*self.tool_specs).clone() + } else { + self.tool_specs + .iter() + .filter(|spec| self.visible_tool_names.contains(&spec.name)) + .cloned() + .collect() + }; + self.visible_tool_specs = Arc::new(visible_specs); + + log::info!( + "[agent] refresh_delegation_tools: added {} delegation tool(s) for agent '{}' (display='{}'): {:?}", + new_names.len(), + self.agent_definition_id, + self.agent_definition_name, + new_names + ); + } + /// Builds the system prompt for the current turn, including tool /// instructions and learned context. pub fn build_system_prompt(&self, learned: LearnedContextData) -> Result { diff --git a/src/openhuman/agent/harness/session/types.rs b/src/openhuman/agent/harness/session/types.rs index 67094172c..f2696e000 100644 --- a/src/openhuman/agent/harness/session/types.rs +++ b/src/openhuman/agent/harness/session/types.rs @@ -71,7 +71,25 @@ pub struct Agent { /// Human-readable agent definition name (e.g. `"main"`, /// `"code_executor"`). Used as the `{agent}` component in session /// transcript paths: `sessions/DDMMYYYY/{agent}_{index}.md`. + /// + /// May be rewritten mid-session by + /// [`Agent::set_agent_definition_name`] (e.g. the web channel + /// stamps `"orchestrator_"` so each thread gets its + /// own transcript namespace). Anything that needs to resolve the + /// session back to its registry entry must use + /// [`Self::agent_definition_id`], not this field. pub(super) agent_definition_name: String, + /// Canonical agent id as registered in + /// [`AgentDefinitionRegistry`] (e.g. `"orchestrator"`, + /// `"integrations_agent"`). Set once at build time and never + /// rewritten — `set_agent_definition_name` only touches the + /// transcript-facing `agent_definition_name`, so registry lookups + /// (e.g. `refresh_delegation_tools` re-resolving the agent's + /// `subagents` list post-fetch) stay correct even after the web + /// channel's per-thread rename. + /// + /// [`AgentDefinitionRegistry`]: crate::openhuman::agent::harness::definition::AgentDefinitionRegistry + pub(super) agent_definition_id: String, /// Resolved filesystem path for this session's transcript file. /// Set on first write, reused for subsequent overwrites within the /// same session.