Files
codex/codex-rs/tools/src/tool_registry_plan.rs
T
Eric Traut 32ace07ac5 Add goal model tools (3 / 5) (#18075)
Adds the model-facing goal tools on top of the app-server API from PR 2.

## Why

Once goals are persisted and exposed to clients, the model needs a
small, constrained tool surface for goal workflows. The tool contract
should let the model inspect goals, create them only when explicitly
requested, and mark them complete without giving it broad control over
user/runtime-owned state.

## What changed

- Added `get_goal`, `create_goal`, and `update_goal` tool specs behind
the `goals` feature flag.
- Added core goal tool handlers that validate objectives and token
budgets before mutating persisted state.
- Constrained `create_goal` to create only when no goal exists, with
optional `token_budget` only when a budget is explicitly provided.
- Tightened the `create_goal` instructions so the model does not infer
goals from ordinary task requests.
- Constrained `update_goal` to expose only goal completion; pause,
resume, clear, and budget-limited transitions remain user- or
runtime-controlled.
- Registered the goal tools in the tool registry and kept them out of
review contexts where they should not appear.

## Verification

- Added tool-registry coverage for feature gating and tool availability.
- Added core session tests for create/get/update behavior, duplicate
goal rejection, budget validation, and completion-only updates.
2026-04-24 20:54:40 -07:00

623 lines
23 KiB
Rust

use crate::CommandToolOptions;
use crate::REQUEST_USER_INPUT_TOOL_NAME;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiNamespaceTool;
use crate::ShellToolOptions;
use crate::SpawnAgentToolOptions;
use crate::TOOL_SEARCH_DEFAULT_LIMIT;
use crate::TOOL_SEARCH_TOOL_NAME;
use crate::TOOL_SUGGEST_TOOL_NAME;
use crate::ToolHandlerKind;
use crate::ToolName;
use crate::ToolRegistryPlan;
use crate::ToolRegistryPlanParams;
use crate::ToolSearchSource;
use crate::ToolSearchSourceInfo;
use crate::ToolSpec;
use crate::ToolsConfig;
use crate::ViewImageToolOptions;
use crate::WebSearchToolOptions;
use crate::coalesce_loadable_tool_specs;
use crate::collect_code_mode_exec_prompt_tool_definitions;
use crate::collect_tool_search_source_infos;
use crate::collect_tool_suggest_entries;
use crate::create_apply_patch_freeform_tool;
use crate::create_apply_patch_json_tool;
use crate::create_close_agent_tool_v1;
use crate::create_close_agent_tool_v2;
use crate::create_code_mode_tool;
use crate::create_create_goal_tool;
use crate::create_exec_command_tool;
use crate::create_followup_task_tool;
use crate::create_get_goal_tool;
use crate::create_image_generation_tool;
use crate::create_list_agents_tool;
use crate::create_list_dir_tool;
use crate::create_list_mcp_resource_templates_tool;
use crate::create_list_mcp_resources_tool;
use crate::create_local_shell_tool;
use crate::create_read_mcp_resource_tool;
use crate::create_report_agent_job_result_tool;
use crate::create_request_permissions_tool;
use crate::create_request_user_input_tool;
use crate::create_resume_agent_tool;
use crate::create_send_input_tool_v1;
use crate::create_send_message_tool;
use crate::create_shell_command_tool;
use crate::create_shell_tool;
use crate::create_spawn_agent_tool_v1;
use crate::create_spawn_agent_tool_v2;
use crate::create_spawn_agents_on_csv_tool;
use crate::create_test_sync_tool;
use crate::create_tool_search_tool;
use crate::create_tool_suggest_tool;
use crate::create_update_goal_tool;
use crate::create_update_plan_tool;
use crate::create_view_image_tool;
use crate::create_wait_agent_tool_v1;
use crate::create_wait_agent_tool_v2;
use crate::create_wait_tool;
use crate::create_web_search_tool;
use crate::create_write_stdin_tool;
use crate::default_namespace_description;
use crate::dynamic_tool_to_loadable_tool_spec;
use crate::mcp_tool_to_responses_api_tool;
use crate::request_permissions_tool_description;
use crate::request_user_input_tool_description;
use crate::tool_registry_plan_types::agent_type_description;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType;
use std::collections::BTreeMap;
pub fn build_tool_registry_plan(
config: &ToolsConfig,
params: ToolRegistryPlanParams<'_>,
) -> ToolRegistryPlan {
let mut plan = ToolRegistryPlan::new();
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
if config.code_mode_enabled {
let namespace_descriptions = params
.tool_namespaces
.into_iter()
.flatten()
.map(|(namespace, detail)| {
(
namespace.clone(),
codex_code_mode::ToolNamespaceDescription {
name: detail.name.clone(),
description: detail.description.clone().unwrap_or_default(),
},
)
})
.collect::<BTreeMap<_, _>>();
let nested_config = config.for_code_mode_nested_tools();
let nested_plan = build_tool_registry_plan(
&nested_config,
ToolRegistryPlanParams {
discoverable_tools: None,
..params
},
);
let mut enabled_tools = collect_code_mode_exec_prompt_tool_definitions(
nested_plan
.specs
.iter()
.map(|configured_tool| &configured_tool.spec),
);
enabled_tools
.sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions));
plan.push_spec(
create_code_mode_tool(
&enabled_tools,
&namespace_descriptions,
config.code_mode_only_enabled,
config.search_tool
&& params
.deferred_mcp_tools
.is_some_and(|tools| !tools.is_empty()),
),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler(
codex_code_mode::PUBLIC_TOOL_NAME,
ToolHandlerKind::CodeModeExecute,
);
plan.push_spec(
create_wait_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler(
codex_code_mode::WAIT_TOOL_NAME,
ToolHandlerKind::CodeModeWait,
);
}
if config.has_environment {
match &config.shell_type {
ConfigShellToolType::Default => {
plan.push_spec(
create_shell_tool(ShellToolOptions {
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
ConfigShellToolType::Local => {
plan.push_spec(
create_local_shell_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
ConfigShellToolType::UnifiedExec => {
plan.push_spec(
create_exec_command_tool(CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.push_spec(
create_write_stdin_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("exec_command", ToolHandlerKind::UnifiedExec);
plan.register_handler("write_stdin", ToolHandlerKind::UnifiedExec);
}
ConfigShellToolType::Disabled => {}
ConfigShellToolType::ShellCommand => {
plan.push_spec(
create_shell_command_tool(CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
}
}
if config.has_environment && config.shell_type != ConfigShellToolType::Disabled {
plan.register_handler("shell", ToolHandlerKind::Shell);
plan.register_handler("container.exec", ToolHandlerKind::Shell);
plan.register_handler("local_shell", ToolHandlerKind::Shell);
plan.register_handler("shell_command", ToolHandlerKind::ShellCommand);
}
if params.mcp_tools.is_some() {
plan.push_spec(
create_list_mcp_resources_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.push_spec(
create_list_mcp_resource_templates_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.push_spec(
create_read_mcp_resource_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.register_handler("list_mcp_resources", ToolHandlerKind::McpResource);
plan.register_handler("list_mcp_resource_templates", ToolHandlerKind::McpResource);
plan.register_handler("read_mcp_resource", ToolHandlerKind::McpResource);
}
plan.push_spec(
create_update_plan_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("update_plan", ToolHandlerKind::Plan);
if config.goal_tools {
plan.push_spec(
create_get_goal_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("get_goal", ToolHandlerKind::Goal);
plan.push_spec(
create_create_goal_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("create_goal", ToolHandlerKind::Goal);
plan.push_spec(
create_update_goal_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("update_goal", ToolHandlerKind::Goal);
}
plan.push_spec(
create_request_user_input_tool(request_user_input_tool_description(
config.default_mode_request_user_input,
)),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler(
REQUEST_USER_INPUT_TOOL_NAME,
ToolHandlerKind::RequestUserInput,
);
if config.request_permissions_tool_enabled {
plan.push_spec(
create_request_permissions_tool(request_permissions_tool_description()),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("request_permissions", ToolHandlerKind::RequestPermissions);
}
let deferred_dynamic_tools = params
.dynamic_tools
.iter()
.filter(|tool| tool.defer_loading)
.collect::<Vec<_>>();
if config.search_tool
&& (params.deferred_mcp_tools.is_some() || !deferred_dynamic_tools.is_empty())
{
let mut search_source_infos = params
.deferred_mcp_tools
.map(|deferred_mcp_tools| {
collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| {
ToolSearchSource {
server_name: tool.server_name,
connector_name: tool.connector_name,
connector_description: tool.connector_description,
}
}))
})
.unwrap_or_default();
if !deferred_dynamic_tools.is_empty() {
search_source_infos.push(ToolSearchSourceInfo {
name: "Dynamic tools".to_string(),
description: Some("Tools provided by the current Codex thread.".to_string()),
});
}
plan.push_spec(
create_tool_search_tool(&search_source_infos, TOOL_SEARCH_DEFAULT_LIMIT),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch);
if let Some(deferred_mcp_tools) = params.deferred_mcp_tools {
for tool in deferred_mcp_tools {
plan.register_handler(tool.name.clone(), ToolHandlerKind::Mcp);
}
}
}
if config.tool_suggest
&& let Some(discoverable_tools) =
params.discoverable_tools.filter(|tools| !tools.is_empty())
{
plan.push_spec(
create_tool_suggest_tool(&collect_tool_suggest_entries(discoverable_tools)),
/*supports_parallel_tool_calls*/ true,
/*code_mode_enabled*/ false,
);
plan.register_handler(TOOL_SUGGEST_TOOL_NAME, ToolHandlerKind::ToolSuggest);
}
if config.has_environment
&& let Some(apply_patch_tool_type) = &config.apply_patch_tool_type
{
match apply_patch_tool_type {
ApplyPatchToolType::Freeform => {
plan.push_spec(
create_apply_patch_freeform_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
ApplyPatchToolType::Function => {
plan.push_spec(
create_apply_patch_json_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
}
plan.register_handler("apply_patch", ToolHandlerKind::ApplyPatch);
}
if config.has_environment
&& config
.experimental_supported_tools
.iter()
.any(|tool| tool == "list_dir")
{
plan.push_spec(
create_list_dir_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.register_handler("list_dir", ToolHandlerKind::ListDir);
}
if config
.experimental_supported_tools
.iter()
.any(|tool| tool == "test_sync_tool")
{
plan.push_spec(
create_test_sync_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.register_handler("test_sync_tool", ToolHandlerKind::TestSync);
}
if let Some(web_search_tool) = create_web_search_tool(WebSearchToolOptions {
web_search_mode: config.web_search_mode,
web_search_config: config.web_search_config.as_ref(),
web_search_tool_type: config.web_search_tool_type,
}) {
plan.push_spec(
web_search_tool,
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
if config.image_gen_tool {
plan.push_spec(
create_image_generation_tool("png"),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
if config.has_environment {
plan.push_spec(
create_view_image_tool(ViewImageToolOptions {
can_request_original_image_detail: config.can_request_original_image_detail,
}),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.register_handler("view_image", ToolHandlerKind::ViewImage);
}
if config.collab_tools {
if config.multi_agent_v2 {
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
plan.push_spec(
create_spawn_agent_tool_v2(SpawnAgentToolOptions {
available_models: &config.available_models,
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.push_spec(
create_send_message_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.push_spec(
create_followup_task_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.push_spec(
create_wait_agent_tool_v2(params.wait_agent_timeouts),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.push_spec(
create_close_agent_tool_v2(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.push_spec(
create_list_agents_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("spawn_agent", ToolHandlerKind::SpawnAgentV2);
plan.register_handler("send_message", ToolHandlerKind::SendMessageV2);
plan.register_handler("followup_task", ToolHandlerKind::FollowupTaskV2);
plan.register_handler("wait_agent", ToolHandlerKind::WaitAgentV2);
plan.register_handler("close_agent", ToolHandlerKind::CloseAgentV2);
plan.register_handler("list_agents", ToolHandlerKind::ListAgentsV2);
} else {
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
plan.push_spec(
create_spawn_agent_tool_v1(SpawnAgentToolOptions {
available_models: &config.available_models,
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.push_spec(
create_send_input_tool_v1(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.push_spec(
create_resume_agent_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("resume_agent", ToolHandlerKind::ResumeAgentV1);
plan.push_spec(
create_wait_agent_tool_v1(params.wait_agent_timeouts),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.push_spec(
create_close_agent_tool_v1(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("spawn_agent", ToolHandlerKind::SpawnAgentV1);
plan.register_handler("send_input", ToolHandlerKind::SendInputV1);
plan.register_handler("wait_agent", ToolHandlerKind::WaitAgentV1);
plan.register_handler("close_agent", ToolHandlerKind::CloseAgentV1);
}
}
if config.agent_jobs_tools {
plan.push_spec(
create_spawn_agents_on_csv_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("spawn_agents_on_csv", ToolHandlerKind::AgentJobs);
if config.agent_jobs_worker_tools {
plan.push_spec(
create_report_agent_job_result_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
plan.register_handler("report_agent_job_result", ToolHandlerKind::AgentJobs);
}
}
if let Some(mcp_tools) = params.mcp_tools {
let mut entries = mcp_tools.to_vec();
entries.sort_by_key(|tool| tool.name.display());
let mut namespace_entries = BTreeMap::new();
for tool in entries {
let Some(namespace) = tool.name.namespace.as_ref() else {
let tool_name = &tool.name;
tracing::error!("Skipping MCP tool `{tool_name}`: MCP tools must be namespaced");
continue;
};
namespace_entries
.entry(namespace.clone())
.or_insert_with(Vec::new)
.push(tool);
}
for (namespace, mut entries) in namespace_entries {
entries.sort_by_key(|tool| tool.name.name.clone());
let tool_namespace = params
.tool_namespaces
.and_then(|namespaces| namespaces.get(&namespace));
let description = tool_namespace
.and_then(|namespace| namespace.description.as_deref())
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string)
.unwrap_or_else(|| {
let namespace_name = tool_namespace
.map(|namespace| namespace.name.as_str())
.unwrap_or(namespace.as_str());
default_namespace_description(namespace_name)
});
let mut tools = Vec::new();
for tool in entries {
match mcp_tool_to_responses_api_tool(&tool.name, tool.tool) {
Ok(converted_tool) => {
tools.push(ResponsesApiNamespaceTool::Function(converted_tool));
plan.register_handler(tool.name, ToolHandlerKind::Mcp);
}
Err(error) => {
let tool_name = &tool.name;
tracing::error!(
"Failed to convert `{tool_name}` MCP tool to OpenAI tool: {error:?}"
);
}
}
}
if !tools.is_empty() {
plan.push_spec(
ToolSpec::Namespace(ResponsesApiNamespace {
name: namespace,
description,
tools,
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
}
}
let mut dynamic_tool_specs = Vec::new();
for tool in params.dynamic_tools {
match dynamic_tool_to_loadable_tool_spec(tool) {
Ok(loadable_tool) => {
let handler_name = ToolName::new(tool.namespace.clone(), tool.name.clone());
dynamic_tool_specs.push(loadable_tool);
plan.register_handler(handler_name, ToolHandlerKind::DynamicTool);
}
Err(error) => {
tracing::error!(
"Failed to convert dynamic tool {:?} to OpenAI tool: {error:?}",
tool.name
);
}
}
}
for spec in coalesce_loadable_tool_specs(dynamic_tool_specs) {
plan.push_spec(
spec.into(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
plan
}
fn compare_code_mode_tools(
left: &codex_code_mode::ToolDefinition,
right: &codex_code_mode::ToolDefinition,
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
) -> std::cmp::Ordering {
let left_namespace = code_mode_namespace_name(left, namespace_descriptions);
let right_namespace = code_mode_namespace_name(right, namespace_descriptions);
left_namespace
.cmp(&right_namespace)
.then_with(|| left.tool_name.name.cmp(&right.tool_name.name))
.then_with(|| left.name.cmp(&right.name))
}
fn code_mode_namespace_name<'a>(
tool: &codex_code_mode::ToolDefinition,
namespace_descriptions: &'a BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
) -> Option<&'a str> {
tool.tool_name
.namespace
.as_ref()
.and_then(|namespace| namespace_descriptions.get(namespace))
.map(|namespace_description| namespace_description.name.as_str())
}
#[cfg(test)]
#[path = "tool_registry_plan_tests.rs"]
mod tests;