diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index bc48d0e30..27522adc1 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -309,10 +309,15 @@ pub(crate) fn recover_mangled_tool_name<'a>( let owner_mangled = owner.map(|o| format!("{o}.{name}")); let owner_prefixed = owner.map(|o| format!("{o}__{name}")); + // Models prompted with bare tool names ("write_file") drop the extension + // prefix entirely; recover only when exactly one extension owns that tool. + let bare_tool = name.split_once("__").map(|(_, tool)| tool); + let matches = stripped == name || separator_mangled.as_deref() == Some(stripped) || owner_mangled.as_deref() == Some(stripped) - || owner_prefixed.as_deref() == Some(stripped); + || owner_prefixed.as_deref() == Some(stripped) + || bare_tool == Some(stripped); if name == emitted || !matches { continue; } @@ -3805,6 +3810,36 @@ mod tests { ); } + #[test] + fn test_recover_mangled_tool_name_bare_tool_without_extension_prefix() { + let tools = [ + ("sandbox-fs__write_file", None), + ("sandbox-fs__read_file", None), + ("developer__shell", None), + ]; + assert_eq!( + recover_mangled_tool_name("write_file", tools.iter().copied()).as_deref(), + Some("sandbox-fs__write_file") + ); + assert_eq!( + recover_mangled_tool_name("functions.read_file", tools.iter().copied()).as_deref(), + Some("sandbox-fs__read_file") + ); + assert_eq!( + recover_mangled_tool_name("edit_file", tools.iter().copied()), + None + ); + + let ambiguous = [ + ("sandbox-fs__write_file", None), + ("other__write_file", None), + ]; + assert_eq!( + recover_mangled_tool_name("write_file", ambiguous.iter().copied()), + None + ); + } + #[test] fn test_recover_mangled_tool_name_non_extension_manager_tools() { // recipe__final_output and platform__manage_schedule are appended by