mirror of
https://github.com/openai/codex.git
synced 2026-10-30 00:09:58 +00:00
## Why `code_mode_only` filters code-mode nested tools out of the top-level tool list. For multi-agent v2, we need a rollout shape where the collaboration tools remain callable as normal model tools without also being embedded into the code-mode `exec` tool declaration. Related to this: https://openai-corpws.slack.com/archives/C0AQLHB4U75/p1778660267922549 ## What Changed - Adds `features.multi_agent_v2.non_code_mode_only`, including config resolution, profile override handling, and generated schema coverage. - Introduces `ToolExposure::DirectModelOnly` so a tool can be included in the initial model-visible list while staying out of the nested code-mode tool surface. - Applies that exposure to the multi-agent v2 tools when the new flag is set: `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `close_agent`, and `list_agents`. - Updates code-mode-only filtering so direct-model-only tools remain visible while ordinary nested code-mode tools are still hidden. ## Verification - Added config parsing/profile tests for `non_code_mode_only`. - Added tool spec coverage for the code-mode-only multi-agent v2 exposure behavior.
207 lines
6.2 KiB
Rust
207 lines
6.2 KiB
Rust
use crate::function_tool::FunctionCallError;
|
|
use crate::session::session::Session;
|
|
use crate::session::turn_context::TurnContext;
|
|
use crate::tools::context::SharedTurnDiffTracker;
|
|
use crate::tools::context::ToolInvocation;
|
|
use crate::tools::context::ToolPayload;
|
|
use crate::tools::registry::AnyToolResult;
|
|
use crate::tools::registry::ToolArgumentDiffConsumer;
|
|
use crate::tools::registry::ToolExposure;
|
|
use crate::tools::registry::ToolRegistry;
|
|
use crate::tools::spec::build_specs_with_discoverable_tools;
|
|
use codex_extension_api::ExtensionToolExecutor;
|
|
use codex_mcp::ToolInfo;
|
|
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
|
use codex_protocol::models::ResponseItem;
|
|
use codex_protocol::models::SearchToolCallParams;
|
|
use codex_tools::DiscoverableTool;
|
|
use codex_tools::ToolName;
|
|
use codex_tools::ToolSpec;
|
|
use codex_tools::ToolsConfig;
|
|
use std::sync::Arc;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tracing::instrument;
|
|
|
|
pub use crate::tools::context::ToolCallSource;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ToolCall {
|
|
pub tool_name: ToolName,
|
|
pub call_id: String,
|
|
pub payload: ToolPayload,
|
|
}
|
|
|
|
pub struct ToolRouter {
|
|
registry: ToolRegistry,
|
|
model_visible_specs: Vec<ToolSpec>,
|
|
}
|
|
|
|
pub(crate) struct ToolRouterParams<'a> {
|
|
pub(crate) mcp_tools: Option<Vec<ToolInfo>>,
|
|
pub(crate) deferred_mcp_tools: Option<Vec<ToolInfo>>,
|
|
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
|
|
pub(crate) extension_tool_executors: Vec<Arc<dyn ExtensionToolExecutor>>,
|
|
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
|
|
}
|
|
|
|
impl ToolRouter {
|
|
pub fn from_config(config: &ToolsConfig, params: ToolRouterParams<'_>) -> Self {
|
|
let ToolRouterParams {
|
|
mcp_tools,
|
|
deferred_mcp_tools,
|
|
discoverable_tools,
|
|
extension_tool_executors,
|
|
dynamic_tools,
|
|
} = params;
|
|
let builder = build_specs_with_discoverable_tools(
|
|
config,
|
|
mcp_tools,
|
|
deferred_mcp_tools,
|
|
discoverable_tools,
|
|
&extension_tool_executors,
|
|
dynamic_tools,
|
|
);
|
|
let (specs, registry) = builder.build();
|
|
let model_visible_specs = specs
|
|
.into_iter()
|
|
.filter(|spec| !is_hidden_by_code_mode_only(config, ®istry, spec))
|
|
.collect();
|
|
|
|
Self {
|
|
registry,
|
|
model_visible_specs,
|
|
}
|
|
}
|
|
|
|
pub fn model_visible_specs(&self) -> Vec<ToolSpec> {
|
|
self.model_visible_specs.clone()
|
|
}
|
|
|
|
pub(crate) fn create_diff_consumer(
|
|
&self,
|
|
tool_name: &ToolName,
|
|
) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
|
|
self.registry.create_diff_consumer(tool_name)
|
|
}
|
|
|
|
pub fn tool_supports_parallel(&self, call: &ToolCall) -> bool {
|
|
self.registry
|
|
.supports_parallel_tool_calls(&call.tool_name)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[instrument(level = "trace", skip_all, err)]
|
|
pub fn build_tool_call(item: ResponseItem) -> Result<Option<ToolCall>, FunctionCallError> {
|
|
match item {
|
|
ResponseItem::FunctionCall {
|
|
name,
|
|
namespace,
|
|
arguments,
|
|
call_id,
|
|
..
|
|
} => {
|
|
let tool_name = ToolName::new(namespace, name);
|
|
Ok(Some(ToolCall {
|
|
tool_name,
|
|
call_id,
|
|
payload: ToolPayload::Function { arguments },
|
|
}))
|
|
}
|
|
ResponseItem::ToolSearchCall {
|
|
call_id: Some(call_id),
|
|
execution,
|
|
arguments,
|
|
..
|
|
} if execution == "client" => {
|
|
let arguments: SearchToolCallParams =
|
|
serde_json::from_value(arguments).map_err(|err| {
|
|
FunctionCallError::RespondToModel(format!(
|
|
"failed to parse tool_search arguments: {err}"
|
|
))
|
|
})?;
|
|
Ok(Some(ToolCall {
|
|
tool_name: ToolName::plain("tool_search"),
|
|
call_id,
|
|
payload: ToolPayload::ToolSearch { arguments },
|
|
}))
|
|
}
|
|
ResponseItem::ToolSearchCall { .. } => Ok(None),
|
|
ResponseItem::CustomToolCall {
|
|
name,
|
|
input,
|
|
call_id,
|
|
..
|
|
} => Ok(Some(ToolCall {
|
|
tool_name: ToolName::plain(name),
|
|
call_id,
|
|
payload: ToolPayload::Custom { input },
|
|
})),
|
|
_ => Ok(None),
|
|
}
|
|
}
|
|
|
|
#[instrument(level = "trace", skip_all, err)]
|
|
pub async fn dispatch_tool_call_with_code_mode_result(
|
|
&self,
|
|
session: Arc<Session>,
|
|
turn: Arc<TurnContext>,
|
|
cancellation_token: CancellationToken,
|
|
tracker: SharedTurnDiffTracker,
|
|
call: ToolCall,
|
|
source: ToolCallSource,
|
|
) -> Result<AnyToolResult, FunctionCallError> {
|
|
let ToolCall {
|
|
tool_name,
|
|
call_id,
|
|
payload,
|
|
} = call;
|
|
|
|
let invocation = ToolInvocation {
|
|
session,
|
|
turn,
|
|
cancellation_token,
|
|
tracker,
|
|
call_id,
|
|
tool_name,
|
|
source,
|
|
payload,
|
|
};
|
|
|
|
self.registry.dispatch_any(invocation).await
|
|
}
|
|
}
|
|
|
|
fn is_hidden_by_code_mode_only(
|
|
config: &ToolsConfig,
|
|
registry: &ToolRegistry,
|
|
spec: &ToolSpec,
|
|
) -> bool {
|
|
if !config.code_mode_only_enabled || !codex_code_mode::is_code_mode_nested_tool(spec.name()) {
|
|
return false;
|
|
}
|
|
|
|
let exposure = registry
|
|
.tool_exposure(&ToolName::plain(spec.name()))
|
|
.unwrap_or(ToolExposure::Direct);
|
|
exposure != ToolExposure::DirectModelOnly
|
|
}
|
|
|
|
pub(crate) fn extension_tool_executors(session: &Session) -> Vec<Arc<dyn ExtensionToolExecutor>> {
|
|
session
|
|
.services
|
|
.extensions
|
|
.tool_contributors()
|
|
.iter()
|
|
.flat_map(|contributor| {
|
|
contributor.tools(
|
|
&session.services.session_extension_data,
|
|
&session.services.thread_extension_data,
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "router_tests.rs"]
|
|
mod tests;
|