Files
codex/codex-rs/core/src/tools/router.rs
T
jif 03edf16f0b Support plaintext collaboration tool messages (#35845)
## What changed

- Preserve `encrypted_function_args` on function calls so an empty list can mark plaintext collaboration arguments across request replay.
- Deliver `spawn_agent`, `send_message`, and `followup_task` payloads as structured plaintext agent messages when that marker is present; retain encrypted delivery otherwise.
- Redact plaintext collaboration arguments from tool and communication logs, and omit the metadata when sending requests to non-OpenAI providers.

## Testing

- Cover serialization of empty encrypted-argument metadata and plaintext versus encrypted subagent message delivery.
- Verify plaintext tool arguments are redacted and provider-specific metadata is removed from non-OpenAI requests.

GitOrigin-RevId: 64db98ff0b61a3af2f04ed609292363f2e2362a8
2026-07-28 23:49:37 +00:00

321 lines
9.8 KiB
Rust

use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::function_tool::FunctionCallError;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::ToolSearchHandlerCache;
use crate::tools::registry::AnyToolResult;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::registry::ToolRegistry;
use crate::tools::spec_plan::build_tool_router;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::SearchToolCallParams;
use codex_tools::DiscoverableTool;
use codex_tools::ToolCall as ExtensionToolCall;
use codex_tools::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
pub use crate::tools::context::ToolCallSource;
#[derive(Clone, Debug, PartialEq)]
pub struct ToolCall {
pub tool_name: ToolName,
pub call_id: String,
pub payload: ToolPayload,
pub encrypted_function_args: Option<Vec<String>>,
}
impl ToolCall {
pub(crate) fn direct_source(&self) -> ToolCallSource {
if self.tool_name.namespace.as_deref() == Some("collaboration")
&& matches!(
self.tool_name.name.as_str(),
"spawn_agent" | "send_message" | "followup_task"
)
&& self
.encrypted_function_args
.as_ref()
.is_some_and(Vec::is_empty)
{
ToolCallSource::DirectPlaintextMessage
} else {
ToolCallSource::Direct
}
}
}
pub(crate) fn tool_log_payload<'a>(
payload: &'a ToolPayload,
source: &ToolCallSource,
) -> Cow<'a, str> {
if matches!(source, ToolCallSource::DirectPlaintextMessage) {
return Cow::Borrowed("[plaintext arguments]");
}
payload.log_payload()
}
pub struct ToolRouter {
registry: ToolRegistry,
model_visible_specs: Vec<ToolSpec>,
}
pub(crate) struct ToolRouterParams<'a> {
pub(crate) tool_runtimes: Vec<Arc<dyn CoreToolRuntime>>,
pub(crate) tool_suggest_candidates: Option<ToolSuggestCandidates>,
pub(crate) extension_tool_executors: Vec<Arc<dyn ToolExecutor<ExtensionToolCall>>>,
pub(crate) wait_for_environment_tool_config: Option<Arc<crate::WaitForEnvironmentToolConfig>>,
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ToolSuggestPresentation {
ListTool,
RecommendationContext,
}
#[derive(Clone, Debug)]
pub(crate) struct ToolSuggestCandidates {
pub(crate) tools: Vec<DiscoverableTool>,
pub(crate) presentation: ToolSuggestPresentation,
}
impl ToolRouter {
pub(crate) fn from_context(
turn_context: &TurnContext,
environments: &TurnEnvironmentSnapshot,
mcp: &codex_mcp::McpBinding,
params: ToolRouterParams<'_>,
tool_search_handler_cache: &ToolSearchHandlerCache,
) -> Self {
build_tool_router(
turn_context,
environments,
mcp,
params,
tool_search_handler_cache,
)
}
pub(crate) fn from_parts(registry: ToolRegistry, model_visible_specs: Vec<ToolSpec>) -> Self {
Self {
registry,
model_visible_specs,
}
}
pub(crate) fn model_visible_specs(&self) -> Vec<ToolSpec> {
self.model_visible_specs.clone()
}
pub(crate) fn deferred_tool_namespaces(&self) -> BTreeMap<String, String> {
self.registry.deferred_tool_namespaces()
}
#[cfg(test)]
pub(crate) fn registered_tool_names_for_test(&self) -> Vec<ToolName> {
self.registry.tool_names_for_test()
}
#[cfg(test)]
pub(crate) fn tool_exposure_for_test(
&self,
name: &ToolName,
) -> Option<crate::tools::registry::ToolExposure> {
self.registry.tool_exposure(name)
}
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)
}
pub fn tool_waits_for_runtime_cancellation(&self, call: &ToolCall) -> bool {
self.registry
.waits_for_runtime_cancellation(&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,
encrypted_function_args,
call_id,
..
} => {
let tool_name = ToolName::new(namespace, name);
Ok(Some(ToolCall {
tool_name,
call_id,
payload: ToolPayload::Function { arguments },
encrypted_function_args,
}))
}
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 },
encrypted_function_args: None,
}))
}
ResponseItem::ToolSearchCall { .. } => Ok(None),
ResponseItem::CustomToolCall {
name,
namespace,
input,
call_id,
..
} => Ok(Some(ToolCall {
tool_name: ToolName::new(namespace, name),
call_id,
payload: ToolPayload::Custom { input },
encrypted_function_args: None,
})),
_ => Ok(None),
}
}
#[allow(dead_code)]
#[instrument(level = "trace", skip_all, err)]
pub async fn dispatch_tool_call_with_code_mode_result(
&self,
session: Arc<Session>,
step_context: Arc<StepContext>,
cancellation_token: CancellationToken,
tracker: SharedTurnDiffTracker,
call: ToolCall,
source: ToolCallSource,
) -> Result<AnyToolResult, FunctionCallError> {
self.dispatch_tool_call_with_code_mode_result_inner(
session,
step_context,
cancellation_token,
tracker,
call,
source,
/*terminal_outcome_reached*/ None,
)
.await
}
#[instrument(level = "trace", skip_all, err)]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn dispatch_tool_call_with_terminal_outcome(
&self,
session: Arc<Session>,
step_context: Arc<StepContext>,
cancellation_token: CancellationToken,
tracker: SharedTurnDiffTracker,
call: ToolCall,
source: ToolCallSource,
terminal_outcome_reached: Arc<AtomicBool>,
) -> Result<AnyToolResult, FunctionCallError> {
self.dispatch_tool_call_with_code_mode_result_inner(
session,
step_context,
cancellation_token,
tracker,
call,
source,
Some(terminal_outcome_reached),
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn dispatch_tool_call_with_code_mode_result_inner(
&self,
session: Arc<Session>,
step_context: Arc<StepContext>,
cancellation_token: CancellationToken,
tracker: SharedTurnDiffTracker,
call: ToolCall,
source: ToolCallSource,
terminal_outcome_reached: Option<Arc<AtomicBool>>,
) -> Result<AnyToolResult, FunctionCallError> {
let ToolCall {
tool_name,
call_id,
payload,
..
} = call;
// Keep the legacy ToolInvocation.turn field tied to the same request state until handlers migrate.
let turn = Arc::clone(&step_context.turn);
let invocation = ToolInvocation {
session,
turn,
step_context,
cancellation_token,
tracker,
call_id,
tool_name,
source,
payload,
};
self.registry
.dispatch_any_with_terminal_outcome(invocation, terminal_outcome_reached)
.await
}
}
#[instrument(level = "trace", skip_all)]
pub(crate) fn extension_tool_executors(
session: &Session,
step_store: &codex_extension_api::ExtensionData,
) -> Vec<Arc<dyn ToolExecutor<ExtensionToolCall>>> {
session
.services
.extensions
.tool_contributors()
.iter()
.flat_map(|contributor| {
contributor.tools_for_step(
&session.services.session_extension_data,
&session.services.thread_extension_data,
step_store,
)
})
.collect()
}
#[cfg(test)]
#[path = "router_tests.rs"]
mod tests;