mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
## Why With deferred executors, an environment can become ready between two sampling requests in the same turn. The model-visible environment update, advertised tools, and eventual tool execution must all describe the same request-time view. Otherwise, a request built while only environment B is ready can advertise a tool without an `environment_id`; if higher-priority environment A becomes ready before execution, that call could silently run in A instead. This PR is stacked on #29527. ## Design `run_turn` captures one `Arc<StepContext>` at each sampling-request boundary. That step owns the request's `TurnContext` and environment snapshot. - World-state environment updates and tool planning borrow that same step. - `ToolCallRuntime` retains the `Arc` while asynchronous tool calls execute. - `ToolInvocation` carries the step to handlers; its temporary `turn` compatibility field is derived from the same object. - `ToolRouter` does not retain `StepContext`; it only uses it while constructing the request's tool set. - With `DeferredExecutor` disabled, step capture keeps using the environments frozen at turn start. Simply: every sampling request gets one consistent picture of its environments, from what the model sees through where its tool calls run. ## What changed - Build environment-dependent tool specs from the current request's `StepContext`. - Use that same step for unified exec, legacy shell, `apply_patch`, `view_image`, and `request_permissions` execution. - Hide environment-backed tools, including `request_permissions`, while no environment is attached. - Resolve legacy shell paths and metadata from the selected step environment instead of the stale turn-start environment. - Capture explicit steps at non-turn-loop boundaries such as compaction, prompt debug, and startup prewarm. - Reconcile prompt-debug history from the same step used to build its tools. ## Follow-up - Bind yielded code-mode cells to the tool runtime that created them, so nested calls made after yielding continue to use the originating request's `StepContext`. ## Test plan - `just test -p codex-core deferred_executor_updates_context_and_tools_after_startup` - `just test -p codex-core environment_count_controls_environment_backed_tools` - `just test -p codex-core build_prompt_input_includes_context_and_user_message`
267 lines
8.1 KiB
Rust
267 lines
8.1 KiB
Rust
use crate::function_tool::FunctionCallError;
|
|
use crate::session::session::Session;
|
|
use crate::session::step_context::StepContext;
|
|
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::ToolArgumentDiffConsumer;
|
|
use crate::tools::registry::ToolRegistry;
|
|
use crate::tools::spec_plan::build_tool_router;
|
|
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::ToolCall as ExtensionToolCall;
|
|
use codex_tools::ToolExecutor;
|
|
use codex_tools::ToolName;
|
|
use codex_tools::ToolSpec;
|
|
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)]
|
|
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) tool_suggest_candidates: Option<ToolSuggestCandidates>,
|
|
pub(crate) extension_tool_executors: Vec<Arc<dyn ToolExecutor<ExtensionToolCall>>>,
|
|
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(
|
|
step_context: &StepContext,
|
|
params: ToolRouterParams<'_>,
|
|
tool_search_handler_cache: &ToolSearchHandlerCache,
|
|
) -> Self {
|
|
build_tool_router(step_context, params, tool_search_handler_cache)
|
|
}
|
|
|
|
pub(crate) fn from_parts(registry: ToolRegistry, model_visible_specs: Vec<ToolSpec>) -> Self {
|
|
Self {
|
|
registry,
|
|
model_visible_specs,
|
|
}
|
|
}
|
|
|
|
pub fn model_visible_specs(&self) -> Vec<ToolSpec> {
|
|
self.model_visible_specs.clone()
|
|
}
|
|
|
|
#[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,
|
|
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),
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
) -> Vec<Arc<dyn ToolExecutor<ExtensionToolCall>>> {
|
|
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;
|