Extract tool-suggest wire helpers into codex-tools (#16499)

## Why

This is another straight-refactor step in the `codex-tools` migration.

`core/src/tools/handlers/tool_suggest.rs` still owned request/response
payload structs, elicitation metadata shaping, and connector-completion
predicates that do not depend on `codex-core` session/runtime internals.
Per the `AGENTS.md` guidance to keep shrinking `codex-core`, this moves
that pure wire-format logic into `codex-rs/tools` so the core handler
keeps only session orchestration, plugin/config refresh, and MCP cache
updates.

## What changed

- Added `codex-rs/tools/src/tool_suggest.rs` and exported its API from
`codex-rs/tools/src/lib.rs`.
- Moved `ToolSuggestArgs`, `ToolSuggestResult`, `ToolSuggestMeta`,
`build_tool_suggestion_elicitation_request()`,
`all_suggested_connectors_picked_up()`, and
`verified_connector_suggestion_completed()` into `codex-tools`.
- Rewired `core/src/tools/handlers/tool_suggest.rs` to consume those
exports directly.
- Ported the existing pure helper tests from
`core/src/tools/handlers/tool_suggest_tests.rs` to
`tools/src/tool_suggest_tests.rs` without adding new behavior coverage.

## Validation

```shell
cargo test -p codex-tools
cargo test -p codex-core tools::handlers::tool_suggest::tests
just argument-comment-lint
```
This commit is contained in:
Michael Bolin
2026-04-01 20:49:15 -07:00
committed by GitHub
parent c2699c666c
commit d1068e057a
5 changed files with 346 additions and 323 deletions

View File

@@ -19,6 +19,7 @@ mod tool_config;
mod tool_definition;
mod tool_discovery;
mod tool_spec;
mod tool_suggest;
mod utility_tool;
mod view_image;
@@ -112,6 +113,13 @@ pub use tool_spec::create_image_generation_tool;
pub use tool_spec::create_local_shell_tool;
pub use tool_spec::create_tools_json_for_responses_api;
pub use tool_spec::create_web_search_tool;
pub use tool_suggest::TOOL_SUGGEST_APPROVAL_KIND_VALUE;
pub use tool_suggest::ToolSuggestArgs;
pub use tool_suggest::ToolSuggestMeta;
pub use tool_suggest::ToolSuggestResult;
pub use tool_suggest::all_suggested_connectors_picked_up;
pub use tool_suggest::build_tool_suggestion_elicitation_request;
pub use tool_suggest::verified_connector_suggestion_completed;
pub use utility_tool::create_list_dir_tool;
pub use utility_tool::create_test_sync_tool;
pub use view_image::ViewImageToolOptions;

View File

@@ -0,0 +1,125 @@
use std::collections::BTreeMap;
use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::McpElicitationObjectType;
use codex_app_server_protocol::McpElicitationSchema;
use codex_app_server_protocol::McpServerElicitationRequest;
use codex_app_server_protocol::McpServerElicitationRequestParams;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
use crate::DiscoverableTool;
use crate::DiscoverableToolAction;
use crate::DiscoverableToolType;
pub const TOOL_SUGGEST_APPROVAL_KIND_VALUE: &str = "tool_suggestion";
#[derive(Debug, Deserialize)]
pub struct ToolSuggestArgs {
pub tool_type: DiscoverableToolType,
pub action_type: DiscoverableToolAction,
pub tool_id: String,
pub suggest_reason: String,
}
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct ToolSuggestResult {
pub completed: bool,
pub user_confirmed: bool,
pub tool_type: DiscoverableToolType,
pub action_type: DiscoverableToolAction,
pub tool_id: String,
pub tool_name: String,
pub suggest_reason: String,
}
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct ToolSuggestMeta<'a> {
pub codex_approval_kind: &'static str,
pub tool_type: DiscoverableToolType,
pub suggest_type: DiscoverableToolAction,
pub suggest_reason: &'a str,
pub tool_id: &'a str,
pub tool_name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub install_url: Option<&'a str>,
}
pub fn build_tool_suggestion_elicitation_request(
server_name: &str,
thread_id: String,
turn_id: String,
args: &ToolSuggestArgs,
suggest_reason: &str,
tool: &DiscoverableTool,
) -> McpServerElicitationRequestParams {
let tool_name = tool.name().to_string();
let install_url = tool.install_url().map(ToString::to_string);
let message = suggest_reason.to_string();
McpServerElicitationRequestParams {
thread_id,
turn_id: Some(turn_id),
server_name: server_name.to_string(),
request: McpServerElicitationRequest::Form {
meta: Some(json!(build_tool_suggestion_meta(
args.tool_type,
args.action_type,
suggest_reason,
tool.id(),
tool_name.as_str(),
install_url.as_deref(),
))),
message,
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
},
},
}
}
pub fn all_suggested_connectors_picked_up(
expected_connector_ids: &[String],
accessible_connectors: &[AppInfo],
) -> bool {
expected_connector_ids.iter().all(|connector_id| {
verified_connector_suggestion_completed(connector_id, accessible_connectors)
})
}
pub fn verified_connector_suggestion_completed(
tool_id: &str,
accessible_connectors: &[AppInfo],
) -> bool {
accessible_connectors
.iter()
.find(|connector| connector.id == tool_id)
.is_some_and(|connector| connector.is_accessible)
}
fn build_tool_suggestion_meta<'a>(
tool_type: DiscoverableToolType,
action_type: DiscoverableToolAction,
suggest_reason: &'a str,
tool_id: &'a str,
tool_name: &'a str,
install_url: Option<&'a str>,
) -> ToolSuggestMeta<'a> {
ToolSuggestMeta {
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
tool_type,
suggest_type: action_type,
suggest_reason,
tool_id,
tool_name,
install_url,
}
}
#[cfg(test)]
#[path = "tool_suggest_tests.rs"]
mod tests;

