From 269b58ef30a979f9ffcba3ca73bb69d66723d923 Mon Sep 17 00:00:00 2001 From: Shanu Date: Thu, 21 May 2026 18:26:21 +0530 Subject: [PATCH 1/9] refactor(session): change dedup_visible_tool_specs visibility to crate level and re-export for sibling modules - Updated the visibility of from to to allow access within the crate. - Re-exported in for use in sibling harness modules, ensuring a shared implementation across provider call sites. --- src/openhuman/agent/harness/session/builder.rs | 2 +- src/openhuman/agent/harness/session/mod.rs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/openhuman/agent/harness/session/builder.rs b/src/openhuman/agent/harness/session/builder.rs index 6a441accfa..96890d5e50 100644 --- a/src/openhuman/agent/harness/session/builder.rs +++ b/src/openhuman/agent/harness/session/builder.rs @@ -39,7 +39,7 @@ use std::sync::Arc; /// list — initial build, post-composio refresh, scope-filter change — /// so the request the provider sees is always name-unique regardless /// of which path produced it. -pub(super) fn dedup_visible_tool_specs(specs: Vec) -> Vec { +pub(crate) fn dedup_visible_tool_specs(specs: Vec) -> Vec { let mut seen: std::collections::HashSet = std::collections::HashSet::new(); let mut deduped: Vec = Vec::with_capacity(specs.len()); let mut dropped: Vec = Vec::new(); diff --git a/src/openhuman/agent/harness/session/mod.rs b/src/openhuman/agent/harness/session/mod.rs index 7e69c3ac73..09b74b6b75 100644 --- a/src/openhuman/agent/harness/session/mod.rs +++ b/src/openhuman/agent/harness/session/mod.rs @@ -33,3 +33,8 @@ pub use migration::{migrate_session_layout_if_needed, MigrationOutcome}; mod tests; pub use types::{Agent, AgentBuilder}; + +// Re-export the duplicate-tool-spec guard for sibling harness modules +// (`tool_loop`, `subagent_runner`) so all three provider call sites +// share one tested implementation. +pub(crate) use builder::dedup_visible_tool_specs; From 33cadaa0bea10c7528aea94793a163cdab35accd Mon Sep 17 00:00:00 2001 From: Shanu Date: Thu, 21 May 2026 18:27:46 +0530 Subject: [PATCH 2/9] fix(tool_loop): deduplicate visible tool specs before sending to provider - Introduced a filtering step to deduplicate tool specifications by name, addressing potential collisions between registry tools and per-turn synthesized extra tools. - Updated the tool specification collection process to ensure only visible tools are included, improving compatibility with providers that enforce uniqueness on tool names. --- src/openhuman/agent/harness/tool_loop.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/openhuman/agent/harness/tool_loop.rs b/src/openhuman/agent/harness/tool_loop.rs index 015ad5d118..e09a877585 100644 --- a/src/openhuman/agent/harness/tool_loop.rs +++ b/src/openhuman/agent/harness/tool_loop.rs @@ -133,12 +133,21 @@ pub(crate) async fn run_tool_call_loop( } }; - let tool_specs: Vec = tools_registry + // Filter to visible tools, then dedup by name before sending to the + // provider. Registry tools may collide with per-turn synthesised + // extra_tools (e.g. an `ArchetypeDelegationTool` whose + // `delegate_name = "research"` shadowing a same-named skill). Some + // providers (Anthropic, OpenHuman cloud after the uniqueness-enforcement + // rollout) 400 on duplicate tool names — see TAURI-RUST-4. + let filtered_specs: Vec = tools_registry .iter() .chain(extra_tools.iter()) .filter(|tool| is_visible(tool.name())) .map(|tool| tool.spec()) .collect(); + let tool_specs = crate::openhuman::agent::harness::session::dedup_visible_tool_specs( + filtered_specs, + ); let use_native_tools = provider.supports_native_tools() && !tool_specs.is_empty(); log::debug!( From 9f0ae346cd57d76a929a961cd01d9850bb7664a3 Mon Sep 17 00:00:00 2001 From: Shanu Date: Thu, 21 May 2026 18:28:43 +0530 Subject: [PATCH 3/9] fix(subagent_runner): log dropped duplicate tool specs during deduplication - Added a logging mechanism to warn when duplicate tool specifications are dropped before making a provider call. - Enhanced the deduplication process to ensure that only unique tool specs are sent, addressing potential issues with providers that enforce name uniqueness. --- .../agent/harness/subagent_runner/ops.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/openhuman/agent/harness/subagent_runner/ops.rs b/src/openhuman/agent/harness/subagent_runner/ops.rs index 7be0d0d72a..e1b30091ff 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops.rs @@ -838,6 +838,22 @@ async fn run_typed_mode( filtered_specs.push(tool.spec()); allowed_names.insert(tool.name().to_string()); } + // Dedup by name: first occurrence wins. Dynamic Composio action tools + // can share a name with an inherited parent-registry spec when the + // agent's AllowedAll scope includes a same-named skill tool. Some + // providers (Anthropic, OpenHuman cloud after the uniqueness-enforcement + // rollout) 400 on duplicate tool names — see TAURI-RUST-4. + let before_dedup = filtered_specs.len(); + let filtered_specs = crate::openhuman::agent::harness::session::dedup_visible_tool_specs( + filtered_specs, + ); + if filtered_specs.len() != before_dedup { + tracing::warn!( + agent_id = %definition.id, + dropped = before_dedup - filtered_specs.len(), + "[subagent_runner:typed] dropped duplicate tool spec(s) before provider call" + ); + } tracing::debug!( agent_id = %definition.id, From 2afcc24dd907e5fbb2b794574af9c74dc0233b74 Mon Sep 17 00:00:00 2001 From: Shanu Date: Thu, 21 May 2026 18:29:32 +0530 Subject: [PATCH 4/9] test(tool_loop): add regression test for deduplication of duplicate tool names - Implemented a new test to ensure that duplicate tool names are correctly deduplicated before being sent to the provider, addressing the issue where providers reject requests with non-unique tool names. - Introduced a to record tool specifications and validate that only unique names are passed during the call. --- .../agent/harness/tool_loop_tests.rs | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/openhuman/agent/harness/tool_loop_tests.rs b/src/openhuman/agent/harness/tool_loop_tests.rs index 684d4e69da..0324472527 100644 --- a/src/openhuman/agent/harness/tool_loop_tests.rs +++ b/src/openhuman/agent/harness/tool_loop_tests.rs @@ -886,3 +886,128 @@ async fn run_tool_call_loop_applies_per_tool_max_result_size_cap() { tool_results.content.len() ); } + +// ── TAURI-RUST-4 regression guard ──────────────────────────────────── +// +// Some providers (Anthropic, OpenHuman cloud after the uniqueness- +// enforcement rollout) reject chat requests whose `tools` list contains +// two specs with the same `name` — HTTP 400 "Tool names must be unique." +// `run_tool_call_loop` chains the persistent `tools_registry` with the +// per-turn synthesised `extra_tools`; if any name collides across the +// two lists, both would have made it to the provider before the fix. +// +// This test wires a capturing provider, builds a colliding tool list +// (one `EchoTool` in the registry + a second `EchoTool` clone in +// `extra_tools`), and asserts the names the provider sees contain +// `"echo"` exactly once. + +/// Provider that records the tool-spec names of every `chat()` request +/// it sees, then returns the next scripted response. +struct CapturingProvider { + /// One entry per `chat()` call — the tool-name list extracted from + /// `ChatRequest.tools`. `None` if `tools` was `None`. + captured: Mutex>>>, + responses: Mutex>>, + native_tools: bool, +} + +#[async_trait] +impl Provider for CapturingProvider { + async fn chat_with_system( + &self, + _system_prompt: Option<&str>, + _message: &str, + _model: &str, + _temperature: f64, + ) -> Result { + Ok("fallback".into()) + } + + async fn chat( + &self, + request: ChatRequest<'_>, + _model: &str, + _temperature: f64, + ) -> Result { + let names = request + .tools + .map(|specs| specs.iter().map(|s| s.name.clone()).collect::>()); + self.captured.lock().push(names); + let mut guard = self.responses.lock(); + guard.remove(0) + } + + fn capabilities(&self) -> ProviderCapabilities { + ProviderCapabilities { + native_tool_calling: self.native_tools, + vision: false, + ..ProviderCapabilities::default() + } + } +} + +#[tokio::test] +async fn run_tool_call_loop_dedups_duplicate_tool_names_before_provider_call() { + // Provider returns a single final text response — no tool calls — + // so the loop terminates after exactly one `chat()` invocation, + // and the captured tool list reflects what the fix is supposed to + // guard against (no duplicate names reaching the wire). + let provider = CapturingProvider { + captured: Mutex::new(Vec::new()), + responses: Mutex::new(vec![Ok(ChatResponse { + text: Some("done".into()), + tool_calls: vec![], + usage: None, + })]), + // Native tool-calling on: only when the provider supports native + // tools does `run_tool_call_loop` populate `ChatRequest.tools`. + native_tools: true, + }; + + // Registry has `EchoTool` (name = "echo"). `extra_tools` adds a + // second tool also named "echo" — the exact collision pattern from + // the bug report (a synthesised delegation tool whose + // `delegate_name` shadows a same-named skill tool). + let registry: Vec> = vec![Box::new(EchoTool)]; + let extra: Vec> = vec![Box::new(EchoTool)]; + + let mut history = vec![ChatMessage::user("hi")]; + let result = run_tool_call_loop( + &provider, + &mut history, + ®istry, + "test-provider", + "model", + 0.0, + true, + None, + "channel", + &crate::openhuman::config::MultimodalConfig::default(), + 2, + None, + None, + &extra, + None, + None, + ) + .await + .expect("loop should succeed with deduplicated tool list"); + assert_eq!(result, "done"); + + let captured = provider.captured.lock(); + assert_eq!( + captured.len(), + 1, + "exactly one chat() call expected for a final-only response" + ); + let names = captured[0] + .as_ref() + .expect("native_tools=true should populate ChatRequest.tools"); + let echo_count = names.iter().filter(|n| n.as_str() == "echo").count(); + assert_eq!( + echo_count, 1, + "duplicate tool names must be dropped before the provider call \ + (TAURI-RUST-4) — got names={:?}", + names + ); +} From bb3721223670d0e83036682556e9e493cecaf602 Mon Sep 17 00:00:00 2001 From: Shanu Date: Thu, 21 May 2026 18:31:19 +0530 Subject: [PATCH 5/9] refactor: apply preetier formatting --- src/openhuman/agent/harness/session/mod.rs | 2 +- src/openhuman/agent/harness/subagent_runner/ops.rs | 5 ++--- src/openhuman/agent/harness/tool_loop.rs | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/openhuman/agent/harness/session/mod.rs b/src/openhuman/agent/harness/session/mod.rs index 09b74b6b75..16d9ec3c55 100644 --- a/src/openhuman/agent/harness/session/mod.rs +++ b/src/openhuman/agent/harness/session/mod.rs @@ -36,5 +36,5 @@ pub use types::{Agent, AgentBuilder}; // Re-export the duplicate-tool-spec guard for sibling harness modules // (`tool_loop`, `subagent_runner`) so all three provider call sites -// share one tested implementation. +// share one tested implementation. pub(crate) use builder::dedup_visible_tool_specs; diff --git a/src/openhuman/agent/harness/subagent_runner/ops.rs b/src/openhuman/agent/harness/subagent_runner/ops.rs index e1b30091ff..fe913e24de 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops.rs @@ -844,9 +844,8 @@ async fn run_typed_mode( // providers (Anthropic, OpenHuman cloud after the uniqueness-enforcement // rollout) 400 on duplicate tool names — see TAURI-RUST-4. let before_dedup = filtered_specs.len(); - let filtered_specs = crate::openhuman::agent::harness::session::dedup_visible_tool_specs( - filtered_specs, - ); + let filtered_specs = + crate::openhuman::agent::harness::session::dedup_visible_tool_specs(filtered_specs); if filtered_specs.len() != before_dedup { tracing::warn!( agent_id = %definition.id, diff --git a/src/openhuman/agent/harness/tool_loop.rs b/src/openhuman/agent/harness/tool_loop.rs index e09a877585..13f7f68af0 100644 --- a/src/openhuman/agent/harness/tool_loop.rs +++ b/src/openhuman/agent/harness/tool_loop.rs @@ -145,9 +145,8 @@ pub(crate) async fn run_tool_call_loop( .filter(|tool| is_visible(tool.name())) .map(|tool| tool.spec()) .collect(); - let tool_specs = crate::openhuman::agent::harness::session::dedup_visible_tool_specs( - filtered_specs, - ); + let tool_specs = + crate::openhuman::agent::harness::session::dedup_visible_tool_specs(filtered_specs); let use_native_tools = provider.supports_native_tools() && !tool_specs.is_empty(); log::debug!( From 79f1ef1b97096e20531517ef765ece95a8b542d4 Mon Sep 17 00:00:00 2001 From: Shanu Date: Thu, 21 May 2026 19:15:13 +0530 Subject: [PATCH 6/9] refactor(subagent_runner): reorder tool specification collection for execution precedence - Adjusted the order of tool specifications to prioritize dynamic tools before parent specs, ensuring consistency between the schema seen by the model and the tools that execute. - Enhanced the deduplication process to maintain the execution order, addressing potential issues with duplicate tool names during provider calls. --- .../agent/harness/subagent_runner/ops.rs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/openhuman/agent/harness/subagent_runner/ops.rs b/src/openhuman/agent/harness/subagent_runner/ops.rs index fe913e24de..545470b4b0 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops.rs @@ -824,25 +824,35 @@ async fn run_typed_mode( None }; - let mut filtered_specs: Vec = allowed_indices - .iter() - .map(|&i| parent.all_tool_specs[i].clone()) - .collect(); + // Build provider-visible tool schemas in EXECUTION-PRECEDENCE order: + // `dynamic_tools` (extra_tools at runtime) before parent specs, because + // the inner loop's name lookup (see end of this fn) resolves + // `extra_tools` first and only falls back to `parent_tools`. Aligning + // the dedup order with the runtime lookup order guarantees the schema + // the model sees and the tool that actually executes describe the same + // behaviour. (CodeRabbit review on PR #2446.) + let mut filtered_specs: Vec = dynamic_tools.iter().map(|t| t.spec()).collect(); + filtered_specs.extend( + allowed_indices + .iter() + .map(|&i| parent.all_tool_specs[i].clone()), + ); let mut allowed_names: HashSet = allowed_indices .iter() .map(|&i| parent.all_tools[i].name().to_string()) .collect(); - // Append dynamic tool specs / names so they're discoverable by the - // provider (native tool-calling) and by the inner loop's allowlist. + // Dynamic tool names must also be in the allowlist so the inner loop + // accepts model tool_calls that reference them. for tool in &dynamic_tools { - filtered_specs.push(tool.spec()); allowed_names.insert(tool.name().to_string()); } // Dedup by name: first occurrence wins. Dynamic Composio action tools // can share a name with an inherited parent-registry spec when the // agent's AllowedAll scope includes a same-named skill tool. Some // providers (Anthropic, OpenHuman cloud after the uniqueness-enforcement - // rollout) 400 on duplicate tool names — see TAURI-RUST-4. + // rollout) 400 on duplicate tool names — see TAURI-RUST-4. Because + // `filtered_specs` is in execution order (dynamic first), the kept + // schema matches what the runtime will actually dispatch. let before_dedup = filtered_specs.len(); let filtered_specs = crate::openhuman::agent::harness::session::dedup_visible_tool_specs(filtered_specs); From e522c74b2f20567751443e2d08ecf5310360e434 Mon Sep 17 00:00:00 2001 From: Shanu Date: Fri, 22 May 2026 11:49:42 +0530 Subject: [PATCH 7/9] refactor(subagent_runner): remove logging for dropped duplicate tool specs --- src/openhuman/agent/harness/subagent_runner/ops.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/openhuman/agent/harness/subagent_runner/ops.rs b/src/openhuman/agent/harness/subagent_runner/ops.rs index 545470b4b0..defd75260f 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops.rs @@ -853,16 +853,8 @@ async fn run_typed_mode( // rollout) 400 on duplicate tool names — see TAURI-RUST-4. Because // `filtered_specs` is in execution order (dynamic first), the kept // schema matches what the runtime will actually dispatch. - let before_dedup = filtered_specs.len(); let filtered_specs = crate::openhuman::agent::harness::session::dedup_visible_tool_specs(filtered_specs); - if filtered_specs.len() != before_dedup { - tracing::warn!( - agent_id = %definition.id, - dropped = before_dedup - filtered_specs.len(), - "[subagent_runner:typed] dropped duplicate tool spec(s) before provider call" - ); - } tracing::debug!( agent_id = %definition.id, From 0e2e0e4e5aae1bfb100b5ded950cbfe1730f7daf Mon Sep 17 00:00:00 2001 From: Shanu Date: Fri, 22 May 2026 12:39:55 +0530 Subject: [PATCH 8/9] feat(i18n): add German translations for subconscious provider and MCP server settings --- app/src/lib/i18n/chunks/de-3.ts | 2 ++ app/src/lib/i18n/chunks/de-5.ts | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/src/lib/i18n/chunks/de-3.ts b/app/src/lib/i18n/chunks/de-3.ts index 8cbb4e8ae7..1ae11dd905 100644 --- a/app/src/lib/i18n/chunks/de-3.ts +++ b/app/src/lib/i18n/chunks/de-3.ts @@ -384,6 +384,8 @@ const de3: TranslationMap = { 'channels.telegram.savedRestartRequired': 'Kanal gespeichert. Starte die App neu, um sie zu aktivieren.', 'channels.web.alwaysAvailable': 'Immer verfügbar', + 'subconscious.providerUnavailableTitle': 'Unterbewusstsein ist pausiert', + 'subconscious.providerSettings': 'KI-Einstellungen', }; export default de3; diff --git a/app/src/lib/i18n/chunks/de-5.ts b/app/src/lib/i18n/chunks/de-5.ts index c698c292fd..67a94494d5 100644 --- a/app/src/lib/i18n/chunks/de-5.ts +++ b/app/src/lib/i18n/chunks/de-5.ts @@ -501,6 +501,28 @@ const de5: TranslationMap = { 'settings.mascot.colorYellow': 'Gelb', 'settings.mascot.libraryUnavailable': 'OpenHuman Bibliothek nicht verfügbar', 'settings.mascot.title': 'OpenHuman', + 'settings.developerMenu.mcpServer.title': 'MCP-Server', + 'settings.developerMenu.mcpServer.desc': + 'Konfiguriere externe MCP-Clients für die Verbindung mit OpenHuman', + 'settings.mcpServer.title': 'MCP-Server', + 'settings.mcpServer.toolsSectionTitle': 'Verfügbare Tools', + 'settings.mcpServer.toolsSectionDesc': + 'Tools, die externe MCP-Clients über diesen Server aufrufen können.', + 'settings.mcpServer.configSectionTitle': 'Client-Konfiguration', + 'settings.mcpServer.configSectionDesc': + 'Füge diesen Eintrag in die Konfigurationsdatei deines MCP-Clients ein.', + 'settings.mcpServer.copySnippet': 'In Zwischenablage kopieren', + 'settings.mcpServer.copied': 'Kopiert!', + 'settings.mcpServer.openConfigFile': 'Konfigurationsdatei öffnen', + 'settings.mcpServer.binaryPathNotFound': + 'Pfad zur OpenHuman-Binärdatei konnte nicht gefunden werden.', + 'settings.mcpServer.openConfigError': 'Konfigurationsdatei konnte nicht geöffnet werden', + 'settings.mcpServer.clientClaudeDesktop': 'Claude Desktop', + 'settings.mcpServer.clientCursor': 'Cursor', + 'settings.mcpServer.clientCodex': 'Codex', + 'settings.mcpServer.clientZed': 'Zed', + 'settings.mcpServer.configFilePath': 'Konfigurationsdatei', + 'settings.mcpServer.clientSelectorAriaLabel': 'MCP-Client-Auswahl', }; export default de5; From 9b15e12d3c8cd465a2f40f799271c55efa25465b Mon Sep 17 00:00:00 2001 From: Shanu Date: Fri, 22 May 2026 14:53:22 +0530 Subject: [PATCH 9/9] fix(i18n): remove unused German translations for subconscious provider --- app/src/lib/i18n/chunks/de-3.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/lib/i18n/chunks/de-3.ts b/app/src/lib/i18n/chunks/de-3.ts index bcd2de4262..b80b416417 100644 --- a/app/src/lib/i18n/chunks/de-3.ts +++ b/app/src/lib/i18n/chunks/de-3.ts @@ -386,8 +386,6 @@ const de3: TranslationMap = { 'channels.telegram.savedRestartRequired': 'Kanal gespeichert. Starte die App neu, um sie zu aktivieren.', 'channels.web.alwaysAvailable': 'Immer verfügbar', - 'subconscious.providerUnavailableTitle': 'Unterbewusstsein ist pausiert', - 'subconscious.providerSettings': 'KI-Einstellungen', }; export default de3;