From 50f4f6be168a5ba2a2ef8ec65283ef72bbd93d97 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Tue, 28 Apr 2026 15:27:07 -0700 Subject: [PATCH 1/8] Move tool_suggest into a namespace --- codex-rs/core/src/tools/router.rs | 21 +++-- codex-rs/core/src/tools/router_tests.rs | 49 +++++++++++ codex-rs/core/tests/suite/tool_suggest.rs | 30 +++++-- codex-rs/tools/src/lib.rs | 1 + codex-rs/tools/src/tool_discovery.rs | 39 +++++---- codex-rs/tools/src/tool_discovery_tests.rs | 82 ++++++++++--------- codex-rs/tools/src/tool_registry_plan.rs | 6 +- .../tools/src/tool_registry_plan_tests.rs | 20 ++--- 8 files changed, 169 insertions(+), 79 deletions(-) diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index aeba3b0556ee..ac400e34c530 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -144,17 +144,24 @@ impl ToolRouter { } fn configured_tool_supports_parallel(&self, tool_name: &ToolName) -> bool { - if tool_name.namespace.is_some() { - return false; - } - self.specs .iter() .filter(|config| config.supports_parallel_tool_calls) .any(|config| match &config.spec { - ToolSpec::Function(tool) => tool.name == tool_name.name.as_str(), - ToolSpec::Freeform(tool) => tool.name == tool_name.name.as_str(), - ToolSpec::Namespace(_) + ToolSpec::Function(tool) if tool_name.namespace.is_none() => { + tool.name == tool_name.name.as_str() + } + ToolSpec::Freeform(tool) if tool_name.namespace.is_none() => { + tool.name == tool_name.name.as_str() + } + ToolSpec::Namespace(namespace) => namespace.tools.iter().any(|tool| match tool { + ResponsesApiNamespaceTool::Function(tool) => { + tool_name.namespace.as_deref() == Some(namespace.name.as_str()) + && tool.name == tool_name.name + } + }), + ToolSpec::Function(_) + | ToolSpec::Freeform(_) | ToolSpec::ToolSearch { .. } | ToolSpec::LocalShell {} | ToolSpec::ImageGeneration { .. } diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs index ac859d9aa61c..f1b250d70fe2 100644 --- a/codex-rs/core/src/tools/router_tests.rs +++ b/codex-rs/core/src/tools/router_tests.rs @@ -1,11 +1,17 @@ +use std::collections::BTreeMap; use std::collections::HashSet; use std::sync::Arc; use crate::session::tests::make_session_and_context; use crate::tools::context::ToolPayload; +use crate::tools::registry::ToolRegistry; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::ResponseItem; +use codex_tools::ConfiguredToolSpec; +use codex_tools::JsonSchema; +use codex_tools::ResponsesApiNamespace; use codex_tools::ResponsesApiNamespaceTool; +use codex_tools::ResponsesApiTool; use codex_tools::ToolName; use codex_tools::ToolSpec; use pretty_assertions::assert_eq; @@ -139,6 +145,49 @@ async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()> Ok(()) } +#[test] +fn tool_suggest_namespaced_parallel_support_uses_child_tool_name() { + let router = ToolRouter { + registry: ToolRegistry::empty_for_test(), + specs: vec![ConfiguredToolSpec::new( + ToolSpec::Namespace(ResponsesApiNamespace { + name: "tool_suggest".to_string(), + description: "Suggest tools".to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: "tool_suggest".to_string(), + description: "Suggest a missing tool".to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + BTreeMap::new(), + /*required*/ None, + /*additional_properties*/ None, + ), + output_schema: None, + })], + }), + /*supports_parallel_tool_calls*/ true, + )], + model_visible_specs: Vec::new(), + parallel_mcp_server_names: HashSet::new(), + }; + + assert!(router.tool_supports_parallel(&ToolCall { + tool_name: ToolName::namespaced("tool_suggest", "tool_suggest"), + call_id: "call-namespaced-parallel".to_string(), + payload: ToolPayload::Function { + arguments: "{}".to_string(), + }, + })); + assert!(!router.tool_supports_parallel(&ToolCall { + tool_name: ToolName::plain("tool_suggest"), + call_id: "call-plain-not-parallel".to_string(), + payload: ToolPayload::Function { + arguments: "{}".to_string(), + }, + })); +} + #[tokio::test] async fn model_visible_specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> { let (_, turn) = make_session_and_context().await; diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index 30da83e9ebf0..f3b563d9eb68 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -47,13 +47,31 @@ fn function_tool_description(body: &Value, name: &str) -> Option { .and_then(Value::as_array) .and_then(|tools| { tools.iter().find_map(|tool| { - if tool.get("name").and_then(Value::as_str) == Some(name) { - tool.get("description") - .and_then(Value::as_str) - .map(str::to_string) - } else { - None + if tool.get("name").and_then(Value::as_str) != Some(name) { + return None; } + + if tool.get("type").and_then(Value::as_str) == Some("namespace") { + return tool.get("tools").and_then(Value::as_array).and_then( + |namespace_tools| { + namespace_tools.iter().find_map(|namespace_tool| { + if namespace_tool.get("name").and_then(Value::as_str) == Some(name) + { + namespace_tool + .get("description") + .and_then(Value::as_str) + .map(str::to_string) + } else { + None + } + }) + }, + ); + } + + tool.get("description") + .and_then(Value::as_str) + .map(str::to_string) }) }) } diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index 516bda6859ab..94c3f70b5eb1 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -111,6 +111,7 @@ pub use tool_discovery::DiscoverableToolAction; pub use tool_discovery::DiscoverableToolType; pub use tool_discovery::TOOL_SEARCH_DEFAULT_LIMIT; pub use tool_discovery::TOOL_SEARCH_TOOL_NAME; +pub use tool_discovery::TOOL_SUGGEST_NAMESPACE_NAME; pub use tool_discovery::TOOL_SUGGEST_TOOL_NAME; pub use tool_discovery::ToolSearchResultSource; pub use tool_discovery::ToolSearchSource; diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index ddcd2f1ce351..2b1bcfc284eb 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -15,6 +15,7 @@ use std::collections::BTreeMap; const TUI_CLIENT_NAME: &str = "codex-tui"; pub const TOOL_SEARCH_TOOL_NAME: &str = "tool_search"; pub const TOOL_SEARCH_DEFAULT_LIMIT: usize = 8; +pub const TOOL_SUGGEST_NAMESPACE_NAME: &str = "tool_suggest"; pub const TOOL_SUGGEST_TOOL_NAME: &str = "tool_suggest"; #[derive(Clone, Debug, PartialEq, Eq)] @@ -311,22 +312,28 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool "# Tool suggestion discovery\n\nSuggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.\n\nUse this ONLY when:\n- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `{TOOL_SEARCH_TOOL_NAME}` (if available) and other means.\n- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.\n- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.\n\nTool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.\n\nDiscoverable tools:\n{discoverable_tools}\n\nWorkflow:\n\n1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.\n2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the discoverable tools list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it." ); - ToolSpec::Function(ResponsesApiTool { - name: TOOL_SUGGEST_TOOL_NAME.to_string(), - description, - strict: false, - defer_loading: None, - parameters: JsonSchema::object( - properties, - Some(vec![ - "tool_type".to_string(), - "action_type".to_string(), - "tool_id".to_string(), - "suggest_reason".to_string(), - ]), - Some(false.into()), - ), - output_schema: None, + ToolSpec::Namespace(ResponsesApiNamespace { + name: TOOL_SUGGEST_NAMESPACE_NAME.to_string(), + description: + "Suggest missing connectors or plugins when no available tool matches the current request." + .to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: TOOL_SUGGEST_TOOL_NAME.to_string(), + description, + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + properties, + Some(vec![ + "tool_type".to_string(), + "action_type".to_string(), + "tool_id".to_string(), + "suggest_reason".to_string(), + ]), + Some(false.into()), + ), + output_schema: None, + })], }) } diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index 9afd32231e1d..08f0ebb632b7 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -71,47 +71,53 @@ fn create_tool_suggest_tool_uses_plugin_summary_fallback() { app_connector_ids: vec!["github-app".to_string()], }, ]), - ToolSpec::Function(ResponsesApiTool { + ToolSpec::Namespace(ResponsesApiNamespace { name: "tool_suggest".to_string(), - description: "# Tool suggestion discovery\n\nSuggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.\n\nUse this ONLY when:\n- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means.\n- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.\n- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.\n\nTool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.\n\nDiscoverable tools:\n- GitHub (id: `github`, type: plugin, action: install): skills; MCP servers: github-mcp; app connectors: github-app\n- Slack (id: `slack@openai-curated`, type: connector, action: install): No description provided.\n\nWorkflow:\n\n1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.\n2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.\n3. If one tool clearly fits, call `tool_suggest` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the discoverable tools list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.".to_string(), - strict: false, - defer_loading: None, - parameters: JsonSchema::object(BTreeMap::from([ - ( + description: + "Suggest missing connectors or plugins when no available tool matches the current request." + .to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: "tool_suggest".to_string(), + description: "# Tool suggestion discovery\n\nSuggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.\n\nUse this ONLY when:\n- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means.\n- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.\n- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.\n\nTool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.\n\nDiscoverable tools:\n- GitHub (id: `github`, type: plugin, action: install): skills; MCP servers: github-mcp; app connectors: github-app\n- Slack (id: `slack@openai-curated`, type: connector, action: install): No description provided.\n\nWorkflow:\n\n1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.\n2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.\n3. If one tool clearly fits, call `tool_suggest` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the discoverable tools list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.".to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object(BTreeMap::from([ + ( + "action_type".to_string(), + JsonSchema::string(Some( + "Suggested action for the tool. Use \"install\" or \"enable\"." + .to_string(), + ),), + ), + ( + "suggest_reason".to_string(), + JsonSchema::string(Some( + "Concise one-line user-facing reason why this tool can help with the current request." + .to_string(), + ),), + ), + ( + "tool_id".to_string(), + JsonSchema::string(Some( + "Connector or plugin id to suggest. Must be one of: slack@openai-curated, github." + .to_string(), + ),), + ), + ( + "tool_type".to_string(), + JsonSchema::string(Some( + "Type of discoverable tool to suggest. Use \"connector\" or \"plugin\"." + .to_string(), + ),), + ), + ]), Some(vec![ + "tool_type".to_string(), "action_type".to_string(), - JsonSchema::string(Some( - "Suggested action for the tool. Use \"install\" or \"enable\"." - .to_string(), - ),), - ), - ( - "suggest_reason".to_string(), - JsonSchema::string(Some( - "Concise one-line user-facing reason why this tool can help with the current request." - .to_string(), - ),), - ), - ( "tool_id".to_string(), - JsonSchema::string(Some( - "Connector or plugin id to suggest. Must be one of: slack@openai-curated, github." - .to_string(), - ),), - ), - ( - "tool_type".to_string(), - JsonSchema::string(Some( - "Type of discoverable tool to suggest. Use \"connector\" or \"plugin\"." - .to_string(), - ),), - ), - ]), Some(vec![ - "tool_type".to_string(), - "action_type".to_string(), - "tool_id".to_string(), - "suggest_reason".to_string(), - ]), Some(false.into())), - output_schema: None, + "suggest_reason".to_string(), + ]), Some(false.into())), + output_schema: None, + })], }) ); } diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index f7c10dc2c4e9..d53bf148f3d4 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -6,6 +6,7 @@ use crate::ShellToolOptions; use crate::SpawnAgentToolOptions; use crate::TOOL_SEARCH_DEFAULT_LIMIT; use crate::TOOL_SEARCH_TOOL_NAME; +use crate::TOOL_SUGGEST_NAMESPACE_NAME; use crate::TOOL_SUGGEST_TOOL_NAME; use crate::ToolHandlerKind; use crate::ToolName; @@ -312,7 +313,10 @@ pub fn build_tool_registry_plan( /*supports_parallel_tool_calls*/ true, /*code_mode_enabled*/ false, ); - plan.register_handler(TOOL_SUGGEST_TOOL_NAME, ToolHandlerKind::ToolSuggest); + plan.register_handler( + ToolName::namespaced(TOOL_SUGGEST_NAMESPACE_NAME, TOOL_SUGGEST_TOOL_NAME), + ToolHandlerKind::ToolSuggest, + ); } if config.has_environment diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 8f64349c2f78..dab3985cdab6 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -1558,10 +1558,8 @@ fn tool_suggest_can_be_registered_without_search_tool() { assert_contains_tool_names(&tools, &[TOOL_SUGGEST_TOOL_NAME]); assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); - let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME); - let ToolSpec::Function(ResponsesApiTool { description, .. }) = &tool_suggest.spec else { - panic!("expected function tool"); - }; + let ResponsesApiTool { description, .. } = + find_namespace_function_tool(&tools, TOOL_SUGGEST_NAMESPACE_NAME, TOOL_SUGGEST_TOOL_NAME); assert!(description.contains( "Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin" )); @@ -1611,23 +1609,23 @@ fn tool_suggest_description_lists_discoverable_tools() { })), ]; - let (tools, _) = build_specs_with_discoverable_tools( + let (tools, handlers) = build_specs_with_discoverable_tools( &tools_config, /*mcp_tools*/ None, /*deferred_mcp_tools*/ None, Some(discoverable_tools), &[], ); + assert!(handlers.contains(&ToolHandlerSpec { + name: ToolName::namespaced(TOOL_SUGGEST_NAMESPACE_NAME, TOOL_SUGGEST_TOOL_NAME), + kind: ToolHandlerKind::ToolSuggest, + })); - let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME); - let ToolSpec::Function(ResponsesApiTool { + let ResponsesApiTool { description, parameters, .. - }) = &tool_suggest.spec - else { - panic!("expected function tool"); - }; + } = find_namespace_function_tool(&tools, TOOL_SUGGEST_NAMESPACE_NAME, TOOL_SUGGEST_TOOL_NAME); assert!(description.contains( "Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin" )); From 475576534e23c5d1bef93d4adfc909df6dfffe17 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Tue, 28 Apr 2026 17:00:49 -0700 Subject: [PATCH 2/8] Disable parallel tool calls for tool_suggest --- codex-rs/core/src/tools/router.rs | 21 ++-- codex-rs/core/src/tools/router_tests.rs | 49 -------- .../search_tool/tool_suggest_description.md | 22 ++-- codex-rs/core/tests/suite/tool_suggest.rs | 42 ++----- codex-rs/tools/src/lib.rs | 1 - codex-rs/tools/src/tool_discovery.rs | 47 ++++---- codex-rs/tools/src/tool_discovery_tests.rs | 106 ++++++++++-------- codex-rs/tools/src/tool_registry_plan.rs | 8 +- .../tools/src/tool_registry_plan_tests.rs | 51 ++++++--- 9 files changed, 152 insertions(+), 195 deletions(-) diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index ac400e34c530..aeba3b0556ee 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -144,24 +144,17 @@ impl ToolRouter { } fn configured_tool_supports_parallel(&self, tool_name: &ToolName) -> bool { + if tool_name.namespace.is_some() { + return false; + } + self.specs .iter() .filter(|config| config.supports_parallel_tool_calls) .any(|config| match &config.spec { - ToolSpec::Function(tool) if tool_name.namespace.is_none() => { - tool.name == tool_name.name.as_str() - } - ToolSpec::Freeform(tool) if tool_name.namespace.is_none() => { - tool.name == tool_name.name.as_str() - } - ToolSpec::Namespace(namespace) => namespace.tools.iter().any(|tool| match tool { - ResponsesApiNamespaceTool::Function(tool) => { - tool_name.namespace.as_deref() == Some(namespace.name.as_str()) - && tool.name == tool_name.name - } - }), - ToolSpec::Function(_) - | ToolSpec::Freeform(_) + ToolSpec::Function(tool) => tool.name == tool_name.name.as_str(), + ToolSpec::Freeform(tool) => tool.name == tool_name.name.as_str(), + ToolSpec::Namespace(_) | ToolSpec::ToolSearch { .. } | ToolSpec::LocalShell {} | ToolSpec::ImageGeneration { .. } diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs index f1b250d70fe2..ac859d9aa61c 100644 --- a/codex-rs/core/src/tools/router_tests.rs +++ b/codex-rs/core/src/tools/router_tests.rs @@ -1,17 +1,11 @@ -use std::collections::BTreeMap; use std::collections::HashSet; use std::sync::Arc; use crate::session::tests::make_session_and_context; use crate::tools::context::ToolPayload; -use crate::tools::registry::ToolRegistry; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::ResponseItem; -use codex_tools::ConfiguredToolSpec; -use codex_tools::JsonSchema; -use codex_tools::ResponsesApiNamespace; use codex_tools::ResponsesApiNamespaceTool; -use codex_tools::ResponsesApiTool; use codex_tools::ToolName; use codex_tools::ToolSpec; use pretty_assertions::assert_eq; @@ -145,49 +139,6 @@ async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()> Ok(()) } -#[test] -fn tool_suggest_namespaced_parallel_support_uses_child_tool_name() { - let router = ToolRouter { - registry: ToolRegistry::empty_for_test(), - specs: vec![ConfiguredToolSpec::new( - ToolSpec::Namespace(ResponsesApiNamespace { - name: "tool_suggest".to_string(), - description: "Suggest tools".to_string(), - tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { - name: "tool_suggest".to_string(), - description: "Suggest a missing tool".to_string(), - strict: false, - defer_loading: None, - parameters: JsonSchema::object( - BTreeMap::new(), - /*required*/ None, - /*additional_properties*/ None, - ), - output_schema: None, - })], - }), - /*supports_parallel_tool_calls*/ true, - )], - model_visible_specs: Vec::new(), - parallel_mcp_server_names: HashSet::new(), - }; - - assert!(router.tool_supports_parallel(&ToolCall { - tool_name: ToolName::namespaced("tool_suggest", "tool_suggest"), - call_id: "call-namespaced-parallel".to_string(), - payload: ToolPayload::Function { - arguments: "{}".to_string(), - }, - })); - assert!(!router.tool_supports_parallel(&ToolCall { - tool_name: ToolName::plain("tool_suggest"), - call_id: "call-plain-not-parallel".to_string(), - payload: ToolPayload::Function { - arguments: "{}".to_string(), - }, - })); -} - #[tokio::test] async fn model_visible_specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> { let (_, turn) = make_session_and_context().await; diff --git a/codex-rs/core/templates/search_tool/tool_suggest_description.md b/codex-rs/core/templates/search_tool/tool_suggest_description.md index ad0a50fcc1b0..97f709a30170 100644 --- a/codex-rs/core/templates/search_tool/tool_suggest_description.md +++ b/codex-rs/core/templates/search_tool/tool_suggest_description.md @@ -1,26 +1,26 @@ # Tool suggestion discovery -Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list. +Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled. -Use this ONLY when: -- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means. -- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely. -- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant. +Use this ONLY when all of the following are true: +- The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list. +- The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant. +- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list. -Tool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list. +Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool. -Discoverable tools: +Known plugins/connectors available to install or enable: {{discoverable_tools}} Workflow: -1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent. -2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard. +1. Check the current context and active `tools` list first. If `tool_search` is available and a targeted lookup is appropriate, use it to check for the requested tool; do not run broad or speculative searches just to satisfy this condition. Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery. +2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits. 3. If one tool clearly fits, call `tool_suggest` with: - `tool_type`: `connector` or `plugin` - `action_type`: `install` or `enable` - - `tool_id`: exact id from the discoverable tools list above + - `tool_id`: exact id from the known plugin/connector list above - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request 4. After the suggestion flow completes: - if the user finished the install or enable flow, continue by searching again or using the newly available tool - - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks you to. + - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it. diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index f3b563d9eb68..e2ea34ab8788 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -47,31 +47,13 @@ fn function_tool_description(body: &Value, name: &str) -> Option { .and_then(Value::as_array) .and_then(|tools| { tools.iter().find_map(|tool| { - if tool.get("name").and_then(Value::as_str) != Some(name) { - return None; - } - - if tool.get("type").and_then(Value::as_str) == Some("namespace") { - return tool.get("tools").and_then(Value::as_array).and_then( - |namespace_tools| { - namespace_tools.iter().find_map(|namespace_tool| { - if namespace_tool.get("name").and_then(Value::as_str) == Some(name) - { - namespace_tool - .get("description") - .and_then(Value::as_str) - .map(str::to_string) - } else { - None - } - }) - }, - ); + if tool.get("name").and_then(Value::as_str) == Some(name) { + tool.get("description") + .and_then(Value::as_str) + .map(str::to_string) + } else { + None } - - tool.get("description") - .and_then(Value::as_str) - .map(str::to_string) }) }) } @@ -149,12 +131,12 @@ async fn tool_suggest_is_available_without_search_tool_after_discovery_attempts( let description = function_tool_description(&body, TOOL_SUGGEST_TOOL_NAME).expect("description"); - assert!( - description.contains( - "You've already tried to find a matching available tool for the user's request" - ) - ); - assert!(description.contains("This includes `tool_search` (if available) and other means.")); + assert!(description.contains( + "Use this tool only to ask the user to install or enable one known plugin or connector from the list below" + )); + assert!(description.contains( + "The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant." + )); assert!(!description.contains("tool_search fails to find a good match")); Ok(()) diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index 94c3f70b5eb1..516bda6859ab 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -111,7 +111,6 @@ pub use tool_discovery::DiscoverableToolAction; pub use tool_discovery::DiscoverableToolType; pub use tool_discovery::TOOL_SEARCH_DEFAULT_LIMIT; pub use tool_discovery::TOOL_SEARCH_TOOL_NAME; -pub use tool_discovery::TOOL_SUGGEST_NAMESPACE_NAME; pub use tool_discovery::TOOL_SUGGEST_TOOL_NAME; pub use tool_discovery::ToolSearchResultSource; pub use tool_discovery::ToolSearchSource; diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index 2b1bcfc284eb..148ca628a7ae 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -15,7 +15,6 @@ use std::collections::BTreeMap; const TUI_CLIENT_NAME: &str = "codex-tui"; pub const TOOL_SEARCH_TOOL_NAME: &str = "tool_search"; pub const TOOL_SEARCH_DEFAULT_LIMIT: usize = 8; -pub const TOOL_SUGGEST_NAMESPACE_NAME: &str = "tool_suggest"; pub const TOOL_SUGGEST_TOOL_NAME: &str = "tool_suggest"; #[derive(Clone, Debug, PartialEq, Eq)] @@ -278,6 +277,10 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool .map(|tool| tool.id.as_str()) .collect::>() .join(", "); + let discoverable_tool_id_values = discoverable_tools + .iter() + .map(|tool| serde_json::Value::String(tool.id.clone())) + .collect(); let properties = BTreeMap::from([ ( "tool_type".to_string(), @@ -294,7 +297,7 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool ), ( "tool_id".to_string(), - JsonSchema::string(Some(format!( + JsonSchema::string_enum(discoverable_tool_id_values, Some(format!( "Connector or plugin id to suggest. Must be one of: {discoverable_tool_ids}." ))), ), @@ -309,31 +312,25 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool let discoverable_tools = format_discoverable_tools(discoverable_tools); let description = format!( - "# Tool suggestion discovery\n\nSuggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.\n\nUse this ONLY when:\n- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `{TOOL_SEARCH_TOOL_NAME}` (if available) and other means.\n- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.\n- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.\n\nTool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.\n\nDiscoverable tools:\n{discoverable_tools}\n\nWorkflow:\n\n1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.\n2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the discoverable tools list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it." + "# Tool suggestion discovery\n\nUse this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\nUse this ONLY when all of the following are true:\n- The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list.\n- The tool is not found or made callable through a targeted `{TOOL_SEARCH_TOOL_NAME}` result when `{TOOL_SEARCH_TOOL_NAME}` is available and relevant.\n- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\nDo not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\nKnown plugins/connectors available to install or enable:\n{discoverable_tools}\n\nWorkflow:\n\n1. Check the current context and active `tools` list first. If `{TOOL_SEARCH_TOOL_NAME}` is available and a targeted lookup is appropriate, use it to check for the requested tool; do not run broad or speculative searches just to satisfy this condition. Do not use tool suggestion if the needed tool is already available, found through `{TOOL_SEARCH_TOOL_NAME}`, or callable after discovery.\n2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the known plugin/connector list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it." ); - ToolSpec::Namespace(ResponsesApiNamespace { - name: TOOL_SUGGEST_NAMESPACE_NAME.to_string(), - description: - "Suggest missing connectors or plugins when no available tool matches the current request." - .to_string(), - tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { - name: TOOL_SUGGEST_TOOL_NAME.to_string(), - description, - strict: false, - defer_loading: None, - parameters: JsonSchema::object( - properties, - Some(vec![ - "tool_type".to_string(), - "action_type".to_string(), - "tool_id".to_string(), - "suggest_reason".to_string(), - ]), - Some(false.into()), - ), - output_schema: None, - })], + ToolSpec::Function(ResponsesApiTool { + name: TOOL_SUGGEST_TOOL_NAME.to_string(), + description, + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + properties, + Some(vec![ + "tool_type".to_string(), + "action_type".to_string(), + "tool_id".to_string(), + "suggest_reason".to_string(), + ]), + Some(false.into()), + ), + output_schema: None, }) } diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index 08f0ebb632b7..cb37bad4b33d 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -50,6 +50,30 @@ fn create_tool_search_tool_deduplicates_and_renders_enabled_sources() { #[test] fn create_tool_suggest_tool_uses_plugin_summary_fallback() { + let expected_description = concat!( + "# Tool suggestion discovery\n\n", + "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\n", + "Use this ONLY when all of the following are true:\n", + "- The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list.\n", + "- The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant.\n", + "- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\n", + "Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\n", + "Known plugins/connectors available to install or enable:\n", + "- GitHub (id: `github`, type: plugin, action: install): skills; MCP servers: github-mcp; app connectors: github-app\n", + "- Slack (id: `slack@openai-curated`, type: connector, action: install): No description provided.\n\n", + "Workflow:\n\n", + "1. Check the current context and active `tools` list first. If `tool_search` is available and a targeted lookup is appropriate, use it to check for the requested tool; do not run broad or speculative searches just to satisfy this condition. Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery.\n", + "2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n", + "3. If one tool clearly fits, call `tool_suggest` with:\n", + " - `tool_type`: `connector` or `plugin`\n", + " - `action_type`: `install` or `enable`\n", + " - `tool_id`: exact id from the known plugin/connector list above\n", + " - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n", + "4. After the suggestion flow completes:\n", + " - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n", + " - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.", + ); + assert_eq!( create_tool_suggest_tool(&[ ToolSuggestEntry { @@ -71,53 +95,47 @@ fn create_tool_suggest_tool_uses_plugin_summary_fallback() { app_connector_ids: vec!["github-app".to_string()], }, ]), - ToolSpec::Namespace(ResponsesApiNamespace { + ToolSpec::Function(ResponsesApiTool { name: "tool_suggest".to_string(), - description: - "Suggest missing connectors or plugins when no available tool matches the current request." - .to_string(), - tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { - name: "tool_suggest".to_string(), - description: "# Tool suggestion discovery\n\nSuggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.\n\nUse this ONLY when:\n- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means.\n- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.\n- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.\n\nTool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.\n\nDiscoverable tools:\n- GitHub (id: `github`, type: plugin, action: install): skills; MCP servers: github-mcp; app connectors: github-app\n- Slack (id: `slack@openai-curated`, type: connector, action: install): No description provided.\n\nWorkflow:\n\n1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.\n2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.\n3. If one tool clearly fits, call `tool_suggest` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the discoverable tools list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.".to_string(), - strict: false, - defer_loading: None, - parameters: JsonSchema::object(BTreeMap::from([ - ( - "action_type".to_string(), - JsonSchema::string(Some( - "Suggested action for the tool. Use \"install\" or \"enable\"." - .to_string(), - ),), - ), - ( - "suggest_reason".to_string(), - JsonSchema::string(Some( - "Concise one-line user-facing reason why this tool can help with the current request." - .to_string(), - ),), - ), - ( - "tool_id".to_string(), - JsonSchema::string(Some( - "Connector or plugin id to suggest. Must be one of: slack@openai-curated, github." - .to_string(), - ),), - ), - ( - "tool_type".to_string(), - JsonSchema::string(Some( - "Type of discoverable tool to suggest. Use \"connector\" or \"plugin\"." - .to_string(), - ),), - ), - ]), Some(vec![ - "tool_type".to_string(), + description: expected_description.to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object(BTreeMap::from([ + ( "action_type".to_string(), - "tool_id".to_string(), + JsonSchema::string(Some( + "Suggested action for the tool. Use \"install\" or \"enable\"." + .to_string(), + ),), + ), + ( "suggest_reason".to_string(), - ]), Some(false.into())), - output_schema: None, - })], + JsonSchema::string(Some( + "Concise one-line user-facing reason why this tool can help with the current request." + .to_string(), + ),), + ), + ( + "tool_id".to_string(), + JsonSchema::string_enum(vec![json!("slack@openai-curated"), json!("github")], Some( + "Connector or plugin id to suggest. Must be one of: slack@openai-curated, github." + .to_string(), + ),), + ), + ( + "tool_type".to_string(), + JsonSchema::string(Some( + "Type of discoverable tool to suggest. Use \"connector\" or \"plugin\"." + .to_string(), + ),), + ), + ]), Some(vec![ + "tool_type".to_string(), + "action_type".to_string(), + "tool_id".to_string(), + "suggest_reason".to_string(), + ]), Some(false.into())), + output_schema: None, }) ); } diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index d53bf148f3d4..f562b1022ea1 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -6,7 +6,6 @@ use crate::ShellToolOptions; use crate::SpawnAgentToolOptions; use crate::TOOL_SEARCH_DEFAULT_LIMIT; use crate::TOOL_SEARCH_TOOL_NAME; -use crate::TOOL_SUGGEST_NAMESPACE_NAME; use crate::TOOL_SUGGEST_TOOL_NAME; use crate::ToolHandlerKind; use crate::ToolName; @@ -310,13 +309,10 @@ pub fn build_tool_registry_plan( { plan.push_spec( create_tool_suggest_tool(&collect_tool_suggest_entries(discoverable_tools)), - /*supports_parallel_tool_calls*/ true, + /*supports_parallel_tool_calls*/ false, /*code_mode_enabled*/ false, ); - plan.register_handler( - ToolName::namespaced(TOOL_SUGGEST_NAMESPACE_NAME, TOOL_SUGGEST_TOOL_NAME), - ToolHandlerKind::ToolSuggest, - ); + plan.register_handler(TOOL_SUGGEST_TOOL_NAME, ToolHandlerKind::ToolSuggest); } if config.has_environment diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index dab3985cdab6..5719d226d66c 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -1556,15 +1556,18 @@ fn tool_suggest_can_be_registered_without_search_tool() { ); assert_contains_tool_names(&tools, &[TOOL_SUGGEST_TOOL_NAME]); + let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME); + assert!(!tool_suggest.supports_parallel_tool_calls); assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); - let ResponsesApiTool { description, .. } = - find_namespace_function_tool(&tools, TOOL_SUGGEST_NAMESPACE_NAME, TOOL_SUGGEST_TOOL_NAME); + let ToolSpec::Function(ResponsesApiTool { description, .. }) = &tool_suggest.spec else { + panic!("expected function tool"); + }; assert!(description.contains( - "Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin" + "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled." )); assert!(description.contains( - "You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means." + "The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant." )); } @@ -1617,17 +1620,21 @@ fn tool_suggest_description_lists_discoverable_tools() { &[], ); assert!(handlers.contains(&ToolHandlerSpec { - name: ToolName::namespaced(TOOL_SUGGEST_NAMESPACE_NAME, TOOL_SUGGEST_TOOL_NAME), + name: ToolName::plain(TOOL_SUGGEST_TOOL_NAME), kind: ToolHandlerKind::ToolSuggest, })); - let ResponsesApiTool { + let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME); + let ToolSpec::Function(ResponsesApiTool { description, parameters, .. - } = find_namespace_function_tool(&tools, TOOL_SUGGEST_NAMESPACE_NAME, TOOL_SUGGEST_TOOL_NAME); + }) = &tool_suggest.spec + else { + panic!("expected function tool"); + }; assert!(description.contains( - "Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin" + "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled." )); assert!(description.contains("Google Calendar")); assert!(description.contains("Gmail")); @@ -1641,25 +1648,29 @@ fn tool_suggest_description_lists_discoverable_tools() { ); assert!( description.contains( - "You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means." + "The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list." ) ); assert!(description.contains( - "For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely." + "The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant." )); assert!(description.contains( - "For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself." + "The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list." )); assert!(description.contains( - "Do not suggest a plugin just because one of its connectors or capabilities seems relevant." + "Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful." )); assert!(description.contains( - "Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard." + "Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery." )); - assert!(description.contains("DO NOT explore or recommend tools that are not on this list.")); + assert!( + description + .contains("do not run broad or speculative searches just to satisfy this condition.") + ); + assert!(description.contains("Only proceed when one listed plugin or connector exactly fits.")); assert!(!description.contains("{{discoverable_tools}}")); assert!(!description.contains("tool_search fails to find a good match")); - let (_, required) = expect_object_schema(parameters); + let (properties, required) = expect_object_schema(parameters); assert_eq!( required, Some(&vec![ @@ -1669,6 +1680,16 @@ fn tool_suggest_description_lists_discoverable_tools() { "suggest_reason".to_string(), ]) ); + let tool_id_schema = properties.get("tool_id").expect("tool_id schema"); + let expected_tool_ids = vec![ + json!("connector_2128aebfecb84f64a069897515042a44"), + json!("connector_68df038e0ba48191908c8434991bbac2"), + json!("sample@test"), + ]; + assert_eq!( + tool_id_schema.enum_values.as_ref(), + Some(&expected_tool_ids) + ); } #[test] From 4b4b97a67cca098aecebb1d7678bea6466e94d75 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Tue, 28 Apr 2026 17:06:45 -0700 Subject: [PATCH 3/8] Refine tool_suggest schema and logging --- codex-rs/tools/src/tool_discovery.rs | 12 ++++-------- codex-rs/tools/src/tool_discovery_tests.rs | 4 ++-- codex-rs/tools/src/tool_registry_plan.rs | 9 +++++++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index 148ca628a7ae..920c04b229b9 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -272,11 +272,6 @@ pub fn collect_tool_search_source_infos<'a>( } pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> ToolSpec { - let discoverable_tool_ids = discoverable_tools - .iter() - .map(|tool| tool.id.as_str()) - .collect::>() - .join(", "); let discoverable_tool_id_values = discoverable_tools .iter() .map(|tool| serde_json::Value::String(tool.id.clone())) @@ -297,9 +292,10 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool ), ( "tool_id".to_string(), - JsonSchema::string_enum(discoverable_tool_id_values, Some(format!( - "Connector or plugin id to suggest. Must be one of: {discoverable_tool_ids}." - ))), + JsonSchema::string_enum( + discoverable_tool_id_values, + Some("Connector or plugin id to suggest.".to_string()), + ), ), ( "suggest_reason".to_string(), diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index cb37bad4b33d..54ec028dab8b 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -117,8 +117,8 @@ fn create_tool_suggest_tool_uses_plugin_summary_fallback() { ), ( "tool_id".to_string(), - JsonSchema::string_enum(vec![json!("slack@openai-curated"), json!("github")], Some( - "Connector or plugin id to suggest. Must be one of: slack@openai-curated, github." + JsonSchema::string_enum(vec![json!("slack@openai-curated"), json!("github")], Some( + "Connector or plugin id to suggest." .to_string(), ),), ), diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index f562b1022ea1..0a8083004aec 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -303,6 +303,15 @@ pub fn build_tool_registry_plan( } } + tracing::debug!( + tool_suggest_enabled = config.tool_suggest, + discoverable_tools_present = params.discoverable_tools.is_some(), + discoverable_tool_count = params + .discoverable_tools + .map_or(0, <[crate::DiscoverableTool]>::len), + "evaluating tool_suggest registration" + ); + if config.tool_suggest && let Some(discoverable_tools) = params.discoverable_tools.filter(|tools| !tools.is_empty()) From 5aba4e411c2687426da9242f39926a67c3b897fc Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Tue, 28 Apr 2026 17:16:41 -0700 Subject: [PATCH 4/8] Refine tool suggest gating and prompt wording --- .../search_tool/tool_suggest_description.md | 4 ++-- codex-rs/core/tests/suite/tool_suggest.rs | 2 +- codex-rs/tools/src/tool_discovery.rs | 2 +- codex-rs/tools/src/tool_discovery_tests.rs | 4 ++-- codex-rs/tools/src/tool_registry_plan.rs | 9 --------- codex-rs/tools/src/tool_registry_plan_tests.rs | 13 +++++++------ 6 files changed, 13 insertions(+), 21 deletions(-) diff --git a/codex-rs/core/templates/search_tool/tool_suggest_description.md b/codex-rs/core/templates/search_tool/tool_suggest_description.md index 97f709a30170..09831bb33ed0 100644 --- a/codex-rs/core/templates/search_tool/tool_suggest_description.md +++ b/codex-rs/core/templates/search_tool/tool_suggest_description.md @@ -4,7 +4,7 @@ Use this tool only to ask the user to install or enable one known plugin or conn Use this ONLY when all of the following are true: - The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list. -- The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant. +- If `tool_search` is available, it has already been called and did not find or make the requested tool callable. - The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list. Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool. @@ -14,7 +14,7 @@ Known plugins/connectors available to install or enable: Workflow: -1. Check the current context and active `tools` list first. If `tool_search` is available and a targeted lookup is appropriate, use it to check for the requested tool; do not run broad or speculative searches just to satisfy this condition. Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery. +1. Check the current context and active `tools` list first. If `tool_search` is available, call `tool_search` before calling `tool_suggest`. Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery. 2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits. 3. If one tool clearly fits, call `tool_suggest` with: - `tool_type`: `connector` or `plugin` diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index e2ea34ab8788..ffdfc85e0461 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -135,7 +135,7 @@ async fn tool_suggest_is_available_without_search_tool_after_discovery_attempts( "Use this tool only to ask the user to install or enable one known plugin or connector from the list below" )); assert!(description.contains( - "The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant." + "If `tool_search` is available, it has already been called and did not find or make the requested tool callable." )); assert!(!description.contains("tool_search fails to find a good match")); diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index 920c04b229b9..85c7797bb16b 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -308,7 +308,7 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool let discoverable_tools = format_discoverable_tools(discoverable_tools); let description = format!( - "# Tool suggestion discovery\n\nUse this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\nUse this ONLY when all of the following are true:\n- The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list.\n- The tool is not found or made callable through a targeted `{TOOL_SEARCH_TOOL_NAME}` result when `{TOOL_SEARCH_TOOL_NAME}` is available and relevant.\n- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\nDo not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\nKnown plugins/connectors available to install or enable:\n{discoverable_tools}\n\nWorkflow:\n\n1. Check the current context and active `tools` list first. If `{TOOL_SEARCH_TOOL_NAME}` is available and a targeted lookup is appropriate, use it to check for the requested tool; do not run broad or speculative searches just to satisfy this condition. Do not use tool suggestion if the needed tool is already available, found through `{TOOL_SEARCH_TOOL_NAME}`, or callable after discovery.\n2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the known plugin/connector list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it." + "# Tool suggestion discovery\n\nUse this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\nUse this ONLY when all of the following are true:\n- The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list.\n- If `{TOOL_SEARCH_TOOL_NAME}` is available, it has already been called and did not find or make the requested tool callable.\n- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\nDo not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\nKnown plugins/connectors available to install or enable:\n{discoverable_tools}\n\nWorkflow:\n\n1. Check the current context and active `tools` list first. If `{TOOL_SEARCH_TOOL_NAME}` is available, call `{TOOL_SEARCH_TOOL_NAME}` before calling `{TOOL_SUGGEST_TOOL_NAME}`. Do not use tool suggestion if the needed tool is already available, found through `{TOOL_SEARCH_TOOL_NAME}`, or callable after discovery.\n2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the known plugin/connector list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it." ); ToolSpec::Function(ResponsesApiTool { diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index 54ec028dab8b..c8b7a5543c68 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -55,14 +55,14 @@ fn create_tool_suggest_tool_uses_plugin_summary_fallback() { "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\n", "Use this ONLY when all of the following are true:\n", "- The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list.\n", - "- The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant.\n", + "- If `tool_search` is available, it has already been called and did not find or make the requested tool callable.\n", "- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\n", "Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\n", "Known plugins/connectors available to install or enable:\n", "- GitHub (id: `github`, type: plugin, action: install): skills; MCP servers: github-mcp; app connectors: github-app\n", "- Slack (id: `slack@openai-curated`, type: connector, action: install): No description provided.\n\n", "Workflow:\n\n", - "1. Check the current context and active `tools` list first. If `tool_search` is available and a targeted lookup is appropriate, use it to check for the requested tool; do not run broad or speculative searches just to satisfy this condition. Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery.\n", + "1. Check the current context and active `tools` list first. If `tool_search` is available, call `tool_search` before calling `tool_suggest`. Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery.\n", "2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n", "3. If one tool clearly fits, call `tool_suggest` with:\n", " - `tool_type`: `connector` or `plugin`\n", diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index 0a8083004aec..f562b1022ea1 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -303,15 +303,6 @@ pub fn build_tool_registry_plan( } } - tracing::debug!( - tool_suggest_enabled = config.tool_suggest, - discoverable_tools_present = params.discoverable_tools.is_some(), - discoverable_tool_count = params - .discoverable_tools - .map_or(0, <[crate::DiscoverableTool]>::len), - "evaluating tool_suggest registration" - ); - if config.tool_suggest && let Some(discoverable_tools) = params.discoverable_tools.filter(|tools| !tools.is_empty()) diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 5719d226d66c..5ec13349ef3c 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -1567,7 +1567,7 @@ fn tool_suggest_can_be_registered_without_search_tool() { "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled." )); assert!(description.contains( - "The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant." + "If `tool_search` is available, it has already been called and did not find or make the requested tool callable." )); } @@ -1652,7 +1652,7 @@ fn tool_suggest_description_lists_discoverable_tools() { ) ); assert!(description.contains( - "The tool is not found or made callable through a targeted `tool_search` result when `tool_search` is available and relevant." + "If `tool_search` is available, it has already been called and did not find or make the requested tool callable." )); assert!(description.contains( "The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list." @@ -1663,10 +1663,11 @@ fn tool_suggest_description_lists_discoverable_tools() { assert!(description.contains( "Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery." )); - assert!( - description - .contains("do not run broad or speculative searches just to satisfy this condition.") - ); + assert!(description.contains( + "If `tool_search` is available, call `tool_search` before calling `tool_suggest`." + )); + assert!(!description.contains("targeted lookup")); + assert!(!description.contains("broad or speculative searches")); assert!(description.contains("Only proceed when one listed plugin or connector exactly fits.")); assert!(!description.contains("{{discoverable_tools}}")); assert!(!description.contains("tool_search fails to find a good match")); From 76e560150d47198fa2baeefa210bb966abc4b224 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Tue, 28 Apr 2026 17:19:52 -0700 Subject: [PATCH 5/8] Clarify tool suggestion prerequisites --- .../core/templates/search_tool/tool_suggest_description.md | 4 ++-- codex-rs/core/tests/suite/tool_suggest.rs | 2 +- codex-rs/tools/src/tool_discovery.rs | 2 +- codex-rs/tools/src/tool_discovery_tests.rs | 4 ++-- codex-rs/tools/src/tool_registry_plan_tests.rs | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/codex-rs/core/templates/search_tool/tool_suggest_description.md b/codex-rs/core/templates/search_tool/tool_suggest_description.md index 09831bb33ed0..89346376ad07 100644 --- a/codex-rs/core/templates/search_tool/tool_suggest_description.md +++ b/codex-rs/core/templates/search_tool/tool_suggest_description.md @@ -3,8 +3,8 @@ Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled. Use this ONLY when all of the following are true: -- The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list. -- If `tool_search` is available, it has already been called and did not find or make the requested tool callable. +- The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list. +- `tool_search` is not available, or it has already been called and did not find or make the requested tool callable. - The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list. Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool. diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index ffdfc85e0461..3147a5c053ae 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -135,7 +135,7 @@ async fn tool_suggest_is_available_without_search_tool_after_discovery_attempts( "Use this tool only to ask the user to install or enable one known plugin or connector from the list below" )); assert!(description.contains( - "If `tool_search` is available, it has already been called and did not find or make the requested tool callable." + "`tool_search` is not available, or it has already been called and did not find or make the requested tool callable." )); assert!(!description.contains("tool_search fails to find a good match")); diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index 85c7797bb16b..da2d1fb11642 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -308,7 +308,7 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool let discoverable_tools = format_discoverable_tools(discoverable_tools); let description = format!( - "# Tool suggestion discovery\n\nUse this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\nUse this ONLY when all of the following are true:\n- The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list.\n- If `{TOOL_SEARCH_TOOL_NAME}` is available, it has already been called and did not find or make the requested tool callable.\n- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\nDo not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\nKnown plugins/connectors available to install or enable:\n{discoverable_tools}\n\nWorkflow:\n\n1. Check the current context and active `tools` list first. If `{TOOL_SEARCH_TOOL_NAME}` is available, call `{TOOL_SEARCH_TOOL_NAME}` before calling `{TOOL_SUGGEST_TOOL_NAME}`. Do not use tool suggestion if the needed tool is already available, found through `{TOOL_SEARCH_TOOL_NAME}`, or callable after discovery.\n2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the known plugin/connector list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it." + "# Tool suggestion discovery\n\nUse this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\nUse this ONLY when all of the following are true:\n- The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list.\n- `{TOOL_SEARCH_TOOL_NAME}` is not available, or it has already been called and did not find or make the requested tool callable.\n- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\nDo not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\nKnown plugins/connectors available to install or enable:\n{discoverable_tools}\n\nWorkflow:\n\n1. Check the current context and active `tools` list first. If `{TOOL_SEARCH_TOOL_NAME}` is available, call `{TOOL_SEARCH_TOOL_NAME}` before calling `{TOOL_SUGGEST_TOOL_NAME}`. Do not use tool suggestion if the needed tool is already available, found through `{TOOL_SEARCH_TOOL_NAME}`, or callable after discovery.\n2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the known plugin/connector list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it." ); ToolSpec::Function(ResponsesApiTool { diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index c8b7a5543c68..046c9a16053e 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -54,8 +54,8 @@ fn create_tool_suggest_tool_uses_plugin_summary_fallback() { "# Tool suggestion discovery\n\n", "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\n", "Use this ONLY when all of the following are true:\n", - "- The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list.\n", - "- If `tool_search` is available, it has already been called and did not find or make the requested tool callable.\n", + "- The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list.\n", + "- `tool_search` is not available, or it has already been called and did not find or make the requested tool callable.\n", "- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\n", "Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\n", "Known plugins/connectors available to install or enable:\n", diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 5ec13349ef3c..5cb9959d5320 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -1567,7 +1567,7 @@ fn tool_suggest_can_be_registered_without_search_tool() { "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled." )); assert!(description.contains( - "If `tool_search` is available, it has already been called and did not find or make the requested tool callable." + "`tool_search` is not available, or it has already been called and did not find or make the requested tool callable." )); } @@ -1648,11 +1648,11 @@ fn tool_suggest_description_lists_discoverable_tools() { ); assert!( description.contains( - "The user explicitly wants a specific tool, plugin, connector, or app capability that is not already available in the current context or active `tools` list." + "The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list." ) ); assert!(description.contains( - "If `tool_search` is available, it has already been called and did not find or make the requested tool callable." + "`tool_search` is not available, or it has already been called and did not find or make the requested tool callable." )); assert!(description.contains( "The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list." From 251f33095e3907381a898a9a89fba5eb19246a60 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 29 Apr 2026 10:44:08 -0700 Subject: [PATCH 6/8] update --- codex-rs/tools/src/tool_registry_plan.rs | 2 +- codex-rs/tools/src/tool_registry_plan_tests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index ce976dd39f94..eff3750edda0 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -313,7 +313,7 @@ pub fn build_tool_registry_plan( { plan.push_spec( create_tool_suggest_tool(&collect_tool_suggest_entries(discoverable_tools)), - /*supports_parallel_tool_calls*/ false, + /*supports_parallel_tool_calls*/ true, /*code_mode_enabled*/ false, ); plan.register_handler(TOOL_SUGGEST_TOOL_NAME, ToolHandlerKind::ToolSuggest); diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 4118e76949fe..de44be191d27 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -1722,7 +1722,7 @@ fn tool_suggest_can_be_registered_without_search_tool() { assert_contains_tool_names(&tools, &[TOOL_SUGGEST_TOOL_NAME]); let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME); - assert!(!tool_suggest.supports_parallel_tool_calls); + assert!(tool_suggest.supports_parallel_tool_calls); assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); let ToolSpec::Function(ResponsesApiTool { description, .. }) = &tool_suggest.spec else { From e43d00be4f874465fe28cec5797203d72dad7a33 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 29 Apr 2026 12:06:39 -0700 Subject: [PATCH 7/8] update --- .../search_tool/tool_suggest_description.md | 2 ++ codex-rs/core/tests/suite/tool_suggest.rs | 1 + codex-rs/tools/src/tool_discovery.rs | 11 ++--------- codex-rs/tools/src/tool_discovery_tests.rs | 5 +++-- codex-rs/tools/src/tool_registry_plan_tests.rs | 13 ++----------- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/codex-rs/core/templates/search_tool/tool_suggest_description.md b/codex-rs/core/templates/search_tool/tool_suggest_description.md index 89346376ad07..7c39af512050 100644 --- a/codex-rs/core/templates/search_tool/tool_suggest_description.md +++ b/codex-rs/core/templates/search_tool/tool_suggest_description.md @@ -24,3 +24,5 @@ Workflow: 4. After the suggestion flow completes: - if the user finished the install or enable flow, continue by searching again or using the newly available tool - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it. + +IMPORTANT: DO NOT call this tool in parallel with other tools. diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index 852d852d4f28..b8c3343008f4 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -137,6 +137,7 @@ async fn tool_suggest_is_available_without_search_tool_after_discovery_attempts( assert!(description.contains( "`tool_search` is not available, or it has already been called and did not find or make the requested tool callable." )); + assert!(description.contains("IMPORTANT: DO NOT call this tool in parallel with other tools.")); assert!(!description.contains("tool_search fails to find a good match")); Ok(()) diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index da2d1fb11642..cbcc30f75c38 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -272,10 +272,6 @@ pub fn collect_tool_search_source_infos<'a>( } pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> ToolSpec { - let discoverable_tool_id_values = discoverable_tools - .iter() - .map(|tool| serde_json::Value::String(tool.id.clone())) - .collect(); let properties = BTreeMap::from([ ( "tool_type".to_string(), @@ -292,10 +288,7 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool ), ( "tool_id".to_string(), - JsonSchema::string_enum( - discoverable_tool_id_values, - Some("Connector or plugin id to suggest.".to_string()), - ), + JsonSchema::string(Some("Connector or plugin id to suggest.".to_string())), ), ( "suggest_reason".to_string(), @@ -308,7 +301,7 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool let discoverable_tools = format_discoverable_tools(discoverable_tools); let description = format!( - "# Tool suggestion discovery\n\nUse this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\nUse this ONLY when all of the following are true:\n- The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list.\n- `{TOOL_SEARCH_TOOL_NAME}` is not available, or it has already been called and did not find or make the requested tool callable.\n- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\nDo not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\nKnown plugins/connectors available to install or enable:\n{discoverable_tools}\n\nWorkflow:\n\n1. Check the current context and active `tools` list first. If `{TOOL_SEARCH_TOOL_NAME}` is available, call `{TOOL_SEARCH_TOOL_NAME}` before calling `{TOOL_SUGGEST_TOOL_NAME}`. Do not use tool suggestion if the needed tool is already available, found through `{TOOL_SEARCH_TOOL_NAME}`, or callable after discovery.\n2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the known plugin/connector list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it." + "# Tool suggestion discovery\n\nUse this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\nUse this ONLY when all of the following are true:\n- The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list.\n- `{TOOL_SEARCH_TOOL_NAME}` is not available, or it has already been called and did not find or make the requested tool callable.\n- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\nDo not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\nKnown plugins/connectors available to install or enable:\n{discoverable_tools}\n\nWorkflow:\n\n1. Check the current context and active `tools` list first. If `{TOOL_SEARCH_TOOL_NAME}` is available, call `{TOOL_SEARCH_TOOL_NAME}` before calling `{TOOL_SUGGEST_TOOL_NAME}`. Do not use tool suggestion if the needed tool is already available, found through `{TOOL_SEARCH_TOOL_NAME}`, or callable after discovery.\n2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the known plugin/connector list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.\n\nIMPORTANT: DO NOT call this tool in parallel with other tools." ); ToolSpec::Function(ResponsesApiTool { diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index 046c9a16053e..c6524de21659 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -71,7 +71,8 @@ fn create_tool_suggest_tool_uses_plugin_summary_fallback() { " - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n", "4. After the suggestion flow completes:\n", " - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n", - " - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.", + " - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.\n\n", + "IMPORTANT: DO NOT call this tool in parallel with other tools.", ); assert_eq!( @@ -117,7 +118,7 @@ fn create_tool_suggest_tool_uses_plugin_summary_fallback() { ), ( "tool_id".to_string(), - JsonSchema::string_enum(vec![json!("slack@openai-curated"), json!("github")], Some( + JsonSchema::string(Some( "Connector or plugin id to suggest." .to_string(), ),), diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index de44be191d27..87b9f3ddf8e5 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -1825,6 +1825,7 @@ fn tool_suggest_description_lists_discoverable_tools() { assert!(description.contains( "Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful." )); + assert!(description.contains("IMPORTANT: DO NOT call this tool in parallel with other tools.")); assert!(description.contains( "Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery." )); @@ -1836,7 +1837,7 @@ fn tool_suggest_description_lists_discoverable_tools() { assert!(description.contains("Only proceed when one listed plugin or connector exactly fits.")); assert!(!description.contains("{{discoverable_tools}}")); assert!(!description.contains("tool_search fails to find a good match")); - let (properties, required) = expect_object_schema(parameters); + let (_, required) = expect_object_schema(parameters); assert_eq!( required, Some(&vec![ @@ -1846,16 +1847,6 @@ fn tool_suggest_description_lists_discoverable_tools() { "suggest_reason".to_string(), ]) ); - let tool_id_schema = properties.get("tool_id").expect("tool_id schema"); - let expected_tool_ids = vec![ - json!("connector_2128aebfecb84f64a069897515042a44"), - json!("connector_68df038e0ba48191908c8434991bbac2"), - json!("sample@test"), - ]; - assert_eq!( - tool_id_schema.enum_values.as_ref(), - Some(&expected_tool_ids) - ); } #[test] From 95d25912033821979cc53ab2b39e02aa7c4b1fb6 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 29 Apr 2026 12:34:40 -0700 Subject: [PATCH 8/8] update --- .../search_tool/tool_suggest_description.md | 15 ++++++++------- codex-rs/core/tests/suite/tool_suggest.rs | 2 +- codex-rs/tools/src/tool_discovery.rs | 6 ++---- codex-rs/tools/src/tool_discovery_tests.rs | 17 +++++++++-------- codex-rs/tools/src/tool_registry_plan_tests.rs | 11 +++++++---- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/codex-rs/core/templates/search_tool/tool_suggest_description.md b/codex-rs/core/templates/search_tool/tool_suggest_description.md index 7c39af512050..9bed2d9d7bdb 100644 --- a/codex-rs/core/templates/search_tool/tool_suggest_description.md +++ b/codex-rs/core/templates/search_tool/tool_suggest_description.md @@ -1,28 +1,29 @@ # Tool suggestion discovery -Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled. +Use this tool only to ask the user to install one known plugin or connector from the list below. The list contains known candidates that are not currently installed. Use this ONLY when all of the following are true: - The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list. - `tool_search` is not available, or it has already been called and did not find or make the requested tool callable. -- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list. +- The tool is one of the known installable plugins or connectors listed below. Only ask to install tools from this list. Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool. -Known plugins/connectors available to install or enable: +Known plugins/connectors available to install: {{discoverable_tools}} Workflow: 1. Check the current context and active `tools` list first. If `tool_search` is available, call `tool_search` before calling `tool_suggest`. Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery. 2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits. -3. If one tool clearly fits, call `tool_suggest` with: +3. If we found both connectors and plugins to suggest, use plugins first, only use connectors if the corresponding plugin is installed but the connector is not. +4. If one tool clearly fits, call `tool_suggest` with: - `tool_type`: `connector` or `plugin` - - `action_type`: `install` or `enable` + - `action_type`: `install` - `tool_id`: exact id from the known plugin/connector list above - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request -4. After the suggestion flow completes: - - if the user finished the install or enable flow, continue by searching again or using the newly available tool +5. After the suggestion flow completes: + - if the user finished the install flow, continue by searching again or using the newly available tool - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it. IMPORTANT: DO NOT call this tool in parallel with other tools. diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index b8c3343008f4..6cb19d01a5b5 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -132,7 +132,7 @@ async fn tool_suggest_is_available_without_search_tool_after_discovery_attempts( let description = function_tool_description(&body, TOOL_SUGGEST_TOOL_NAME).expect("description"); assert!(description.contains( - "Use this tool only to ask the user to install or enable one known plugin or connector from the list below" + "Use this tool only to ask the user to install one known plugin or connector from the list below" )); assert!(description.contains( "`tool_search` is not available, or it has already been called and did not find or make the requested tool callable." diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index cbcc30f75c38..74977dce385c 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -282,9 +282,7 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool ), ( "action_type".to_string(), - JsonSchema::string(Some( - "Suggested action for the tool. Use \"install\" or \"enable\".".to_string(), - )), + JsonSchema::string(Some("Suggested action for the tool. Use \"install\".".to_string())), ), ( "tool_id".to_string(), @@ -301,7 +299,7 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool let discoverable_tools = format_discoverable_tools(discoverable_tools); let description = format!( - "# Tool suggestion discovery\n\nUse this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\nUse this ONLY when all of the following are true:\n- The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list.\n- `{TOOL_SEARCH_TOOL_NAME}` is not available, or it has already been called and did not find or make the requested tool callable.\n- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\nDo not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\nKnown plugins/connectors available to install or enable:\n{discoverable_tools}\n\nWorkflow:\n\n1. Check the current context and active `tools` list first. If `{TOOL_SEARCH_TOOL_NAME}` is available, call `{TOOL_SEARCH_TOOL_NAME}` before calling `{TOOL_SUGGEST_TOOL_NAME}`. Do not use tool suggestion if the needed tool is already available, found through `{TOOL_SEARCH_TOOL_NAME}`, or callable after discovery.\n2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the known plugin/connector list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.\n\nIMPORTANT: DO NOT call this tool in parallel with other tools." + "# Tool suggestion discovery\n\nUse this tool only to ask the user to install one known plugin or connector from the list below. The list contains known candidates that are not currently installed.\n\nUse this ONLY when all of the following are true:\n- The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list.\n- `{TOOL_SEARCH_TOOL_NAME}` is not available, or it has already been called and did not find or make the requested tool callable.\n- The tool is one of the known installable plugins or connectors listed below. Only ask to install tools from this list.\n\nDo not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\nKnown plugins/connectors available to install:\n{discoverable_tools}\n\nWorkflow:\n\n1. Check the current context and active `tools` list first. If `{TOOL_SEARCH_TOOL_NAME}` is available, call `{TOOL_SEARCH_TOOL_NAME}` before calling `{TOOL_SUGGEST_TOOL_NAME}`. Do not use tool suggestion if the needed tool is already available, found through `{TOOL_SEARCH_TOOL_NAME}`, or callable after discovery.\n2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n3. If we found both connectors and plugins to suggest, use plugins first, only use connectors if the corresponding plugin is installed but the connector is not.\n4. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install`\n - `tool_id`: exact id from the known plugin/connector list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n5. After the suggestion flow completes:\n - if the user finished the install flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.\n\nIMPORTANT: DO NOT call this tool in parallel with other tools." ); ToolSpec::Function(ResponsesApiTool { diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index c6524de21659..9edbccffaa7a 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -52,25 +52,26 @@ fn create_tool_search_tool_deduplicates_and_renders_enabled_sources() { fn create_tool_suggest_tool_uses_plugin_summary_fallback() { let expected_description = concat!( "# Tool suggestion discovery\n\n", - "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled.\n\n", + "Use this tool only to ask the user to install one known plugin or connector from the list below. The list contains known candidates that are not currently installed.\n\n", "Use this ONLY when all of the following are true:\n", "- The user explicitly wants a specific plugin or connector that is not already available in the current context or active `tools` list.\n", "- `tool_search` is not available, or it has already been called and did not find or make the requested tool callable.\n", - "- The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list.\n\n", + "- The tool is one of the known installable plugins or connectors listed below. Only ask to install tools from this list.\n\n", "Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful. The user's intent must clearly match one listed tool.\n\n", - "Known plugins/connectors available to install or enable:\n", + "Known plugins/connectors available to install:\n", "- GitHub (id: `github`, type: plugin, action: install): skills; MCP servers: github-mcp; app connectors: github-app\n", "- Slack (id: `slack@openai-curated`, type: connector, action: install): No description provided.\n\n", "Workflow:\n\n", "1. Check the current context and active `tools` list first. If `tool_search` is available, call `tool_search` before calling `tool_suggest`. Do not use tool suggestion if the needed tool is already available, found through `tool_search`, or callable after discovery.\n", "2. Match the user's explicit request against the known plugin/connector list above. Only proceed when one listed plugin or connector exactly fits.\n", - "3. If one tool clearly fits, call `tool_suggest` with:\n", + "3. If we found both connectors and plugins to suggest, use plugins first, only use connectors if the corresponding plugin is installed but the connector is not.\n", + "4. If one tool clearly fits, call `tool_suggest` with:\n", " - `tool_type`: `connector` or `plugin`\n", - " - `action_type`: `install` or `enable`\n", + " - `action_type`: `install`\n", " - `tool_id`: exact id from the known plugin/connector list above\n", " - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n", - "4. After the suggestion flow completes:\n", - " - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n", + "5. After the suggestion flow completes:\n", + " - if the user finished the install flow, continue by searching again or using the newly available tool\n", " - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.\n\n", "IMPORTANT: DO NOT call this tool in parallel with other tools.", ); @@ -105,7 +106,7 @@ fn create_tool_suggest_tool_uses_plugin_summary_fallback() { ( "action_type".to_string(), JsonSchema::string(Some( - "Suggested action for the tool. Use \"install\" or \"enable\"." + "Suggested action for the tool. Use \"install\"." .to_string(), ),), ), diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 87b9f3ddf8e5..8d3ab5253c8e 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -1729,7 +1729,7 @@ fn tool_suggest_can_be_registered_without_search_tool() { panic!("expected function tool"); }; assert!(description.contains( - "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled." + "Use this tool only to ask the user to install one known plugin or connector from the list below. The list contains known candidates that are not currently installed." )); assert!(description.contains( "`tool_search` is not available, or it has already been called and did not find or make the requested tool callable." @@ -1799,7 +1799,7 @@ fn tool_suggest_description_lists_discoverable_tools() { panic!("expected function tool"); }; assert!(description.contains( - "Use this tool only to ask the user to install or enable one known plugin or connector from the list below. The list contains known candidates that are not currently installed or not currently enabled." + "Use this tool only to ask the user to install one known plugin or connector from the list below. The list contains known candidates that are not currently installed." )); assert!(description.contains("Google Calendar")); assert!(description.contains("Gmail")); @@ -1807,7 +1807,7 @@ fn tool_suggest_description_lists_discoverable_tools() { assert!(description.contains("Plan events and schedules.")); assert!(description.contains("Find and summarize email threads.")); assert!(description.contains("id: `sample@test`, type: plugin, action: install")); - assert!(description.contains("`action_type`: `install` or `enable`")); + assert!(description.contains("`action_type`: `install`")); assert!( description.contains("skills; MCP servers: sample-docs; app connectors: connector_sample") ); @@ -1820,7 +1820,7 @@ fn tool_suggest_description_lists_discoverable_tools() { "`tool_search` is not available, or it has already been called and did not find or make the requested tool callable." )); assert!(description.contains( - "The tool is one of the known installable or enableable plugins or connectors listed below. Only ask to install or enable tools from this list." + "The tool is one of the known installable plugins or connectors listed below. Only ask to install tools from this list." )); assert!(description.contains( "Do not use tool suggestion for adjacent capabilities, broad recommendations, or tools that merely seem useful." @@ -1835,6 +1835,9 @@ fn tool_suggest_description_lists_discoverable_tools() { assert!(!description.contains("targeted lookup")); assert!(!description.contains("broad or speculative searches")); assert!(description.contains("Only proceed when one listed plugin or connector exactly fits.")); + assert!(description.contains( + "If we found both connectors and plugins to suggest, use plugins first, only use connectors if the corresponding plugin is installed but the connector is not." + )); assert!(!description.contains("{{discoverable_tools}}")); assert!(!description.contains("tool_search fails to find a good match")); let (_, required) = expect_object_schema(parameters);