Extract update_plan tool spec into codex-tools (#16481)

## Why

`codex-rs/core/src/tools/handlers/plan.rs` still owned both the
`update_plan` runtime handler and the static tool definition. The tool
definition is pure metadata, so keeping it in `codex-core` works against
the ongoing effort to move tool-spec code into `codex-tools` and keep
`codex-core` focused on orchestration and execution paths.

This continues the extraction work from #16379, #16471, and #16477.

## What Changed

- added `codex-rs/tools/src/plan_tool.rs` with
`create_update_plan_tool()`
- re-exported that constructor from `codex-rs/tools/src/lib.rs`
- updated `codex-rs/core/src/tools/spec.rs` and
`codex-rs/core/src/tools/spec_tests.rs` to use the `codex-tools` export
instead of a core-local static
- removed the old `PLAN_TOOL` definition from
`codex-rs/core/src/tools/handlers/plan.rs`; the `PlanHandler` runtime
logic still stays in `codex-core`
- tightened two `codex-core` aliases to `#[cfg(test)]` now that
production code no longer needs them

## Testing

- `cargo test -p codex-tools`
- `cargo test -p codex-core tools::spec::tests`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16481).
* #16482
* __->__ #16481
This commit is contained in:
Michael Bolin
2026-04-01 15:51:52 -07:00
committed by GitHub
Unverified
parent 30f6786d62
commit 6cf832fc63
9 changed files with 64 additions and 59 deletions
+1
View File
@@ -157,6 +157,7 @@ fn strip_total_output_header(output: &str) -> Option<(&str, u32)> {
}
pub(crate) mod tools {
#[cfg(test)]
pub(crate) use codex_tools::ResponsesApiTool;
pub(crate) use codex_tools::ToolSearchOutputTool;
pub(crate) use codex_tools::ToolSpec;
+1 -1
View File
@@ -147,7 +147,7 @@ fn tool_search_payloads_roundtrip_as_tool_search_outputs() {
description: String::new(),
strict: false,
defer_loading: Some(true),
parameters: crate::tools::spec::JsonSchema::Object {
parameters: codex_tools::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
-1
View File
@@ -22,7 +22,6 @@ use codex_sandboxing::policy_transforms::intersect_permission_profiles;
use codex_sandboxing::policy_transforms::merge_permission_profiles;
use codex_sandboxing::policy_transforms::normalize_additional_permissions;
use codex_utils_absolute_path::AbsolutePathBufGuard;
pub use plan::PLAN_TOOL;
use serde::Deserialize;
use serde_json::Value;
use std::path::Path;
-49
View File
@@ -1,5 +1,3 @@
use crate::client_common::tools::ResponsesApiTool;
use crate::client_common::tools::ToolSpec;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::function_tool::FunctionCallError;
@@ -8,7 +6,6 @@ use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::spec::JsonSchema;
use async_trait::async_trait;
use codex_protocol::config_types::ModeKind;
use codex_protocol::models::FunctionCallOutputPayload;
@@ -16,8 +13,6 @@ use codex_protocol::models::ResponseInputItem;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::EventMsg;
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
use std::sync::LazyLock;
pub struct PlanHandler;
@@ -49,50 +44,6 @@ impl ToolOutput for PlanToolOutput {
}
}
pub static PLAN_TOOL: LazyLock<ToolSpec> = LazyLock::new(|| {
let mut plan_item_props = BTreeMap::new();
plan_item_props.insert("step".to_string(), JsonSchema::String { description: None });
plan_item_props.insert(
"status".to_string(),
JsonSchema::String {
description: Some("One of: pending, in_progress, completed".to_string()),
},
);
let plan_items_schema = JsonSchema::Array {
description: Some("The list of steps".to_string()),
items: Box::new(JsonSchema::Object {
properties: plan_item_props,
required: Some(vec!["step".to_string(), "status".to_string()]),
additional_properties: Some(false.into()),
}),
};
let mut properties = BTreeMap::new();
properties.insert(
"explanation".to_string(),
JsonSchema::String { description: None },
);
properties.insert("plan".to_string(), plan_items_schema);
ToolSpec::Function(ResponsesApiTool {
name: "update_plan".to_string(),
description: r#"Updates the task plan.
Provide an optional explanation and a list of plan items, each with a step and status.
At most one step can be in_progress at a time.
"#
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["plan".to_string()]),
additional_properties: Some(false.into()),
},
output_schema: None,
})
});
#[async_trait]
impl ToolHandler for PlanHandler {
type Output = PlanToolOutput;
@@ -105,7 +105,7 @@ fn serialize_tool_search_output_tools_groups_results_by_namespace() {
description: "Create a calendar event.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: crate::tools::spec::JsonSchema::Object {
parameters: codex_tools::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
@@ -117,7 +117,7 @@ fn serialize_tool_search_output_tools_groups_results_by_namespace() {
description: "List calendar events.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: crate::tools::spec::JsonSchema::Object {
parameters: codex_tools::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
@@ -134,7 +134,7 @@ fn serialize_tool_search_output_tools_groups_results_by_namespace() {
description: "Read an email.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: crate::tools::spec::JsonSchema::Object {
parameters: codex_tools::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
@@ -187,7 +187,7 @@ fn serialize_tool_search_output_tools_falls_back_to_connector_name_description()
description: "Read multiple emails.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: crate::tools::spec::JsonSchema::Object {
parameters: codex_tools::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
+2 -3
View File
@@ -5,7 +5,6 @@ use crate::shell::Shell;
use crate::shell::ShellType;
use crate::tools::code_mode::PUBLIC_TOOL_NAME;
use crate::tools::code_mode::WAIT_TOOL_NAME;
use crate::tools::handlers::PLAN_TOOL;
use crate::tools::handlers::TOOL_SEARCH_DEFAULT_LIMIT;
use crate::tools::handlers::TOOL_SEARCH_TOOL_NAME;
use crate::tools::handlers::TOOL_SUGGEST_TOOL_NAME;
@@ -59,6 +58,7 @@ use codex_tools::create_spawn_agents_on_csv_tool;
use codex_tools::create_test_sync_tool;
use codex_tools::create_tool_search_tool;
use codex_tools::create_tool_suggest_tool;
use codex_tools::create_update_plan_tool;
use codex_tools::create_view_image_tool;
use codex_tools::create_wait_agent_tool_v1;
use codex_tools::create_wait_agent_tool_v2;
@@ -71,7 +71,6 @@ use codex_tools::request_user_input_tool_description;
use codex_tools::tool_spec_to_code_mode_tool_definition;
use std::collections::HashMap;
pub type JsonSchema = codex_tools::JsonSchema;
pub use codex_tools::ShellCommandBackendConfig;
pub use codex_tools::ToolsConfig;
pub use codex_tools::ToolsConfigParams;
@@ -318,7 +317,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
push_tool_spec(
&mut builder,
PLAN_TOOL.clone(),
create_update_plan_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
+3 -1
View File
@@ -24,6 +24,7 @@ use codex_tools::ConfiguredToolSpec;
use codex_tools::DiscoverablePluginInfo;
use codex_tools::DiscoverableTool;
use codex_tools::FreeformTool;
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiTool;
use codex_tools::ResponsesApiWebSearchFilters;
use codex_tools::ResponsesApiWebSearchUserLocation;
@@ -41,6 +42,7 @@ use codex_tools::create_send_input_tool_v1;
use codex_tools::create_send_message_tool;
use codex_tools::create_spawn_agent_tool_v1;
use codex_tools::create_spawn_agent_tool_v2;
use codex_tools::create_update_plan_tool;
use codex_tools::create_view_image_tool;
use codex_tools::create_wait_agent_tool_v1;
use codex_tools::create_wait_agent_tool_v2;
@@ -331,7 +333,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
exec_permission_approvals_enabled: false,
}),
create_write_stdin_tool(),
PLAN_TOOL.clone(),
create_update_plan_tool(),
request_user_input_tool_spec(/*default_mode_request_user_input*/ false),
create_apply_patch_freeform_tool(),
ToolSpec::WebSearch {
+2
View File
@@ -12,6 +12,7 @@ mod json_schema;
mod local_tool;
mod mcp_resource_tool;
mod mcp_tool;
mod plan_tool;
mod request_user_input_tool;
mod responses_api;
mod tool_config;
@@ -64,6 +65,7 @@ pub use mcp_resource_tool::create_list_mcp_resources_tool;
pub use mcp_resource_tool::create_read_mcp_resource_tool;
pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::parse_mcp_tool;
pub use plan_tool::create_update_plan_tool;
pub use request_user_input_tool::create_request_user_input_tool;
pub use request_user_input_tool::request_user_input_tool_description;
pub use request_user_input_tool::request_user_input_unavailable_message;
+51
View File
@@ -0,0 +1,51 @@
use crate::JsonSchema;
use crate::ResponsesApiTool;
use crate::ToolSpec;
use std::collections::BTreeMap;
pub fn create_update_plan_tool() -> ToolSpec {
let plan_item_properties = BTreeMap::from([
("step".to_string(), JsonSchema::String { description: None }),
(
"status".to_string(),
JsonSchema::String {
description: Some("One of: pending, in_progress, completed".to_string()),
},
),
]);
let properties = BTreeMap::from([
(
"explanation".to_string(),
JsonSchema::String { description: None },
),
(
"plan".to_string(),
JsonSchema::Array {
description: Some("The list of steps".to_string()),
items: Box::new(JsonSchema::Object {
properties: plan_item_properties,
required: Some(vec!["step".to_string(), "status".to_string()]),
additional_properties: Some(false.into()),
}),
},
),
]);
ToolSpec::Function(ResponsesApiTool {
name: "update_plan".to_string(),
description: r#"Updates the task plan.
Provide an optional explanation and a list of plan items, each with a step and status.
At most one step can be in_progress at a time.
"#
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["plan".to_string()]),
additional_properties: Some(false.into()),
},
output_schema: None,
})
}