View File

@@ -0,0 +1,207 @@
use super::*;
use crate::DiscoverablePluginInfo;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn build_tool_suggestion_elicitation_request_uses_expected_shape() {
let args = ToolSuggestArgs {
tool_type: DiscoverableToolType::Connector,
action_type: DiscoverableToolAction::Install,
tool_id: "connector_2128aebfecb84f64a069897515042a44".to_string(),
suggest_reason: "Plan and reference events from your calendar".to_string(),
};
let connector = DiscoverableTool::Connector(Box::new(AppInfo {
id: "connector_2128aebfecb84f64a069897515042a44".to_string(),
name: "Google Calendar".to_string(),
description: Some("Plan events and schedules.".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: Some(
"https://chatgpt.com/apps/google-calendar/connector_2128aebfecb84f64a069897515042a44"
.to_string(),
),
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
}));
let request = build_tool_suggestion_elicitation_request(
"codex-apps",
"thread-1".to_string(),
"turn-1".to_string(),
&args,
"Plan and reference events from your calendar",
&connector,
);
assert_eq!(
request,
McpServerElicitationRequestParams {
thread_id: "thread-1".to_string(),
turn_id: Some("turn-1".to_string()),
server_name: "codex-apps".to_string(),
request: McpServerElicitationRequest::Form {
meta: Some(json!(ToolSuggestMeta {
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
tool_type: DiscoverableToolType::Connector,
suggest_type: DiscoverableToolAction::Install,
suggest_reason: "Plan and reference events from your calendar",
tool_id: "connector_2128aebfecb84f64a069897515042a44",
tool_name: "Google Calendar",
install_url: Some(
"https://chatgpt.com/apps/google-calendar/connector_2128aebfecb84f64a069897515042a44"
),
})),
message: "Plan and reference events from your calendar".to_string(),
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
},
},
},
);
}
#[test]
fn build_tool_suggestion_elicitation_request_for_plugin_omits_install_url() {
let args = ToolSuggestArgs {
tool_type: DiscoverableToolType::Plugin,
action_type: DiscoverableToolAction::Install,
tool_id: "sample@openai-curated".to_string(),
suggest_reason: "Use the sample plugin's skills and MCP server".to_string(),
};
let plugin = DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
id: "sample@openai-curated".to_string(),
name: "Sample Plugin".to_string(),
description: Some("Includes skills, MCP servers, and apps.".to_string()),
has_skills: true,
mcp_server_names: vec!["sample-docs".to_string()],
app_connector_ids: vec!["connector_calendar".to_string()],
}));
let request = build_tool_suggestion_elicitation_request(
"codex-apps",
"thread-1".to_string(),
"turn-1".to_string(),
&args,
"Use the sample plugin's skills and MCP server",
&plugin,
);
assert_eq!(
request,
McpServerElicitationRequestParams {
thread_id: "thread-1".to_string(),
turn_id: Some("turn-1".to_string()),
server_name: "codex-apps".to_string(),
request: McpServerElicitationRequest::Form {
meta: Some(json!(ToolSuggestMeta {
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
tool_type: DiscoverableToolType::Plugin,
suggest_type: DiscoverableToolAction::Install,
suggest_reason: "Use the sample plugin's skills and MCP server",
tool_id: "sample@openai-curated",
tool_name: "Sample Plugin",
install_url: None,
})),
message: "Use the sample plugin's skills and MCP server".to_string(),
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
},
},
},
);
}
#[test]
fn build_tool_suggestion_meta_uses_expected_shape() {
let meta = build_tool_suggestion_meta(
DiscoverableToolType::Connector,
DiscoverableToolAction::Install,
"Find and reference emails from your inbox",
"connector_68df038e0ba48191908c8434991bbac2",
"Gmail",
Some("https://chatgpt.com/apps/gmail/connector_68df038e0ba48191908c8434991bbac2"),
);
assert_eq!(
meta,
ToolSuggestMeta {
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
tool_type: DiscoverableToolType::Connector,
suggest_type: DiscoverableToolAction::Install,
suggest_reason: "Find and reference emails from your inbox",
tool_id: "connector_68df038e0ba48191908c8434991bbac2",
tool_name: "Gmail",
install_url: Some(
"https://chatgpt.com/apps/gmail/connector_68df038e0ba48191908c8434991bbac2"
),
},
);
}
#[test]
fn verified_connector_suggestion_completed_requires_accessible_connector() {
let accessible_connectors = vec![AppInfo {
id: "calendar".to_string(),
name: "Google Calendar".to_string(),
description: None,
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: None,
is_accessible: true,
is_enabled: false,
plugin_display_names: Vec::new(),
}];
assert!(verified_connector_suggestion_completed(
"calendar",
&accessible_connectors,
));
assert!(!verified_connector_suggestion_completed(
"gmail",
&accessible_connectors,
));
}
#[test]
fn all_suggested_connectors_picked_up_requires_every_expected_connector() {
let accessible_connectors = vec![AppInfo {
id: "calendar".to_string(),
name: "Google Calendar".to_string(),
description: None,
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: None,
is_accessible: true,
is_enabled: false,
plugin_display_names: Vec::new(),
}];
assert!(all_suggested_connectors_picked_up(
&["calendar".to_string()],
&accessible_connectors,
));
assert!(!all_suggested_connectors_picked_up(
&["calendar".to_string(), "gmail".to_string()],
&accessible_connectors,
));
}