mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
## Why `core/src/tools/spec.rs` still bundled a set of pure local-host tool builders with the orchestration that actually decides when those tools are exposed and which handlers back them. That made `codex-core` responsible for JSON/tool-shape construction that does not depend on session state, and it kept the `codex-tools` migration from taking a meaningfully larger bite out of `spec.rs`. This PR moves that reusable spec-building layer into `codex-tools` while leaving feature gating, handler registration, and runtime-coupled descriptions in `codex-core`. ## What changed - added `codex-rs/tools/src/local_tool.rs` for the pure builders for `exec_command`, `write_stdin`, `shell`, `shell_command`, and `request_permissions` - added `codex-rs/tools/src/view_image.rs` for the `view_image` tool spec and output schema so the extracted modules stay right-sized - rewired `codex-rs/core/src/tools/spec.rs` to call those extracted builders instead of constructing these specs inline - kept the `request_permissions` description source in `codex-core`, with `codex-tools` taking the description as input so the crate boundary does not grow a dependency on handler/runtime code - moved the direct constructor coverage for this slice from `codex-rs/core/src/tools/spec_tests.rs` into `codex-rs/tools/src/local_tool_tests.rs` and `codex-rs/tools/src/view_image_tests.rs` - updated `codex-rs/tools/README.md` to reflect that `codex-tools` now owns this local-host spec layer ## Test plan - `CARGO_TARGET_DIR=/tmp/codex-tools-local-host cargo test -p codex-tools` - `CARGO_TARGET_DIR=/tmp/codex-core-local-tools cargo test -p codex-core --lib tools::spec::` - `just argument-comment-lint` ## References - #15923 - #15928 - #15944 - #15953 - #16031 - #16047 - #16129 - #16132
2410 lines
88 KiB
Rust
2410 lines
88 KiB
Rust
use crate::client_common::tools::ToolSpec;
|
|
use crate::config::AgentRoleConfig;
|
|
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
|
use crate::mcp_connection_manager::ToolInfo;
|
|
use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
|
use crate::original_image_detail::can_request_original_image_detail;
|
|
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::discoverable::DiscoverablePluginInfo;
|
|
use crate::tools::discoverable::DiscoverableTool;
|
|
use crate::tools::discoverable::DiscoverableToolAction;
|
|
use crate::tools::discoverable::DiscoverableToolType;
|
|
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;
|
|
use crate::tools::handlers::agent_jobs::BatchJobHandler;
|
|
use crate::tools::handlers::apply_patch::create_apply_patch_freeform_tool;
|
|
use crate::tools::handlers::apply_patch::create_apply_patch_json_tool;
|
|
use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS;
|
|
use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS;
|
|
use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS;
|
|
use crate::tools::handlers::request_permissions_tool_description;
|
|
use crate::tools::handlers::request_user_input_tool_description;
|
|
use crate::tools::registry::ToolRegistryBuilder;
|
|
use crate::tools::registry::tool_handler_key;
|
|
use codex_features::Feature;
|
|
use codex_features::Features;
|
|
use codex_protocol::config_types::WebSearchConfig;
|
|
use codex_protocol::config_types::WebSearchMode;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
|
use codex_protocol::openai_models::ApplyPatchToolType;
|
|
use codex_protocol::openai_models::ConfigShellToolType;
|
|
use codex_protocol::openai_models::InputModality;
|
|
use codex_protocol::openai_models::ModelInfo;
|
|
use codex_protocol::openai_models::ModelPreset;
|
|
use codex_protocol::openai_models::WebSearchToolType;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::SubAgentSource;
|
|
use codex_tools::CommandToolOptions;
|
|
use codex_tools::FreeformTool;
|
|
use codex_tools::FreeformToolFormat;
|
|
use codex_tools::ResponsesApiTool;
|
|
use codex_tools::ShellToolOptions;
|
|
use codex_tools::ViewImageToolOptions;
|
|
use codex_tools::augment_tool_spec_for_code_mode;
|
|
use codex_tools::create_exec_command_tool;
|
|
use codex_tools::create_request_permissions_tool;
|
|
use codex_tools::create_shell_command_tool;
|
|
use codex_tools::create_shell_tool;
|
|
use codex_tools::create_view_image_tool;
|
|
use codex_tools::create_write_stdin_tool;
|
|
use codex_tools::dynamic_tool_to_responses_api_tool;
|
|
use codex_tools::mcp_tool_to_responses_api_tool;
|
|
use codex_tools::tool_spec_to_code_mode_tool_definition;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_template::Template;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use serde_json::Value as JsonValue;
|
|
use serde_json::json;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::LazyLock;
|
|
|
|
pub type JsonSchema = codex_tools::JsonSchema;
|
|
|
|
#[cfg(test)]
|
|
pub(crate) use codex_tools::mcp_call_tool_result_output_schema;
|
|
|
|
const TOOL_SEARCH_DESCRIPTION_TEMPLATE_SOURCE: &str =
|
|
include_str!("../../templates/search_tool/tool_description.md");
|
|
const TOOL_SEARCH_DESCRIPTION_TEMPLATE_KEY: &str = "app_descriptions";
|
|
static TOOL_SEARCH_DESCRIPTION_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
|
|
Template::parse(TOOL_SEARCH_DESCRIPTION_TEMPLATE_SOURCE)
|
|
.unwrap_or_else(|err| panic!("tool_search description template must parse: {err}"))
|
|
});
|
|
const TOOL_SUGGEST_DESCRIPTION_TEMPLATE_SOURCE: &str =
|
|
include_str!("../../templates/search_tool/tool_suggest_description.md");
|
|
const TOOL_SUGGEST_DESCRIPTION_TEMPLATE_KEY: &str = "discoverable_tools";
|
|
static TOOL_SUGGEST_DESCRIPTION_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
|
|
Template::parse(TOOL_SUGGEST_DESCRIPTION_TEMPLATE_SOURCE)
|
|
.unwrap_or_else(|err| panic!("tool_suggest description template must parse: {err}"))
|
|
});
|
|
const WEB_SEARCH_CONTENT_TYPES: [&str; 2] = ["text", "image"];
|
|
|
|
fn agent_status_output_schema() -> JsonValue {
|
|
json!({
|
|
"oneOf": [
|
|
{
|
|
"type": "string",
|
|
"enum": ["pending_init", "running", "shutdown", "not_found"]
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"completed": {
|
|
"type": ["string", "null"]
|
|
}
|
|
},
|
|
"required": ["completed"],
|
|
"additionalProperties": false
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"errored": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"required": ["errored"],
|
|
"additionalProperties": false
|
|
}
|
|
]
|
|
})
|
|
}
|
|
|
|
fn spawn_agent_output_schema_v1() -> JsonValue {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"agent_id": {
|
|
"type": "string",
|
|
"description": "Thread identifier for the spawned agent."
|
|
},
|
|
"nickname": {
|
|
"type": ["string", "null"],
|
|
"description": "User-facing nickname for the spawned agent when available."
|
|
}
|
|
},
|
|
"required": ["agent_id", "nickname"],
|
|
"additionalProperties": false
|
|
})
|
|
}
|
|
|
|
fn spawn_agent_output_schema_v2() -> JsonValue {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"agent_id": {
|
|
"type": ["string", "null"],
|
|
"description": "Legacy thread identifier for the spawned agent."
|
|
},
|
|
"task_name": {
|
|
"type": "string",
|
|
"description": "Canonical task name for the spawned agent."
|
|
},
|
|
"nickname": {
|
|
"type": ["string", "null"],
|
|
"description": "User-facing nickname for the spawned agent when available."
|
|
}
|
|
},
|
|
"required": ["agent_id", "task_name", "nickname"],
|
|
"additionalProperties": false
|
|
})
|
|
}
|
|
|
|
fn send_input_output_schema() -> JsonValue {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"submission_id": {
|
|
"type": "string",
|
|
"description": "Identifier for the queued input submission."
|
|
}
|
|
},
|
|
"required": ["submission_id"],
|
|
"additionalProperties": false
|
|
})
|
|
}
|
|
|
|
fn list_agents_output_schema() -> JsonValue {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"agents": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"agent_name": {
|
|
"type": "string",
|
|
"description": "Canonical task name for the agent when available, otherwise the agent id."
|
|
},
|
|
"agent_status": {
|
|
"description": "Last known status of the agent.",
|
|
"allOf": [agent_status_output_schema()]
|
|
},
|
|
"last_task_message": {
|
|
"type": ["string", "null"],
|
|
"description": "Most recent user or inter-agent instruction received by the agent, when available."
|
|
}
|
|
},
|
|
"required": ["agent_name", "agent_status", "last_task_message"],
|
|
"additionalProperties": false
|
|
},
|
|
"description": "Live agents visible in the current root thread tree."
|
|
}
|
|
},
|
|
"required": ["agents"],
|
|
"additionalProperties": false
|
|
})
|
|
}
|
|
|
|
fn resume_agent_output_schema() -> JsonValue {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"status": agent_status_output_schema()
|
|
},
|
|
"required": ["status"],
|
|
"additionalProperties": false
|
|
})
|
|
}
|
|
|
|
fn wait_output_schema_v1() -> JsonValue {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"status": {
|
|
"type": "object",
|
|
"description": "Final statuses keyed by agent id.",
|
|
"additionalProperties": agent_status_output_schema()
|
|
},
|
|
"timed_out": {
|
|
"type": "boolean",
|
|
"description": "Whether the wait call returned due to timeout before any agent reached a final status."
|
|
}
|
|
},
|
|
"required": ["status", "timed_out"],
|
|
"additionalProperties": false
|
|
})
|
|
}
|
|
|
|
fn wait_output_schema_v2() -> JsonValue {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"message": {
|
|
"type": "string",
|
|
"description": "Brief wait summary without the agent's final content."
|
|
},
|
|
"timed_out": {
|
|
"type": "boolean",
|
|
"description": "Whether the wait call returned due to timeout before any agent reached a final status."
|
|
}
|
|
},
|
|
"required": ["message", "timed_out"],
|
|
"additionalProperties": false
|
|
})
|
|
}
|
|
|
|
fn close_agent_output_schema() -> JsonValue {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"previous_status": {
|
|
"description": "The agent status observed before shutdown was requested.",
|
|
"allOf": [agent_status_output_schema()]
|
|
}
|
|
},
|
|
"required": ["previous_status"],
|
|
"additionalProperties": false
|
|
})
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
pub enum ShellCommandBackendConfig {
|
|
Classic,
|
|
ZshFork,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub enum UnifiedExecShellMode {
|
|
Direct,
|
|
ZshFork(ZshForkConfig),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub struct ZshForkConfig {
|
|
pub(crate) shell_zsh_path: AbsolutePathBuf,
|
|
pub(crate) main_execve_wrapper_exe: AbsolutePathBuf,
|
|
}
|
|
|
|
impl UnifiedExecShellMode {
|
|
pub fn for_session(
|
|
shell_command_backend: ShellCommandBackendConfig,
|
|
user_shell: &Shell,
|
|
shell_zsh_path: Option<&PathBuf>,
|
|
main_execve_wrapper_exe: Option<&PathBuf>,
|
|
) -> Self {
|
|
if cfg!(unix)
|
|
&& shell_command_backend == ShellCommandBackendConfig::ZshFork
|
|
&& matches!(user_shell.shell_type, ShellType::Zsh)
|
|
&& let (Some(shell_zsh_path), Some(main_execve_wrapper_exe)) =
|
|
(shell_zsh_path, main_execve_wrapper_exe)
|
|
&& let (Ok(shell_zsh_path), Ok(main_execve_wrapper_exe)) = (
|
|
AbsolutePathBuf::try_from(shell_zsh_path.as_path())
|
|
.inspect_err(|e| tracing::warn!("Failed to convert shell_zsh_path `{shell_zsh_path:?}`: {e:?}")),
|
|
AbsolutePathBuf::try_from(main_execve_wrapper_exe.as_path()).inspect_err(|e| {
|
|
tracing::warn!("Failed to convert main_execve_wrapper_exe `{main_execve_wrapper_exe:?}`: {e:?}")
|
|
}),
|
|
)
|
|
{
|
|
Self::ZshFork(ZshForkConfig {
|
|
shell_zsh_path,
|
|
main_execve_wrapper_exe,
|
|
})
|
|
} else {
|
|
Self::Direct
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct ToolsConfig {
|
|
pub available_models: Vec<ModelPreset>,
|
|
pub shell_type: ConfigShellToolType,
|
|
shell_command_backend: ShellCommandBackendConfig,
|
|
pub unified_exec_shell_mode: UnifiedExecShellMode,
|
|
pub allow_login_shell: bool,
|
|
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
|
pub web_search_mode: Option<WebSearchMode>,
|
|
pub web_search_config: Option<WebSearchConfig>,
|
|
pub web_search_tool_type: WebSearchToolType,
|
|
pub image_gen_tool: bool,
|
|
pub agent_roles: BTreeMap<String, AgentRoleConfig>,
|
|
pub search_tool: bool,
|
|
pub tool_suggest: bool,
|
|
pub exec_permission_approvals_enabled: bool,
|
|
pub request_permissions_tool_enabled: bool,
|
|
pub code_mode_enabled: bool,
|
|
pub code_mode_only_enabled: bool,
|
|
pub js_repl_enabled: bool,
|
|
pub js_repl_tools_only: bool,
|
|
pub can_request_original_image_detail: bool,
|
|
pub collab_tools: bool,
|
|
pub multi_agent_v2: bool,
|
|
pub request_user_input: bool,
|
|
pub default_mode_request_user_input: bool,
|
|
pub experimental_supported_tools: Vec<String>,
|
|
pub agent_jobs_tools: bool,
|
|
pub agent_jobs_worker_tools: bool,
|
|
}
|
|
|
|
pub(crate) struct ToolsConfigParams<'a> {
|
|
pub(crate) model_info: &'a ModelInfo,
|
|
pub(crate) available_models: &'a Vec<ModelPreset>,
|
|
pub(crate) features: &'a Features,
|
|
pub(crate) web_search_mode: Option<WebSearchMode>,
|
|
pub(crate) session_source: SessionSource,
|
|
pub(crate) sandbox_policy: &'a SandboxPolicy,
|
|
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
|
|
}
|
|
|
|
fn unified_exec_allowed_in_environment(
|
|
is_windows: bool,
|
|
sandbox_policy: &SandboxPolicy,
|
|
windows_sandbox_level: WindowsSandboxLevel,
|
|
) -> bool {
|
|
!(is_windows
|
|
&& windows_sandbox_level != WindowsSandboxLevel::Disabled
|
|
&& !matches!(
|
|
sandbox_policy,
|
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
|
))
|
|
}
|
|
|
|
impl ToolsConfig {
|
|
pub fn new(params: &ToolsConfigParams) -> Self {
|
|
let ToolsConfigParams {
|
|
model_info,
|
|
available_models: available_models_ref,
|
|
features,
|
|
web_search_mode,
|
|
session_source,
|
|
sandbox_policy,
|
|
windows_sandbox_level,
|
|
} = params;
|
|
let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform);
|
|
let include_code_mode = features.enabled(Feature::CodeMode);
|
|
let include_code_mode_only = include_code_mode && features.enabled(Feature::CodeModeOnly);
|
|
let include_js_repl = features.enabled(Feature::JsRepl);
|
|
let include_js_repl_tools_only =
|
|
include_js_repl && features.enabled(Feature::JsReplToolsOnly);
|
|
let include_collab_tools = features.enabled(Feature::Collab);
|
|
let include_multi_agent_v2 = features.enabled(Feature::MultiAgentV2);
|
|
let include_agent_jobs = features.enabled(Feature::SpawnCsv);
|
|
let include_request_user_input = !matches!(session_source, SessionSource::SubAgent(_));
|
|
let include_default_mode_request_user_input =
|
|
include_request_user_input && features.enabled(Feature::DefaultModeRequestUserInput);
|
|
let include_search_tool =
|
|
model_info.supports_search_tool && features.enabled(Feature::ToolSearch);
|
|
let include_tool_suggest = features.enabled(Feature::ToolSuggest)
|
|
&& features.enabled(Feature::Apps)
|
|
&& features.enabled(Feature::Plugins);
|
|
let include_original_image_detail = can_request_original_image_detail(features, model_info);
|
|
let include_image_gen_tool =
|
|
features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info);
|
|
let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals);
|
|
let request_permissions_tool_enabled = features.enabled(Feature::RequestPermissionsTool);
|
|
let shell_command_backend =
|
|
if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) {
|
|
ShellCommandBackendConfig::ZshFork
|
|
} else {
|
|
ShellCommandBackendConfig::Classic
|
|
};
|
|
let unified_exec_allowed = unified_exec_allowed_in_environment(
|
|
cfg!(target_os = "windows"),
|
|
sandbox_policy,
|
|
*windows_sandbox_level,
|
|
);
|
|
let shell_type = if !features.enabled(Feature::ShellTool) {
|
|
ConfigShellToolType::Disabled
|
|
} else if features.enabled(Feature::ShellZshFork) {
|
|
ConfigShellToolType::ShellCommand
|
|
} else if features.enabled(Feature::UnifiedExec) && unified_exec_allowed {
|
|
// If ConPTY not supported (for old Windows versions), fallback on ShellCommand.
|
|
if codex_utils_pty::conpty_supported() {
|
|
ConfigShellToolType::UnifiedExec
|
|
} else {
|
|
ConfigShellToolType::ShellCommand
|
|
}
|
|
} else if model_info.shell_type == ConfigShellToolType::UnifiedExec && !unified_exec_allowed
|
|
{
|
|
ConfigShellToolType::ShellCommand
|
|
} else {
|
|
model_info.shell_type
|
|
};
|
|
|
|
let apply_patch_tool_type = match model_info.apply_patch_tool_type {
|
|
Some(ApplyPatchToolType::Freeform) => Some(ApplyPatchToolType::Freeform),
|
|
Some(ApplyPatchToolType::Function) => Some(ApplyPatchToolType::Function),
|
|
None => {
|
|
if include_apply_patch_tool {
|
|
Some(ApplyPatchToolType::Freeform)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
};
|
|
|
|
let agent_jobs_worker_tools = include_agent_jobs
|
|
&& matches!(
|
|
session_source,
|
|
SessionSource::SubAgent(SubAgentSource::Other(label))
|
|
if label.starts_with("agent_job:")
|
|
);
|
|
|
|
Self {
|
|
available_models: available_models_ref.to_vec(),
|
|
shell_type,
|
|
shell_command_backend,
|
|
unified_exec_shell_mode: UnifiedExecShellMode::Direct,
|
|
allow_login_shell: true,
|
|
apply_patch_tool_type,
|
|
web_search_mode: *web_search_mode,
|
|
web_search_config: None,
|
|
web_search_tool_type: model_info.web_search_tool_type,
|
|
image_gen_tool: include_image_gen_tool,
|
|
agent_roles: BTreeMap::new(),
|
|
search_tool: include_search_tool,
|
|
tool_suggest: include_tool_suggest,
|
|
exec_permission_approvals_enabled,
|
|
request_permissions_tool_enabled,
|
|
code_mode_enabled: include_code_mode,
|
|
code_mode_only_enabled: include_code_mode_only,
|
|
js_repl_enabled: include_js_repl,
|
|
js_repl_tools_only: include_js_repl_tools_only,
|
|
can_request_original_image_detail: include_original_image_detail,
|
|
collab_tools: include_collab_tools,
|
|
multi_agent_v2: include_multi_agent_v2,
|
|
request_user_input: include_request_user_input,
|
|
default_mode_request_user_input: include_default_mode_request_user_input,
|
|
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
|
|
agent_jobs_tools: include_agent_jobs,
|
|
agent_jobs_worker_tools,
|
|
}
|
|
}
|
|
|
|
pub fn with_agent_roles(mut self, agent_roles: BTreeMap<String, AgentRoleConfig>) -> Self {
|
|
self.agent_roles = agent_roles;
|
|
self
|
|
}
|
|
|
|
pub fn with_allow_login_shell(mut self, allow_login_shell: bool) -> Self {
|
|
self.allow_login_shell = allow_login_shell;
|
|
self
|
|
}
|
|
|
|
pub fn with_unified_exec_shell_mode(
|
|
mut self,
|
|
unified_exec_shell_mode: UnifiedExecShellMode,
|
|
) -> Self {
|
|
self.unified_exec_shell_mode = unified_exec_shell_mode;
|
|
self
|
|
}
|
|
|
|
pub fn with_unified_exec_shell_mode_for_session(
|
|
mut self,
|
|
user_shell: &Shell,
|
|
shell_zsh_path: Option<&PathBuf>,
|
|
main_execve_wrapper_exe: Option<&PathBuf>,
|
|
) -> Self {
|
|
self.unified_exec_shell_mode = UnifiedExecShellMode::for_session(
|
|
self.shell_command_backend,
|
|
user_shell,
|
|
shell_zsh_path,
|
|
main_execve_wrapper_exe,
|
|
);
|
|
self
|
|
}
|
|
|
|
pub fn with_web_search_config(mut self, web_search_config: Option<WebSearchConfig>) -> Self {
|
|
self.web_search_config = web_search_config;
|
|
self
|
|
}
|
|
|
|
pub fn for_code_mode_nested_tools(&self) -> Self {
|
|
let mut nested = self.clone();
|
|
nested.code_mode_enabled = false;
|
|
nested.code_mode_only_enabled = false;
|
|
nested
|
|
}
|
|
}
|
|
|
|
fn supports_image_generation(model_info: &ModelInfo) -> bool {
|
|
model_info.input_modalities.contains(&InputModality::Image)
|
|
}
|
|
|
|
fn create_wait_tool() -> ToolSpec {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"cell_id".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Identifier of the running exec cell.".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"yield_time_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"How long to wait (in milliseconds) for more output before yielding again."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"max_tokens".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Maximum number of output tokens to return for this wait call.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"terminate".to_string(),
|
|
JsonSchema::Boolean {
|
|
description: Some("Whether to terminate the running exec cell.".to_string()),
|
|
},
|
|
),
|
|
]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: WAIT_TOOL_NAME.to_string(),
|
|
description: format!(
|
|
"Waits on a yielded `{PUBLIC_TOOL_NAME}` cell and returns new output or completion.\n{}",
|
|
codex_code_mode::build_wait_tool_description().trim()
|
|
),
|
|
strict: false,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["cell_id".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
defer_loading: None,
|
|
})
|
|
}
|
|
|
|
fn create_collab_input_items_schema() -> JsonSchema {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"type".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Input item type: text, image, local_image, skill, or mention.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"text".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Text content when type is text.".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"image_url".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Image URL when type is image.".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"path".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Path when type is local_image/skill, or structured mention target such as app://<connector-id> or plugin://<plugin-name>@<marketplace-name> when type is mention."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"name".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Display name when type is skill or mention.".to_string()),
|
|
},
|
|
),
|
|
]);
|
|
|
|
JsonSchema::Array {
|
|
items: Box::new(JsonSchema::Object {
|
|
properties,
|
|
required: None,
|
|
additional_properties: Some(false.into()),
|
|
}),
|
|
description: Some(
|
|
"Structured input items. Use this to pass explicit mentions (for example app:// connector paths)."
|
|
.to_string(),
|
|
),
|
|
}
|
|
}
|
|
|
|
fn spawn_agent_common_properties(config: &ToolsConfig) -> BTreeMap<String, JsonSchema> {
|
|
BTreeMap::from([
|
|
(
|
|
"message".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Initial plain-text task for the new agent. Use either message or items."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
("items".to_string(), create_collab_input_items_schema()),
|
|
(
|
|
"agent_type".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(crate::agent::role::spawn_tool_spec::build(
|
|
&config.agent_roles,
|
|
)),
|
|
},
|
|
),
|
|
(
|
|
"fork_context".to_string(),
|
|
JsonSchema::Boolean {
|
|
description: Some(
|
|
"When true, fork the current thread history into the new agent before sending the initial prompt. This must be used when you want the new agent to have exactly the same context as you."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"model".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Optional model override for the new agent. Replaces the inherited model."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"reasoning_effort".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Optional reasoning effort override for the new agent. Replaces the inherited reasoning effort."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
])
|
|
}
|
|
|
|
fn spawn_agent_tool_description(
|
|
available_models_description: &str,
|
|
return_value_description: &str,
|
|
) -> String {
|
|
format!(
|
|
r#"
|
|
Only use `spawn_agent` if and only if the user explicitly asks for sub-agents, delegation, or parallel agent work.
|
|
Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn.
|
|
Agent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself.
|
|
Spawn a sub-agent for a well-scoped task. {return_value_description} This spawn_agent tool provides you access to smaller but more efficient sub-agents. A mini model can solve many tasks faster than the main model. You should follow the rules and guidelines below to use this tool.
|
|
|
|
{available_models_description}
|
|
### When to delegate vs. do the subtask yourself
|
|
- First, quickly analyze the overall user task and form a succinct high-level plan. Identify which tasks are immediate blockers on the critical path, and which tasks are sidecar tasks that are needed but can run in parallel without blocking the next local step. As part of that plan, explicitly decide what immediate task you should do locally right now. Do this planning step before delegating to agents so you do not hand off the immediate blocking task to a submodel and then waste time waiting on it.
|
|
- Use the smaller subagent when a subtask is easy enough for it to handle and can run in parallel with your local work. Prefer delegating concrete, bounded sidecar tasks that materially advance the main task without blocking your immediate next local step.
|
|
- Do not delegate urgent blocking work when your immediate next step depends on that result. If the very next action is blocked on that task, the main rollout should usually do it locally to keep the critical path moving.
|
|
- Keep work local when the subtask is too difficult to delegate well and when it is tightly coupled, urgent, or likely to block your immediate next step.
|
|
|
|
### Designing delegated subtasks
|
|
- Subtasks must be concrete, well-defined, and self-contained.
|
|
- Delegated subtasks must materially advance the main task.
|
|
- Do not duplicate work between the main rollout and delegated subtasks.
|
|
- Avoid issuing multiple delegate calls on the same unresolved thread unless the new delegated task is genuinely different and necessary.
|
|
- Narrow the delegated ask to the concrete output you need next.
|
|
- For coding tasks, prefer delegating concrete code-change worker subtasks over read-only explorer analysis when the subagent can make a bounded patch in a clear write scope.
|
|
- When delegating coding work, instruct the submodel to edit files directly in its forked workspace and list the file paths it changed in the final answer.
|
|
- For code-edit subtasks, decompose work so each delegated task has a disjoint write set.
|
|
|
|
### After you delegate
|
|
- Call wait_agent very sparingly. Only call wait_agent when you need the result immediately for the next critical-path step and you are blocked until it returns.
|
|
- Do not redo delegated subagent tasks yourself; focus on integrating results or tackling non-overlapping work.
|
|
- While the subagent is running in the background, do meaningful non-overlapping work immediately.
|
|
- Do not repeatedly wait by reflex.
|
|
- When a delegated coding task returns, quickly review the uploaded changes, then integrate or refine them.
|
|
|
|
### Parallel delegation patterns
|
|
- Run multiple independent information-seeking subtasks in parallel when you have distinct questions that can be answered independently.
|
|
- Split implementation into disjoint codebase slices and spawn multiple agents for them in parallel when the write scopes do not overlap.
|
|
- Delegate verification only when it can run in parallel with ongoing implementation and is likely to catch a concrete risk before final integration.
|
|
- The key is to find opportunities to spawn multiple independent subtasks in parallel within the same round, while ensuring each subtask is well-defined, self-contained, and materially advances the main task."#
|
|
)
|
|
}
|
|
|
|
fn create_spawn_agent_tool_v1(config: &ToolsConfig) -> ToolSpec {
|
|
let available_models_description = spawn_agent_models_description(&config.available_models);
|
|
let return_value_description =
|
|
"Returns the spawned agent id plus the user-facing nickname when available.";
|
|
let properties = spawn_agent_common_properties(config);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "spawn_agent".to_string(),
|
|
description: spawn_agent_tool_description(
|
|
&available_models_description,
|
|
return_value_description,
|
|
),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: None,
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(spawn_agent_output_schema_v1()),
|
|
})
|
|
}
|
|
|
|
fn create_spawn_agent_tool_v2(config: &ToolsConfig) -> ToolSpec {
|
|
let available_models_description = spawn_agent_models_description(&config.available_models);
|
|
let return_value_description = "Returns the canonical task name for the spawned agent, plus the user-facing nickname when available.";
|
|
let mut properties = spawn_agent_common_properties(config);
|
|
properties.insert(
|
|
"task_name".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Task name for the new agent. Use lowercase letters, digits, and underscores."
|
|
.to_string(),
|
|
),
|
|
},
|
|
);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "spawn_agent".to_string(),
|
|
description: spawn_agent_tool_description(
|
|
&available_models_description,
|
|
return_value_description,
|
|
),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["task_name".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(spawn_agent_output_schema_v2()),
|
|
})
|
|
}
|
|
|
|
fn spawn_agent_models_description(models: &[ModelPreset]) -> String {
|
|
let visible_models: Vec<&ModelPreset> =
|
|
models.iter().filter(|model| model.show_in_picker).collect();
|
|
if visible_models.is_empty() {
|
|
return "No picker-visible models are currently loaded.".to_string();
|
|
}
|
|
|
|
visible_models
|
|
.into_iter()
|
|
.map(|model| {
|
|
let efforts = model
|
|
.supported_reasoning_efforts
|
|
.iter()
|
|
.map(|preset| format!("{} ({})", preset.effort, preset.description))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
format!(
|
|
"- {} (`{}`): {} Default reasoning effort: {}. Supported reasoning efforts: {}.",
|
|
model.display_name,
|
|
model.model,
|
|
model.description,
|
|
model.default_reasoning_effort,
|
|
efforts
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
fn create_spawn_agents_on_csv_tool() -> ToolSpec {
|
|
let mut properties = BTreeMap::new();
|
|
properties.insert(
|
|
"csv_path".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Path to the CSV file containing input rows.".to_string()),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"instruction".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Instruction template to apply to each CSV row. Use {column_name} placeholders to inject values from the row."
|
|
.to_string(),
|
|
),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"id_column".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Optional column name to use as stable item id.".to_string()),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"output_csv_path".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Optional output CSV path for exported results.".to_string()),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"max_concurrency".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Maximum concurrent workers for this job. Defaults to 16 and is capped by config."
|
|
.to_string(),
|
|
),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"max_workers".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Alias for max_concurrency. Set to 1 to run sequentially.".to_string(),
|
|
),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"max_runtime_seconds".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Maximum runtime per worker before it is failed. Defaults to 1800 seconds."
|
|
.to_string(),
|
|
),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"output_schema".to_string(),
|
|
JsonSchema::Object {
|
|
properties: BTreeMap::new(),
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
);
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "spawn_agents_on_csv".to_string(),
|
|
description: "Process a CSV by spawning one worker sub-agent per row. The instruction string is a template where `{column}` placeholders are replaced with row values. Each worker must call `report_agent_job_result` with a JSON object (matching `output_schema` when provided); missing reports are treated as failures. This call blocks until all rows finish and automatically exports results to `output_csv_path` (or a default path)."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["csv_path".to_string(), "instruction".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
fn create_report_agent_job_result_tool() -> ToolSpec {
|
|
let mut properties = BTreeMap::new();
|
|
properties.insert(
|
|
"job_id".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Identifier of the job.".to_string()),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"item_id".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Identifier of the job item.".to_string()),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"result".to_string(),
|
|
JsonSchema::Object {
|
|
properties: BTreeMap::new(),
|
|
required: None,
|
|
additional_properties: None,
|
|
},
|
|
);
|
|
properties.insert(
|
|
"stop".to_string(),
|
|
JsonSchema::Boolean {
|
|
description: Some(
|
|
"Optional. When true, cancels the remaining job items after this result is recorded."
|
|
.to_string(),
|
|
),
|
|
},
|
|
);
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "report_agent_job_result".to_string(),
|
|
description:
|
|
"Worker-only tool to report a result for an agent job item. Main agents should not call this."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec![
|
|
"job_id".to_string(),
|
|
"item_id".to_string(),
|
|
"result".to_string(),
|
|
]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
fn create_send_input_tool_v1() -> ToolSpec {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"target".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Agent id to message (from spawn_agent).".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"message".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Legacy plain-text message to send to the agent. Use either message or items."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
("items".to_string(), create_collab_input_items_schema()),
|
|
(
|
|
"interrupt".to_string(),
|
|
JsonSchema::Boolean {
|
|
description: Some(
|
|
"When true, stop the agent's current task and handle this immediately. When false (default), queue this message."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "send_input".to_string(),
|
|
description: "Send a message to an existing agent. Use interrupt=true to redirect work immediately. You should reuse the agent by send_input if you believe your assigned task is highly dependent on the context of a previous task."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["target".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(send_input_output_schema()),
|
|
})
|
|
}
|
|
|
|
fn create_send_message_tool() -> ToolSpec {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"target".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Agent id or canonical task name to message (from spawn_agent).".to_string(),
|
|
),
|
|
},
|
|
),
|
|
("items".to_string(), create_collab_input_items_schema()),
|
|
(
|
|
"interrupt".to_string(),
|
|
JsonSchema::Boolean {
|
|
description: Some(
|
|
"When true, stop the agent's current task and handle this immediately. When false (default), queue this message."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "send_message".to_string(),
|
|
description: "Add a message to an existing agent without triggering a new turn. Use interrupt=true to stop the current task first. In MultiAgentV2, this tool currently supports text content only."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["target".to_string(), "items".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(send_input_output_schema()),
|
|
})
|
|
}
|
|
|
|
fn create_assign_task_tool() -> ToolSpec {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"target".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Agent id or canonical task name to message (from spawn_agent).".to_string(),
|
|
),
|
|
},
|
|
),
|
|
("items".to_string(), create_collab_input_items_schema()),
|
|
(
|
|
"interrupt".to_string(),
|
|
JsonSchema::Boolean {
|
|
description: Some(
|
|
"When true, stop the agent's current task and handle this immediately. When false (default), queue this message."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "assign_task".to_string(),
|
|
description: "Add a message to an existing agent and trigger a turn in the target. Use interrupt=true to redirect work immediately. In MultiAgentV2, this tool currently supports text content only."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["target".to_string(), "items".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(send_input_output_schema()),
|
|
})
|
|
}
|
|
|
|
fn create_resume_agent_tool() -> ToolSpec {
|
|
let mut properties = BTreeMap::new();
|
|
properties.insert(
|
|
"id".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Agent id to resume.".to_string()),
|
|
},
|
|
);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "resume_agent".to_string(),
|
|
description:
|
|
"Resume a previously closed agent by id so it can receive send_input and wait_agent calls."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["id".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(resume_agent_output_schema()),
|
|
})
|
|
}
|
|
|
|
fn wait_agent_tool_parameters_v1() -> JsonSchema {
|
|
let mut properties = BTreeMap::new();
|
|
properties.insert(
|
|
"targets".to_string(),
|
|
JsonSchema::Array {
|
|
items: Box::new(JsonSchema::String { description: None }),
|
|
description: Some(
|
|
"Agent ids to wait on. Pass multiple ids to wait for whichever finishes first."
|
|
.to_string(),
|
|
),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"timeout_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(format!(
|
|
"Optional timeout in milliseconds. Defaults to {DEFAULT_WAIT_TIMEOUT_MS}, min {MIN_WAIT_TIMEOUT_MS}, max {MAX_WAIT_TIMEOUT_MS}. Prefer longer waits (minutes) to avoid busy polling."
|
|
)),
|
|
},
|
|
);
|
|
|
|
JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["targets".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
}
|
|
}
|
|
|
|
fn wait_agent_tool_parameters_v2() -> JsonSchema {
|
|
let mut properties = BTreeMap::new();
|
|
properties.insert(
|
|
"targets".to_string(),
|
|
JsonSchema::Array {
|
|
items: Box::new(JsonSchema::String { description: None }),
|
|
description: Some(
|
|
"Agent ids or canonical task names to wait on. Pass multiple targets to wait for whichever finishes first."
|
|
.to_string(),
|
|
),
|
|
},
|
|
);
|
|
properties.insert(
|
|
"timeout_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(format!(
|
|
"Optional timeout in milliseconds. Defaults to {DEFAULT_WAIT_TIMEOUT_MS}, min {MIN_WAIT_TIMEOUT_MS}, max {MAX_WAIT_TIMEOUT_MS}. Prefer longer waits (minutes) to avoid busy polling."
|
|
)),
|
|
},
|
|
);
|
|
|
|
JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["targets".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
}
|
|
}
|
|
|
|
fn create_wait_agent_tool_v1() -> ToolSpec {
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "wait_agent".to_string(),
|
|
description: "Wait for agents to reach a final status. Completed statuses may include the agent's final message. Returns empty status when timed out. Once the agent reaches a final status, a notification message will be received containing the same completed status."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: wait_agent_tool_parameters_v1(),
|
|
output_schema: Some(wait_output_schema_v1()),
|
|
})
|
|
}
|
|
|
|
fn create_wait_agent_tool_v2() -> ToolSpec {
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "wait_agent".to_string(),
|
|
description: "Wait for agents to reach a final status. Returns a brief wait summary instead of the agent's final content. Returns a timeout summary when no agent reaches a final status before the deadline."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: wait_agent_tool_parameters_v2(),
|
|
output_schema: Some(wait_output_schema_v2()),
|
|
})
|
|
}
|
|
|
|
fn create_list_agents_tool() -> ToolSpec {
|
|
let properties = BTreeMap::from([(
|
|
"path_prefix".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Optional task-path prefix. Accepts the same relative or absolute task-path syntax as other MultiAgentV2 agent targets."
|
|
.to_string(),
|
|
),
|
|
},
|
|
)]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "list_agents".to_string(),
|
|
description: "List live agents in the current root thread tree. Optionally filter by task-path prefix."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: None,
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(list_agents_output_schema()),
|
|
})
|
|
}
|
|
|
|
fn create_request_user_input_tool(
|
|
collaboration_modes_config: CollaborationModesConfig,
|
|
) -> ToolSpec {
|
|
let mut option_props = BTreeMap::new();
|
|
option_props.insert(
|
|
"label".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("User-facing label (1-5 words).".to_string()),
|
|
},
|
|
);
|
|
option_props.insert(
|
|
"description".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"One short sentence explaining impact/tradeoff if selected.".to_string(),
|
|
),
|
|
},
|
|
);
|
|
|
|
let options_schema = JsonSchema::Array {
|
|
description: Some(
|
|
"Provide 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option in this list; the client will add a free-form \"Other\" option automatically."
|
|
.to_string(),
|
|
),
|
|
items: Box::new(JsonSchema::Object {
|
|
properties: option_props,
|
|
required: Some(vec!["label".to_string(), "description".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
}),
|
|
};
|
|
|
|
let mut question_props = BTreeMap::new();
|
|
question_props.insert(
|
|
"id".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Stable identifier for mapping answers (snake_case).".to_string()),
|
|
},
|
|
);
|
|
question_props.insert(
|
|
"header".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Short header label shown in the UI (12 or fewer chars).".to_string(),
|
|
),
|
|
},
|
|
);
|
|
question_props.insert(
|
|
"question".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Single-sentence prompt shown to the user.".to_string()),
|
|
},
|
|
);
|
|
question_props.insert("options".to_string(), options_schema);
|
|
|
|
let questions_schema = JsonSchema::Array {
|
|
description: Some("Questions to show the user. Prefer 1 and do not exceed 3".to_string()),
|
|
items: Box::new(JsonSchema::Object {
|
|
properties: question_props,
|
|
required: Some(vec![
|
|
"id".to_string(),
|
|
"header".to_string(),
|
|
"question".to_string(),
|
|
"options".to_string(),
|
|
]),
|
|
additional_properties: Some(false.into()),
|
|
}),
|
|
};
|
|
|
|
let mut properties = BTreeMap::new();
|
|
properties.insert("questions".to_string(), questions_schema);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "request_user_input".to_string(),
|
|
description: request_user_input_tool_description(
|
|
collaboration_modes_config.default_mode_request_user_input,
|
|
),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["questions".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
fn create_close_agent_tool_v1() -> ToolSpec {
|
|
let mut properties = BTreeMap::new();
|
|
properties.insert(
|
|
"target".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Agent id to close (from spawn_agent).".to_string()),
|
|
},
|
|
);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "close_agent".to_string(),
|
|
description: "Close an agent and any open descendants when they are no longer needed, and return the target agent's previous status before shutdown was requested. Don't keep agents open for too long if they are not needed anymore.".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["target".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(close_agent_output_schema()),
|
|
})
|
|
}
|
|
|
|
fn create_close_agent_tool_v2() -> ToolSpec {
|
|
let mut properties = BTreeMap::new();
|
|
properties.insert(
|
|
"target".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Agent id or canonical task name to close (from spawn_agent).".to_string(),
|
|
),
|
|
},
|
|
);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "close_agent".to_string(),
|
|
description: "Close an agent and any open descendants when they are no longer needed, and return the target agent's previous status before shutdown was requested. Don't keep agents open for too long if they are not needed anymore.".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["target".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(close_agent_output_schema()),
|
|
})
|
|
}
|
|
|
|
fn create_test_sync_tool() -> ToolSpec {
|
|
let barrier_properties = BTreeMap::from([
|
|
(
|
|
"id".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Identifier shared by concurrent calls that should rendezvous".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"participants".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Number of tool calls that must arrive before the barrier opens".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"timeout_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Maximum time in milliseconds to wait at the barrier".to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"sleep_before_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Optional delay in milliseconds before any other action".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"sleep_after_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Optional delay in milliseconds after completing the barrier".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"barrier".to_string(),
|
|
JsonSchema::Object {
|
|
properties: barrier_properties,
|
|
required: Some(vec!["id".to_string(), "participants".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
),
|
|
]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "test_sync_tool".to_string(),
|
|
description: "Internal synchronization helper used by Codex integration tests.".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: None,
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
fn create_tool_search_tool(app_tools: &HashMap<String, ToolInfo>) -> ToolSpec {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"query".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Search query for apps tools.".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"limit".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(format!(
|
|
"Maximum number of tools to return (defaults to {TOOL_SEARCH_DEFAULT_LIMIT})."
|
|
)),
|
|
},
|
|
),
|
|
]);
|
|
let mut app_descriptions = BTreeMap::new();
|
|
for tool in app_tools.values() {
|
|
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
|
continue;
|
|
}
|
|
|
|
let Some(connector_name) = tool
|
|
.connector_name
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|connector_name| !connector_name.is_empty())
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let connector_description = tool
|
|
.connector_description
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|connector_description| !connector_description.is_empty())
|
|
.map(str::to_string);
|
|
|
|
app_descriptions
|
|
.entry(connector_name.to_string())
|
|
.and_modify(|existing: &mut Option<String>| {
|
|
if existing.is_none() {
|
|
*existing = connector_description.clone();
|
|
}
|
|
})
|
|
.or_insert(connector_description);
|
|
}
|
|
|
|
let app_descriptions = if app_descriptions.is_empty() {
|
|
"None currently enabled.".to_string()
|
|
} else {
|
|
app_descriptions
|
|
.into_iter()
|
|
.map(
|
|
|(connector_name, connector_description)| match connector_description {
|
|
Some(connector_description) => {
|
|
format!("- {connector_name}: {connector_description}")
|
|
}
|
|
None => format!("- {connector_name}"),
|
|
},
|
|
)
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
};
|
|
|
|
let description = TOOL_SEARCH_DESCRIPTION_TEMPLATE
|
|
.render([(
|
|
TOOL_SEARCH_DESCRIPTION_TEMPLATE_KEY,
|
|
app_descriptions.as_str(),
|
|
)])
|
|
.unwrap_or_else(|err| panic!("tool_search description template must render: {err}"));
|
|
|
|
ToolSpec::ToolSearch {
|
|
execution: "client".to_string(),
|
|
description,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["query".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn create_tool_suggest_tool(discoverable_tools: &[DiscoverableTool]) -> ToolSpec {
|
|
let discoverable_tool_ids = discoverable_tools
|
|
.iter()
|
|
.map(DiscoverableTool::id)
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"tool_type".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Type of discoverable tool to suggest. Use \"connector\" or \"plugin\"."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"action_type".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Suggested action for the tool. Use \"install\" or \"enable\".".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"tool_id".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(format!(
|
|
"Connector or plugin id to suggest. Must be one of: {discoverable_tool_ids}."
|
|
)),
|
|
},
|
|
),
|
|
(
|
|
"suggest_reason".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Concise one-line user-facing reason why this tool can help with the current request."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
let discoverable_tools = format_discoverable_tools(discoverable_tools);
|
|
let description = TOOL_SUGGEST_DESCRIPTION_TEMPLATE
|
|
.render([(
|
|
TOOL_SUGGEST_DESCRIPTION_TEMPLATE_KEY,
|
|
discoverable_tools.as_str(),
|
|
)])
|
|
.unwrap_or_else(|err| panic!("tool_suggest description template must render: {err}"));
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: TOOL_SUGGEST_TOOL_NAME.to_string(),
|
|
description,
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec![
|
|
"tool_type".to_string(),
|
|
"action_type".to_string(),
|
|
"tool_id".to_string(),
|
|
"suggest_reason".to_string(),
|
|
]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
fn format_discoverable_tools(discoverable_tools: &[DiscoverableTool]) -> String {
|
|
let mut discoverable_tools = discoverable_tools.to_vec();
|
|
discoverable_tools.sort_by(|left, right| {
|
|
left.name()
|
|
.cmp(right.name())
|
|
.then_with(|| left.id().cmp(right.id()))
|
|
});
|
|
|
|
discoverable_tools
|
|
.into_iter()
|
|
.map(|tool| {
|
|
let description = tool
|
|
.description()
|
|
.filter(|description| !description.trim().is_empty())
|
|
.map(ToString::to_string)
|
|
.unwrap_or_else(|| match &tool {
|
|
DiscoverableTool::Connector(_) => "No description provided.".to_string(),
|
|
DiscoverableTool::Plugin(plugin) => format_plugin_summary(plugin.as_ref()),
|
|
});
|
|
let default_action = match tool.tool_type() {
|
|
DiscoverableToolType::Connector => DiscoverableToolAction::Install,
|
|
DiscoverableToolType::Plugin => DiscoverableToolAction::Install,
|
|
};
|
|
format!(
|
|
"- {} (id: `{}`, type: {}, action: {}): {}",
|
|
tool.name(),
|
|
tool.id(),
|
|
tool.tool_type().as_str(),
|
|
default_action.as_str(),
|
|
description
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
fn format_plugin_summary(plugin: &DiscoverablePluginInfo) -> String {
|
|
let mut details = Vec::new();
|
|
if plugin.has_skills {
|
|
details.push("skills".to_string());
|
|
}
|
|
if !plugin.mcp_server_names.is_empty() {
|
|
details.push(format!(
|
|
"MCP servers: {}",
|
|
plugin.mcp_server_names.join(", ")
|
|
));
|
|
}
|
|
if !plugin.app_connector_ids.is_empty() {
|
|
details.push(format!(
|
|
"app connectors: {}",
|
|
plugin.app_connector_ids.join(", ")
|
|
));
|
|
}
|
|
|
|
if details.is_empty() {
|
|
"No description provided.".to_string()
|
|
} else {
|
|
details.join("; ")
|
|
}
|
|
}
|
|
|
|
fn create_list_dir_tool() -> ToolSpec {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"dir_path".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Absolute path to the directory to list.".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"offset".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"The entry number to start listing from. Must be 1 or greater.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"limit".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some("The maximum number of entries to return.".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"depth".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"The maximum directory depth to traverse. Must be 1 or greater.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "list_dir".to_string(),
|
|
description:
|
|
"Lists entries in a local directory with 1-indexed entry numbers and simple type labels."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["dir_path".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
fn create_js_repl_tool() -> ToolSpec {
|
|
// Keep JS input freeform, but block the most common malformed payload shapes
|
|
// (JSON wrappers, quoted strings, and markdown fences) before they reach the
|
|
// runtime `reject_json_or_quoted_source` validation. The API's regex engine
|
|
// does not support look-around, so this uses a "first significant token"
|
|
// pattern rather than negative lookaheads.
|
|
const JS_REPL_FREEFORM_GRAMMAR: &str = r#"
|
|
start: pragma_source | plain_source
|
|
|
|
pragma_source: PRAGMA_LINE NEWLINE js_source
|
|
plain_source: PLAIN_JS_SOURCE
|
|
|
|
js_source: JS_SOURCE
|
|
|
|
PRAGMA_LINE: /[ \t]*\/\/ codex-js-repl:[^\r\n]*/
|
|
NEWLINE: /\r?\n/
|
|
PLAIN_JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
|
JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
|
"#;
|
|
|
|
ToolSpec::Freeform(FreeformTool {
|
|
name: "js_repl".to_string(),
|
|
description: "Runs JavaScript in a persistent Node kernel with top-level await. This is a freeform tool: send raw JavaScript source text, optionally with a first-line pragma like `// codex-js-repl: timeout_ms=15000`; do not send JSON/quotes/markdown fences."
|
|
.to_string(),
|
|
format: FreeformToolFormat {
|
|
r#type: "grammar".to_string(),
|
|
syntax: "lark".to_string(),
|
|
definition: JS_REPL_FREEFORM_GRAMMAR.to_string(),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn create_js_repl_reset_tool() -> ToolSpec {
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "js_repl_reset".to_string(),
|
|
description:
|
|
"Restarts the js_repl kernel for this run and clears persisted top-level bindings."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties: BTreeMap::new(),
|
|
required: None,
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
fn create_code_mode_tool(
|
|
enabled_tools: &[(String, String)],
|
|
code_mode_only_enabled: bool,
|
|
) -> ToolSpec {
|
|
const CODE_MODE_FREEFORM_GRAMMAR: &str = r#"
|
|
start: pragma_source | plain_source
|
|
pragma_source: PRAGMA_LINE NEWLINE SOURCE
|
|
plain_source: SOURCE
|
|
|
|
PRAGMA_LINE: /[ \t]*\/\/ @exec:[^\r\n]*/
|
|
NEWLINE: /\r?\n/
|
|
SOURCE: /[\s\S]+/
|
|
"#;
|
|
|
|
ToolSpec::Freeform(FreeformTool {
|
|
name: PUBLIC_TOOL_NAME.to_string(),
|
|
description: codex_code_mode::build_exec_tool_description(
|
|
enabled_tools,
|
|
code_mode_only_enabled,
|
|
),
|
|
format: FreeformToolFormat {
|
|
r#type: "grammar".to_string(),
|
|
syntax: "lark".to_string(),
|
|
definition: CODE_MODE_FREEFORM_GRAMMAR.to_string(),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn create_list_mcp_resources_tool() -> ToolSpec {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"server".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Optional MCP server name. When omitted, lists resources from every configured server."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"cursor".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Opaque cursor returned by a previous list_mcp_resources call for the same server."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "list_mcp_resources".to_string(),
|
|
description: "Lists resources provided by MCP servers. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Prefer resources over web search when possible.".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: None,
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
fn create_list_mcp_resource_templates_tool() -> ToolSpec {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"server".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Optional MCP server name. When omitted, lists resource templates from all configured servers."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"cursor".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Opaque cursor returned by a previous list_mcp_resource_templates call for the same server."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "list_mcp_resource_templates".to_string(),
|
|
description: "Lists resource templates provided by MCP servers. Parameterized resource templates allow servers to share data that takes parameters and provides context to language models, such as files, database schemas, or application-specific information. Prefer resource templates over web search when possible.".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: None,
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
fn create_read_mcp_resource_tool() -> ToolSpec {
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"server".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"MCP server name exactly as configured. Must match the 'server' field returned by list_mcp_resources."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"uri".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Resource URI to read. Must be one of the URIs returned by list_mcp_resources."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "read_mcp_resource".to_string(),
|
|
description:
|
|
"Read a specific resource from an MCP server given the server name and resource URI."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["server".to_string(), "uri".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
}
|
|
|
|
/// TODO(dylan): deprecate once we get rid of json tool
|
|
#[derive(Serialize, Deserialize)]
|
|
pub(crate) struct ApplyPatchToolArgs {
|
|
pub(crate) input: String,
|
|
}
|
|
|
|
fn push_tool_spec(
|
|
builder: &mut ToolRegistryBuilder,
|
|
spec: ToolSpec,
|
|
supports_parallel_tool_calls: bool,
|
|
code_mode_enabled: bool,
|
|
) {
|
|
let spec = if code_mode_enabled {
|
|
augment_tool_spec_for_code_mode(spec)
|
|
} else {
|
|
spec
|
|
};
|
|
if supports_parallel_tool_calls {
|
|
builder.push_spec_with_parallel_support(spec, /*supports_parallel_tool_calls*/ true);
|
|
} else {
|
|
builder.push_spec(spec);
|
|
}
|
|
}
|
|
|
|
/// Builds the tool registry builder while collecting tool specs for later serialization.
|
|
#[cfg(test)]
|
|
pub(crate) fn build_specs(
|
|
config: &ToolsConfig,
|
|
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
|
app_tools: Option<HashMap<String, ToolInfo>>,
|
|
dynamic_tools: &[DynamicToolSpec],
|
|
) -> ToolRegistryBuilder {
|
|
build_specs_with_discoverable_tools(
|
|
config,
|
|
mcp_tools,
|
|
app_tools,
|
|
/*discoverable_tools*/ None,
|
|
dynamic_tools,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn build_specs_with_discoverable_tools(
|
|
config: &ToolsConfig,
|
|
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
|
app_tools: Option<HashMap<String, ToolInfo>>,
|
|
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
|
dynamic_tools: &[DynamicToolSpec],
|
|
) -> ToolRegistryBuilder {
|
|
use crate::tools::handlers::ApplyPatchHandler;
|
|
use crate::tools::handlers::CodeModeExecuteHandler;
|
|
use crate::tools::handlers::CodeModeWaitHandler;
|
|
use crate::tools::handlers::DynamicToolHandler;
|
|
use crate::tools::handlers::JsReplHandler;
|
|
use crate::tools::handlers::JsReplResetHandler;
|
|
use crate::tools::handlers::ListDirHandler;
|
|
use crate::tools::handlers::McpHandler;
|
|
use crate::tools::handlers::McpResourceHandler;
|
|
use crate::tools::handlers::PlanHandler;
|
|
use crate::tools::handlers::RequestPermissionsHandler;
|
|
use crate::tools::handlers::RequestUserInputHandler;
|
|
use crate::tools::handlers::ShellCommandHandler;
|
|
use crate::tools::handlers::ShellHandler;
|
|
use crate::tools::handlers::TestSyncHandler;
|
|
use crate::tools::handlers::ToolSearchHandler;
|
|
use crate::tools::handlers::ToolSuggestHandler;
|
|
use crate::tools::handlers::UnifiedExecHandler;
|
|
use crate::tools::handlers::ViewImageHandler;
|
|
use crate::tools::handlers::multi_agents::CloseAgentHandler;
|
|
use crate::tools::handlers::multi_agents::ResumeAgentHandler;
|
|
use crate::tools::handlers::multi_agents::SendInputHandler;
|
|
use crate::tools::handlers::multi_agents::SpawnAgentHandler;
|
|
use crate::tools::handlers::multi_agents::WaitAgentHandler;
|
|
use crate::tools::handlers::multi_agents_v2::AssignTaskHandler as AssignTaskHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
|
|
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
|
|
use std::sync::Arc;
|
|
|
|
let mut builder = ToolRegistryBuilder::new();
|
|
|
|
let shell_handler = Arc::new(ShellHandler);
|
|
let unified_exec_handler = Arc::new(UnifiedExecHandler);
|
|
let plan_handler = Arc::new(PlanHandler);
|
|
let apply_patch_handler = Arc::new(ApplyPatchHandler);
|
|
let dynamic_tool_handler = Arc::new(DynamicToolHandler);
|
|
let view_image_handler = Arc::new(ViewImageHandler);
|
|
let mcp_handler = Arc::new(McpHandler);
|
|
let mcp_resource_handler = Arc::new(McpResourceHandler);
|
|
let shell_command_handler = Arc::new(ShellCommandHandler::from(config.shell_command_backend));
|
|
let request_permissions_handler = Arc::new(RequestPermissionsHandler);
|
|
let request_user_input_handler = Arc::new(RequestUserInputHandler {
|
|
default_mode_request_user_input: config.default_mode_request_user_input,
|
|
});
|
|
let tool_suggest_handler = Arc::new(ToolSuggestHandler);
|
|
let code_mode_handler = Arc::new(CodeModeExecuteHandler);
|
|
let code_mode_wait_handler = Arc::new(CodeModeWaitHandler);
|
|
let js_repl_handler = Arc::new(JsReplHandler);
|
|
let js_repl_reset_handler = Arc::new(JsReplResetHandler);
|
|
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
|
|
|
|
if config.code_mode_enabled {
|
|
let nested_config = config.for_code_mode_nested_tools();
|
|
let (nested_specs, _) = build_specs_with_discoverable_tools(
|
|
&nested_config,
|
|
mcp_tools.clone(),
|
|
app_tools.clone(),
|
|
/*discoverable_tools*/ None,
|
|
dynamic_tools,
|
|
)
|
|
.build();
|
|
let mut enabled_tools = nested_specs
|
|
.into_iter()
|
|
.filter_map(|spec| tool_spec_to_code_mode_tool_definition(&spec.spec))
|
|
.map(|tool| (tool.name, tool.description))
|
|
.collect::<Vec<_>>();
|
|
enabled_tools.sort_by(|left, right| left.0.cmp(&right.0));
|
|
enabled_tools.dedup_by(|left, right| left.0 == right.0);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_code_mode_tool(&enabled_tools, config.code_mode_only_enabled),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler(PUBLIC_TOOL_NAME, code_mode_handler);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_wait_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler(WAIT_TOOL_NAME, code_mode_wait_handler);
|
|
}
|
|
|
|
match &config.shell_type {
|
|
ConfigShellToolType::Default => {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_shell_tool(ShellToolOptions {
|
|
exec_permission_approvals_enabled,
|
|
}),
|
|
/*supports_parallel_tool_calls*/ true,
|
|
config.code_mode_enabled,
|
|
);
|
|
}
|
|
ConfigShellToolType::Local => {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
ToolSpec::LocalShell {},
|
|
/*supports_parallel_tool_calls*/ true,
|
|
config.code_mode_enabled,
|
|
);
|
|
}
|
|
ConfigShellToolType::UnifiedExec => {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
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,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_write_stdin_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("exec_command", unified_exec_handler.clone());
|
|
builder.register_handler("write_stdin", unified_exec_handler);
|
|
}
|
|
ConfigShellToolType::Disabled => {
|
|
// Do nothing.
|
|
}
|
|
ConfigShellToolType::ShellCommand => {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
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.shell_type != ConfigShellToolType::Disabled {
|
|
// Always register shell aliases so older prompts remain compatible.
|
|
builder.register_handler("shell", shell_handler.clone());
|
|
builder.register_handler("container.exec", shell_handler.clone());
|
|
builder.register_handler("local_shell", shell_handler);
|
|
builder.register_handler("shell_command", shell_command_handler);
|
|
}
|
|
|
|
if mcp_tools.is_some() {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_list_mcp_resources_tool(),
|
|
/*supports_parallel_tool_calls*/ true,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_list_mcp_resource_templates_tool(),
|
|
/*supports_parallel_tool_calls*/ true,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_read_mcp_resource_tool(),
|
|
/*supports_parallel_tool_calls*/ true,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("list_mcp_resources", mcp_resource_handler.clone());
|
|
builder.register_handler("list_mcp_resource_templates", mcp_resource_handler.clone());
|
|
builder.register_handler("read_mcp_resource", mcp_resource_handler);
|
|
}
|
|
|
|
push_tool_spec(
|
|
&mut builder,
|
|
PLAN_TOOL.clone(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("update_plan", plan_handler);
|
|
|
|
if config.js_repl_enabled {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_js_repl_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_js_repl_reset_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("js_repl", js_repl_handler);
|
|
builder.register_handler("js_repl_reset", js_repl_reset_handler);
|
|
}
|
|
|
|
if config.request_user_input {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_request_user_input_tool(CollaborationModesConfig {
|
|
default_mode_request_user_input: config.default_mode_request_user_input,
|
|
}),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("request_user_input", request_user_input_handler);
|
|
}
|
|
|
|
if config.request_permissions_tool_enabled {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_request_permissions_tool(request_permissions_tool_description()),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("request_permissions", request_permissions_handler);
|
|
}
|
|
|
|
if config.search_tool
|
|
&& let Some(app_tools) = app_tools
|
|
{
|
|
let search_tool_handler = Arc::new(ToolSearchHandler::new(app_tools.clone()));
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_tool_search_tool(&app_tools),
|
|
/*supports_parallel_tool_calls*/ true,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler(TOOL_SEARCH_TOOL_NAME, search_tool_handler);
|
|
|
|
for tool in app_tools.values() {
|
|
let alias_name =
|
|
tool_handler_key(tool.tool_name.as_str(), Some(tool.tool_namespace.as_str()));
|
|
|
|
builder.register_handler(alias_name, mcp_handler.clone());
|
|
}
|
|
}
|
|
|
|
if config.tool_suggest
|
|
&& let Some(discoverable_tools) = discoverable_tools
|
|
.as_ref()
|
|
.filter(|tools| !tools.is_empty())
|
|
{
|
|
builder.push_spec_with_parallel_support(
|
|
create_tool_suggest_tool(discoverable_tools),
|
|
/*supports_parallel_tool_calls*/ true,
|
|
);
|
|
builder.register_handler(TOOL_SUGGEST_TOOL_NAME, tool_suggest_handler);
|
|
}
|
|
|
|
if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type {
|
|
match apply_patch_tool_type {
|
|
ApplyPatchToolType::Freeform => {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_apply_patch_freeform_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
}
|
|
ApplyPatchToolType::Function => {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_apply_patch_json_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
}
|
|
}
|
|
builder.register_handler("apply_patch", apply_patch_handler);
|
|
}
|
|
|
|
if config
|
|
.experimental_supported_tools
|
|
.iter()
|
|
.any(|tool| tool == "list_dir")
|
|
{
|
|
let list_dir_handler = Arc::new(ListDirHandler);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_list_dir_tool(),
|
|
/*supports_parallel_tool_calls*/ true,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("list_dir", list_dir_handler);
|
|
}
|
|
|
|
if config
|
|
.experimental_supported_tools
|
|
.contains(&"test_sync_tool".to_string())
|
|
{
|
|
let test_sync_handler = Arc::new(TestSyncHandler);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_test_sync_tool(),
|
|
/*supports_parallel_tool_calls*/ true,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("test_sync_tool", test_sync_handler);
|
|
}
|
|
|
|
let external_web_access = match config.web_search_mode {
|
|
Some(WebSearchMode::Cached) => Some(false),
|
|
Some(WebSearchMode::Live) => Some(true),
|
|
Some(WebSearchMode::Disabled) | None => None,
|
|
};
|
|
|
|
if let Some(external_web_access) = external_web_access {
|
|
let search_content_types = match config.web_search_tool_type {
|
|
WebSearchToolType::Text => None,
|
|
WebSearchToolType::TextAndImage => Some(
|
|
WEB_SEARCH_CONTENT_TYPES
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.collect(),
|
|
),
|
|
};
|
|
|
|
push_tool_spec(
|
|
&mut builder,
|
|
ToolSpec::WebSearch {
|
|
external_web_access: Some(external_web_access),
|
|
filters: config
|
|
.web_search_config
|
|
.as_ref()
|
|
.and_then(|cfg| cfg.filters.clone().map(Into::into)),
|
|
user_location: config
|
|
.web_search_config
|
|
.as_ref()
|
|
.and_then(|cfg| cfg.user_location.clone().map(Into::into)),
|
|
search_context_size: config
|
|
.web_search_config
|
|
.as_ref()
|
|
.and_then(|cfg| cfg.search_context_size),
|
|
search_content_types,
|
|
},
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
}
|
|
|
|
if config.image_gen_tool {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
ToolSpec::ImageGeneration {
|
|
output_format: "png".to_string(),
|
|
},
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
}
|
|
|
|
push_tool_spec(
|
|
&mut builder,
|
|
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,
|
|
);
|
|
builder.register_handler("view_image", view_image_handler);
|
|
|
|
if config.collab_tools {
|
|
if config.multi_agent_v2 {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_spawn_agent_tool_v2(config),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_send_message_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_assign_task_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_wait_agent_tool_v2(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_close_agent_tool_v2(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_list_agents_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandlerV2));
|
|
builder.register_handler("send_message", Arc::new(SendMessageHandlerV2));
|
|
builder.register_handler("assign_task", Arc::new(AssignTaskHandlerV2));
|
|
builder.register_handler("wait_agent", Arc::new(WaitAgentHandlerV2));
|
|
builder.register_handler("close_agent", Arc::new(CloseAgentHandlerV2));
|
|
builder.register_handler("list_agents", Arc::new(ListAgentsHandlerV2));
|
|
} else {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_spawn_agent_tool_v1(config),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_send_input_tool_v1(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_resume_agent_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler));
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_wait_agent_tool_v1(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_close_agent_tool_v1(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandler));
|
|
builder.register_handler("send_input", Arc::new(SendInputHandler));
|
|
builder.register_handler("wait_agent", Arc::new(WaitAgentHandler));
|
|
builder.register_handler("close_agent", Arc::new(CloseAgentHandler));
|
|
}
|
|
}
|
|
|
|
if config.agent_jobs_tools {
|
|
let agent_jobs_handler = Arc::new(BatchJobHandler);
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_spawn_agents_on_csv_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("spawn_agents_on_csv", agent_jobs_handler.clone());
|
|
if config.agent_jobs_worker_tools {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
create_report_agent_job_result_tool(),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler("report_agent_job_result", agent_jobs_handler);
|
|
}
|
|
}
|
|
|
|
if let Some(mcp_tools) = mcp_tools {
|
|
let mut entries: Vec<(String, rmcp::model::Tool)> = mcp_tools.into_iter().collect();
|
|
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
|
|
|
for (name, tool) in entries.into_iter() {
|
|
match mcp_tool_to_responses_api_tool(name.clone(), &tool) {
|
|
Ok(converted_tool) => {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
ToolSpec::Function(converted_tool),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler(name, mcp_handler.clone());
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to convert {name:?} MCP tool to OpenAI tool: {e:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !dynamic_tools.is_empty() {
|
|
for tool in dynamic_tools {
|
|
match dynamic_tool_to_responses_api_tool(tool) {
|
|
Ok(converted_tool) => {
|
|
push_tool_spec(
|
|
&mut builder,
|
|
ToolSpec::Function(converted_tool),
|
|
/*supports_parallel_tool_calls*/ false,
|
|
config.code_mode_enabled,
|
|
);
|
|
builder.register_handler(tool.name.clone(), dynamic_tool_handler.clone());
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(
|
|
"Failed to convert dynamic tool {:?} to OpenAI tool: {e:?}",
|
|
tool.name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
builder
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "spec_tests.rs"]
|
|
mod tests;
|