mirror of
https://github.com/openai/codex.git
synced 2026-05-02 04:11:39 +03:00
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:
@@ -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;
|
||||
|
||||
125
codex-rs/tools/src/tool_suggest.rs
Normal file
125
codex-rs/tools/src/tool_suggest.rs
Normal 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;
|
||||
207
codex-rs/tools/src/tool_suggest_tests.rs
Normal file
207
codex-rs/tools/src/tool_suggest_tests.rs
Normal 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,
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user