core: use current step environments for tools (#29547)

## 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`
This commit is contained in:
sayan-oai
2026-06-23 20:21:13 +00:00
committed by GitHub
parent 7aab4b04bf
commit 4cc6a4bab5
36 changed files with 501 additions and 199 deletions
+12 -7
View File
@@ -18,6 +18,7 @@ use crate::hook_runtime::run_pre_compact_hooks;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_metadata::CompactionTurnMetadata;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn::built_tools;
use crate::session::turn_context::TurnContext;
use codex_analytics::CompactionImplementation;
@@ -45,7 +46,7 @@ const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: &str =
pub(crate) async fn run_inline_remote_auto_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
step_context: Arc<StepContext>,
turn_state: Arc<OnceLock<String>>,
initial_context_injection: InitialContextInjection,
reason: CompactionReason,
@@ -53,7 +54,7 @@ pub(crate) async fn run_inline_remote_auto_compact_task(
) -> CodexResult<()> {
run_remote_compact_task_inner(
&sess,
&turn_context,
&step_context,
Some(turn_state),
initial_context_injection,
CompactionTrigger::Auto,
@@ -68,6 +69,8 @@ pub(crate) async fn run_remote_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
) -> CodexResult<()> {
// Standalone compaction is its own request boundary, so it captures a fresh step.
let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await;
let start_event = EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_context.sub_id.clone(),
trace_id: turn_context.trace_id.clone(),
@@ -79,7 +82,7 @@ pub(crate) async fn run_remote_compact_task(
run_remote_compact_task_inner(
&sess,
&turn_context,
&step_context,
/*turn_state*/ None,
InitialContextInjection::DoNotInject,
CompactionTrigger::Manual,
@@ -92,13 +95,14 @@ pub(crate) async fn run_remote_compact_task(
async fn run_remote_compact_task_inner(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
step_context: &Arc<StepContext>,
turn_state: Option<Arc<OnceLock<String>>>,
initial_context_injection: InitialContextInjection,
trigger: CompactionTrigger,
reason: CompactionReason,
phase: CompactionPhase,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
let compaction_metadata = CompactionTurnMetadata::new(
trigger,
reason,
@@ -136,7 +140,7 @@ async fn run_remote_compact_task_inner(
}
let result = run_remote_compact_task_inner_impl(
sess,
turn_context,
step_context,
turn_state,
initial_context_injection,
compaction_metadata,
@@ -170,12 +174,13 @@ async fn run_remote_compact_task_inner(
async fn run_remote_compact_task_inner_impl(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
step_context: &Arc<StepContext>,
turn_state: Option<Arc<OnceLock<String>>>,
initial_context_injection: InitialContextInjection,
compaction_metadata: CompactionTurnMetadata,
analytics_details: &mut CompactionAnalyticsDetails,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
let context_compaction_item = ContextCompactionItem::new();
// Use the UI compaction item ID as the trace compaction ID so protocol lifecycle events,
// endpoint attempts, and the installed history checkpoint all have one join key.
@@ -221,7 +226,7 @@ async fn run_remote_compact_task_inner_impl(
let prompt_input = history.for_prompt(&turn_context.model_info.input_modalities);
let tool_router = built_tools(
sess.as_ref(),
turn_context.as_ref(),
step_context.as_ref(),
&CancellationToken::new(),
)
.await?;
+12 -7
View File
@@ -21,6 +21,7 @@ use crate::responses_metadata::CompactionTurnMetadata;
use crate::responses_retry::ResponsesStreamRequest;
use crate::responses_retry::handle_retryable_response_stream_error;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn::built_tools;
use crate::session::turn_context::TurnContext;
use codex_analytics::CompactionImplementation;
@@ -55,7 +56,7 @@ const MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES: u64 = 2;
pub(crate) async fn run_inline_remote_auto_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
step_context: Arc<StepContext>,
client_session: &mut ModelClientSession,
initial_context_injection: InitialContextInjection,
reason: CompactionReason,
@@ -63,7 +64,7 @@ pub(crate) async fn run_inline_remote_auto_compact_task(
) -> CodexResult<()> {
run_remote_compact_task_inner(
&sess,
&turn_context,
&step_context,
Some(client_session),
initial_context_injection,
CompactionTrigger::Auto,
@@ -77,6 +78,8 @@ pub(crate) async fn run_remote_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
) -> CodexResult<()> {
// Standalone compaction is its own request boundary, so it captures a fresh step.
let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await;
let start_event = EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_context.sub_id.clone(),
trace_id: turn_context.trace_id.clone(),
@@ -88,7 +91,7 @@ pub(crate) async fn run_remote_compact_task(
run_remote_compact_task_inner(
&sess,
&turn_context,
&step_context,
/*client_session*/ None,
InitialContextInjection::DoNotInject,
CompactionTrigger::Manual,
@@ -100,13 +103,14 @@ pub(crate) async fn run_remote_compact_task(
async fn run_remote_compact_task_inner(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
step_context: &Arc<StepContext>,
client_session: Option<&mut ModelClientSession>,
initial_context_injection: InitialContextInjection,
trigger: CompactionTrigger,
reason: CompactionReason,
phase: CompactionPhase,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
let compaction_metadata = CompactionTurnMetadata::new(
trigger,
reason,
@@ -144,7 +148,7 @@ async fn run_remote_compact_task_inner(
}
let result = run_remote_compact_task_inner_impl(
sess,
turn_context,
step_context,
client_session,
initial_context_injection,
compaction_metadata,
@@ -181,12 +185,13 @@ async fn run_remote_compact_task_inner(
async fn run_remote_compact_task_inner_impl(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
step_context: &Arc<StepContext>,
client_session: Option<&mut ModelClientSession>,
initial_context_injection: InitialContextInjection,
compaction_metadata: CompactionTurnMetadata,
analytics_details: &mut CompactionAnalyticsDetails,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
let context_compaction_item = ContextCompactionItem::new();
let compaction_trace = sess.services.rollout_thread_trace.compaction_trace_context(
turn_context.sub_id.as_str(),
@@ -229,7 +234,7 @@ async fn run_remote_compact_task_inner_impl(
let prompt_input = history.for_prompt(&turn_context.model_info.input_modalities);
let tool_router = built_tools(
sess.as_ref(),
turn_context.as_ref(),
step_context.as_ref(),
&CancellationToken::new(),
)
.await?;
+14 -2
View File
@@ -3,6 +3,7 @@ use std::sync::Arc;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
use codex_extension_api::UserInstructionsProvider;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
@@ -77,7 +78,8 @@ pub(crate) async fn build_prompt_input_from_session(
input: Vec<UserInput>,
) -> CodexResult<Vec<ResponseItem>> {
let turn_context = sess.new_default_turn().await;
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
let world_state = sess
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
.await;
if !input.is_empty() {
@@ -86,11 +88,21 @@ pub(crate) async fn build_prompt_input_from_session(
.await;
}
let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await;
if turn_context
.config
.features
.enabled(Feature::DeferredExecutor)
{
sess.record_step_environment_context_if_changed(&world_state, step_context.as_ref())
.await;
}
let prompt_input = sess
.clone_history()
.await
.for_prompt(&turn_context.model_info.input_modalities);
let router = built_tools(sess, turn_context.as_ref(), &CancellationToken::new()).await?;
let router = built_tools(sess, step_context.as_ref(), &CancellationToken::new()).await?;
let base_instructions = sess.get_base_instructions().await;
let prompt = build_prompt(
prompt_input,
+20 -2
View File
@@ -40,6 +40,7 @@ use crate::exec_policy::ExecPolicyManager;
use crate::image_preparation::prepare_response_items;
use crate::parse_turn_item;
use crate::realtime_conversation::RealtimeConversationManager;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnEnvironment;
use crate::session_prefix::format_inter_agent_completion_message;
use crate::skills::SkillRenderSideEffects;
@@ -388,7 +389,6 @@ use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::TurnModerationMetadataEvent;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::UserInput;
use codex_tools::ToolEnvironmentMode;
use codex_tools::UnifiedExecShellMode;
use codex_utils_absolute_path::AbsolutePathBuf;
#[cfg(test)]
@@ -2774,10 +2774,11 @@ impl Session {
pub(crate) async fn record_step_environment_context_if_changed(
&self,
turn_context: &TurnContext,
previous_world_state: &Arc<WorldState>,
step_context: &step_context::StepContext,
) -> Arc<WorldState> {
let turn_context = step_context.turn.as_ref();
// Render model-visible state from the same step used to build and run tools.
let world_state = Arc::new(
self.build_world_state_for_environments(turn_context, &step_context.environments)
.await,
@@ -2798,6 +2799,23 @@ impl Session {
world_state
}
pub(crate) async fn capture_step_context(
&self,
turn_context: Arc<TurnContext>,
) -> Arc<StepContext> {
// Keep the old turn-frozen view unless deferred executors are explicitly enabled.
let environments = if turn_context
.config
.features
.enabled(Feature::DeferredExecutor)
{
self.services.turn_environments.snapshot().await
} else {
turn_context.environments.clone()
};
Arc::new(StepContext::new(turn_context, environments))
}
pub(crate) async fn record_inter_agent_communication(
&self,
turn_context: &TurnContext,
+10
View File
@@ -1,7 +1,17 @@
use std::sync::Arc;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::session::turn_context::TurnContext;
/// Request-scoped state that may change between model sampling requests.
#[derive(Debug)]
pub(crate) struct StepContext {
pub(crate) turn: Arc<TurnContext>,
pub(crate) environments: TurnEnvironmentSnapshot,
}
impl StepContext {
pub(crate) fn new(turn: Arc<TurnContext>, environments: TurnEnvironmentSnapshot) -> Self {
Self { turn, environments }
}
}
+27 -7
View File
@@ -8,6 +8,7 @@ use crate::context::ContextualUserFragment;
use crate::context::TurnAborted;
use crate::environment_selection::ThreadEnvironments;
use crate::function_tool::FunctionCallError;
use crate::session::step_context::StepContext;
use crate::shell::default_user_shell;
use crate::shell_snapshot::ShellSnapshot;
use crate::skills::SkillRenderSideEffects;
@@ -182,6 +183,13 @@ use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration as StdDuration;
impl StepContext {
pub(crate) fn for_test(turn: Arc<TurnContext>) -> Arc<Self> {
let environments = turn.environments.clone();
Arc::new(Self::new(turn, environments))
}
}
mod guardian_tests;
struct InstructionsTestCase {
@@ -608,8 +616,9 @@ async fn preview_session_start_hooks(
}
fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> ToolCallRuntime {
let router = Arc::new(ToolRouter::from_turn_context(
&turn_context,
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let router = Arc::new(ToolRouter::from_context(
step_context.as_ref(),
crate::tools::router::ToolRouterParams {
tool_suggest_candidates: None,
mcp_tools: None,
@@ -620,7 +629,7 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
&Default::default(),
));
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
ToolCallRuntime::new(router, session, turn_context, tracker)
ToolCallRuntime::new(router, session, step_context, tracker)
}
fn make_connector(id: &str, name: &str) -> AppInfo {
@@ -5903,15 +5912,18 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
let call_id = "call-1".to_string();
let handler = RequestPermissionsHandler;
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let handle = tokio::spawn({
let session = Arc::clone(&session);
let turn_context = Arc::clone(&turn_context);
let step_context = Arc::clone(&step_context);
let tracker = Arc::clone(&tracker);
let call_id = call_id.clone();
async move {
handler
.handle(ToolInvocation {
session,
step_context,
turn: turn_context,
cancellation_token: CancellationToken::new(),
tracker,
@@ -5983,10 +5995,13 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
#[tokio::test]
async fn request_permissions_tool_rejects_unknown_environment_id() {
let (session, turn_context) = make_session_and_context().await;
let turn_context = Arc::new(turn_context);
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let result = RequestPermissionsHandler
.handle(ToolInvocation {
session: Arc::new(session),
turn: Arc::new(turn_context),
step_context,
turn: turn_context,
cancellation_token: CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call_id: "call-1".to_string(),
@@ -9911,8 +9926,9 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
.await
};
let deferred_mcp_tools = Some(tools.clone());
let router = ToolRouter::from_turn_context(
&turn_context,
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let router = ToolRouter::from_context(
step_context.as_ref(),
crate::tools::router::ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools,
@@ -9938,7 +9954,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
let err = router
.dispatch_tool_call_with_code_mode_result(
Arc::clone(&session),
Arc::clone(&turn_context),
step_context,
CancellationToken::new(),
tracker,
call,
@@ -10151,6 +10167,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
.expect("test setup should allow updating approval policy");
let session = Arc::new(session);
let mut turn_context = Arc::new(turn_context_raw);
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let command_script = "echo hi";
let timeout_ms = 1000;
@@ -10168,6 +10185,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
step_context,
cancellation_token: CancellationToken::new(),
tracker: Arc::clone(&turn_diff_tracker),
call_id,
@@ -10314,6 +10332,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
.expect("test setup should allow updating approval policy");
let session = Arc::new(session);
let turn_context = Arc::new(turn_context_raw);
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let handler = ExecCommandHandler::default();
@@ -10321,6 +10340,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
step_context,
cancellation_token: CancellationToken::new(),
tracker: Arc::clone(&tracker),
call_id: "exec-call".to_string(),
@@ -3,6 +3,7 @@ use crate::compact::InitialContextInjection;
use crate::exec_policy::ExecPolicyManager;
use crate::guardian::GUARDIAN_REVIEWER_NAME;
use crate::sandboxing::SandboxPermissions;
use crate::session::step_context::StepContext;
use crate::test_support::models_manager_with_provider;
use crate::tools::context::ToolCallSource;
use crate::tools::context::ToolOutput;
@@ -329,10 +330,12 @@ async fn guardian_allows_shell_command_additional_permissions_requests_past_poli
);
#[allow(deprecated)]
let workdir = Some(turn_context.cwd.to_string_lossy().to_string());
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
step_context,
cancellation_token: CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call_id: "test-call".to_string(),
@@ -433,10 +436,12 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_
);
#[allow(deprecated)]
let workdir = Some(turn_context.cwd.to_string_lossy().to_string());
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
step_context,
cancellation_token: CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call_id: "strict-shell-command-call".to_string(),
@@ -478,12 +483,14 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic
let session = Arc::new(session);
let turn_context = Arc::new(turn_context_raw);
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let handler = ExecCommandHandler::default();
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
step_context,
cancellation_token: CancellationToken::new(),
tracker: Arc::clone(&tracker),
call_id: "exec-call".to_string(),
@@ -608,10 +615,12 @@ async fn shell_command_allows_sticky_turn_permissions_without_inline_request_per
);
#[allow(deprecated)]
let workdir = Some(turn_context.cwd.to_string_lossy().to_string());
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
step_context,
cancellation_token: CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call_id: "sticky-turn-grant".to_string(),
+34 -29
View File
@@ -236,6 +236,8 @@ pub(crate) async fn run_turn(
)
.await;
// Capture once so context, advertised tools, and tool calls share one request view.
let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await;
let sampling_request_result: CodexResult<_> = async {
super::time_reminder::maybe_record_current_time_reminder(
sess.as_ref(),
@@ -249,15 +251,8 @@ pub(crate) async fn run_turn(
.features
.enabled(Feature::DeferredExecutor)
{
let step_context = StepContext {
environments: sess.services.turn_environments.snapshot().await,
};
world_state = sess
.record_step_environment_context_if_changed(
turn_context.as_ref(),
&world_state,
&step_context,
)
.record_step_environment_context_if_changed(&world_state, step_context.as_ref())
.await;
}
@@ -277,7 +272,7 @@ pub(crate) async fn run_turn(
);
run_sampling_request(
Arc::clone(&sess),
Arc::clone(&turn_context),
Arc::clone(&step_context),
Arc::clone(&turn_extension_data),
Arc::clone(&turn_diff_tracker),
&mut client_session,
@@ -363,7 +358,7 @@ pub(crate) async fn run_turn(
{
if let Err(err) = run_auto_compact(
&sess,
&turn_context,
Arc::clone(&step_context),
&mut client_session,
InitialContextInjection::BeforeLastUserMessage(Arc::clone(&world_state)),
CompactionReason::ContextLimit,
@@ -886,9 +881,10 @@ async fn run_pre_sampling_compact(
let token_status = auto_compact_token_status(sess.as_ref(), turn_context.as_ref()).await;
// Compact if the configured auto-compaction budget or usable context window is exhausted.
if token_status.token_limit_reached {
let step_context = sess.capture_step_context(Arc::clone(turn_context)).await;
run_auto_compact(
sess,
turn_context,
step_context,
client_session,
InitialContextInjection::DoNotInject,
CompactionReason::ContextLimit,
@@ -930,9 +926,12 @@ async fn maybe_run_previous_model_inline_compact(
);
if should_compact_for_comp_hash_change {
let step_context = sess
.capture_step_context(Arc::clone(&previous_model_turn_context))
.await;
run_auto_compact(
sess,
&previous_model_turn_context,
step_context,
client_session,
InitialContextInjection::DoNotInject,
CompactionReason::CompHashChanged,
@@ -967,9 +966,12 @@ async fn maybe_run_previous_model_inline_compact(
&& previous_model_turn_context.model_info.slug != turn_context.model_info.slug
&& old_context_window > new_context_window;
if should_run {
let step_context = sess
.capture_step_context(Arc::clone(&previous_model_turn_context))
.await;
run_auto_compact(
sess,
&previous_model_turn_context,
step_context,
client_session,
InitialContextInjection::DoNotInject,
CompactionReason::ModelDownshift,
@@ -987,12 +989,13 @@ async fn maybe_run_previous_model_inline_compact(
)]
async fn run_auto_compact(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
step_context: Arc<StepContext>,
client_session: &mut ModelClientSession,
initial_context_injection: InitialContextInjection,
reason: CompactionReason,
phase: CompactionPhase,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
if should_use_remote_compact_task(turn_context.provider.info()) {
if turn_context
.config
@@ -1006,7 +1009,7 @@ async fn run_auto_compact(
);
run_inline_remote_auto_compact_task_v2(
Arc::clone(sess),
Arc::clone(turn_context),
step_context,
client_session,
initial_context_injection,
reason,
@@ -1022,7 +1025,7 @@ async fn run_auto_compact(
);
run_inline_remote_auto_compact_task(
Arc::clone(sess),
Arc::clone(turn_context),
step_context,
client_session.turn_state(),
initial_context_injection,
reason,
@@ -1122,14 +1125,14 @@ pub(crate) fn build_prompt(
#[instrument(level = "trace",
skip_all,
fields(
turn_id = %turn_context.sub_id,
model = %turn_context.model_info.slug,
cwd = %turn_context.cwd.display()
turn_id = %step_context.turn.sub_id,
model = %step_context.turn.model_info.slug,
cwd = %step_context.turn.cwd.display()
)
)]
async fn run_sampling_request(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
step_context: Arc<StepContext>,
turn_store: Arc<codex_extension_api::ExtensionData>,
turn_diff_tracker: SharedTurnDiffTracker,
client_session: &mut ModelClientSession,
@@ -1137,19 +1140,20 @@ async fn run_sampling_request(
input: Vec<ResponseItem>,
cancellation_token: CancellationToken,
) -> CodexResult<(SamplingRequestResult, Vec<ResponseItem>)> {
let router = built_tools(sess.as_ref(), turn_context.as_ref(), &cancellation_token).await?;
let turn_context = Arc::clone(&step_context.turn);
let router = built_tools(sess.as_ref(), step_context.as_ref(), &cancellation_token).await?;
let base_instructions = sess.get_base_instructions().await;
let tool_runtime = ToolCallRuntime::new(
Arc::clone(&router),
Arc::clone(&sess),
Arc::clone(&turn_context),
Arc::clone(&step_context),
Arc::clone(&turn_diff_tracker),
);
let _code_mode_worker = sess.services.code_mode_service.start_turn_worker(
&sess,
&turn_context,
Arc::clone(&step_context),
Arc::clone(&router),
Arc::clone(&turn_diff_tracker),
);
@@ -1226,16 +1230,17 @@ async fn run_sampling_request(
#[instrument(level = "trace",
skip_all,
fields(
turn_id = %turn_context.sub_id,
model = %turn_context.model_info.slug,
apps_enabled = turn_context.apps_enabled()
turn_id = %step_context.turn.sub_id,
model = %step_context.turn.model_info.slug,
apps_enabled = step_context.turn.apps_enabled()
)
)]
pub(crate) async fn built_tools(
sess: &Session,
turn_context: &TurnContext,
step_context: &StepContext,
cancellation_token: &CancellationToken,
) -> CodexResult<Arc<ToolRouter>> {
let turn_context = step_context.turn.as_ref();
let mcp_connection_manager = sess.services.mcp_connection_manager.load_full();
let has_mcp_servers = mcp_connection_manager.has_servers();
let all_mcp_tools = mcp_connection_manager
@@ -1351,8 +1356,8 @@ pub(crate) async fn built_tools(
);
let mcp_tools = has_mcp_servers.then_some(mcp_tool_exposure.direct_tools);
let deferred_mcp_tools = mcp_tool_exposure.deferred_tools;
Ok(Arc::new(ToolRouter::from_turn_context(
turn_context,
Ok(Arc::new(ToolRouter::from_context(
step_context,
ToolRouterParams {
mcp_tools,
deferred_mcp_tools,
-11
View File
@@ -207,10 +207,6 @@ impl TurnContext {
&& self.config.orchestrator_mcp_enabled
}
pub(crate) fn tool_environment_mode(&self) -> ToolEnvironmentMode {
ToolEnvironmentMode::from_count(self.environments.turn_environments.len())
}
pub(crate) async fn with_model(
&self,
model: String,
@@ -303,13 +299,6 @@ impl TurnContext {
}
}
#[deprecated(note = "resolve paths from the selected turn environment cwd instead")]
pub(crate) fn resolve_path(&self, path: Option<String>) -> AbsolutePathBuf {
#[allow(deprecated)]
path.as_ref()
.map_or_else(|| self.cwd.clone(), |path| self.cwd.join(path))
}
pub(crate) fn file_system_sandbox_context(
&self,
additional_permissions: Option<AdditionalPermissionProfile>,
+4 -1
View File
@@ -258,9 +258,12 @@ async fn schedule_startup_prewarm_inner(
}
let startup_cancellation_token = CancellationToken::new();
let built_tools_started_at = Instant::now();
let step_context = session
.capture_step_context(Arc::clone(&startup_turn_context))
.await;
let startup_router = built_tools(
session.as_ref(),
startup_turn_context.as_ref(),
step_context.as_ref(),
&startup_cancellation_token,
)
.await?;
@@ -8,6 +8,7 @@ use super::image_generation_artifact_path;
use super::last_assistant_message_from_item;
use super::response_item_may_include_external_context;
use super::save_image_generation_result;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::tools::ToolRouter;
use crate::tools::parallel::ToolCallRuntime;
@@ -279,8 +280,9 @@ async fn handle_output_item_done_returns_contributed_last_agent_message() {
session.services.extensions = Arc::new(builder.build());
let session = Arc::new(session);
let turn_context = Arc::new(turn_context);
let router = Arc::new(ToolRouter::from_turn_context(
&turn_context,
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let router = Arc::new(ToolRouter::from_context(
step_context.as_ref(),
crate::tools::router::ToolRouterParams {
tool_suggest_candidates: None,
mcp_tools: None,
@@ -291,12 +293,7 @@ async fn handle_output_item_done_returns_contributed_last_agent_message() {
&Default::default(),
));
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let tool_runtime = ToolCallRuntime::new(
router,
Arc::clone(&session),
Arc::clone(&turn_context),
tracker,
);
let tool_runtime = ToolCallRuntime::new(router, Arc::clone(&session), step_context, tracker);
let item = assistant_output_text("original assistant text");
let mut ctx = HandleOutputCtx {
sess: session,
@@ -17,6 +17,7 @@ use tokio_util::sync::CancellationToken;
use super::ExecContext;
use super::PUBLIC_TOOL_NAME;
use super::call_nested_tool;
use crate::session::step_context::StepContext;
use crate::tools::ToolRouter;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::parallel::ToolCallRuntime;
@@ -49,14 +50,11 @@ impl CodeModeDispatchBroker {
&self,
exec: ExecContext,
router: Arc<ToolRouter>,
step_context: Arc<StepContext>,
tracker: SharedTurnDiffTracker,
) -> CodeModeDispatchWorker {
let tool_runtime = ToolCallRuntime::new(
router,
Arc::clone(&exec.session),
Arc::clone(&exec.turn),
tracker,
);
let tool_runtime =
ToolCallRuntime::new(router, Arc::clone(&exec.session), step_context, tracker);
let host = Arc::new(CoreTurnHost { exec, tool_runtime });
let dispatch_rx = self.dispatch_rx.clone();
let dispatch_gates = Arc::clone(&self.dispatch_gates);
+4 -2
View File
@@ -21,6 +21,7 @@ use crate::function_tool::FunctionCallError;
use crate::original_image_detail::can_request_original_image_detail;
use crate::original_image_detail::sanitize_original_image_detail as sanitize_image_detail_items;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use crate::tools::ToolRouter;
use crate::tools::context::FunctionToolOutput;
@@ -113,10 +114,11 @@ impl CodeModeService {
pub(crate) fn start_turn_worker(
&self,
session: &Arc<Session>,
turn: &Arc<TurnContext>,
step_context: Arc<StepContext>,
router: Arc<ToolRouter>,
tracker: SharedTurnDiffTracker,
) -> Option<CodeModeDispatchWorker> {
let turn = &step_context.turn;
let tool_mode = effective_tool_mode(turn);
if !matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly)
|| self.session.is_none()
@@ -130,7 +132,7 @@ impl CodeModeService {
};
Some(
self.dispatch_broker
.start_turn_worker(exec, router, tracker),
.start_turn_worker(exec, router, step_context, tracker),
)
}
+3
View File
@@ -1,6 +1,7 @@
use crate::context_manager::truncate_function_output_payload;
use crate::original_image_detail::sanitize_original_image_detail;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use crate::tools::TELEMETRY_PREVIEW_MAX_BYTES;
use crate::tools::TELEMETRY_PREVIEW_MAX_LINES;
@@ -53,7 +54,9 @@ pub enum ToolCallSource {
#[derive(Clone)]
pub struct ToolInvocation {
pub session: Arc<Session>,
// TODO(sayan): Remove this compatibility field once handlers use `step_context.turn`.
pub turn: Arc<TurnContext>,
pub(crate) step_context: Arc<StepContext>,
pub cancellation_token: CancellationToken,
pub tracker: SharedTurnDiffTracker,
pub call_id: String,
@@ -348,6 +348,7 @@ impl ApplyPatchHandler {
let ToolInvocation {
session,
turn,
step_context,
tracker,
call_id,
tool_name,
@@ -372,8 +373,10 @@ impl ApplyPatchHandler {
require_environment_id(args.environment_id.as_deref(), self.multi_environment)?;
// Verify the parsed patch against the selected environment filesystem.
let Some(turn_environment) =
resolve_tool_environment(turn.as_ref(), selected_environment_id.as_deref())?
let Some(turn_environment) = resolve_tool_environment(
&step_context.environments,
selected_environment_id.as_deref(),
)?
else {
return Err(FunctionCallError::RespondToModel(
"apply_patch is unavailable in this session".to_string(),
@@ -13,6 +13,7 @@ use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::Mutex;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::tools::context::ToolInvocation;
use crate::tools::hook_names::HookToolName;
@@ -29,9 +30,11 @@ fn sample_patch() -> &'static str {
async fn invocation_for_payload(payload: ToolPayload) -> ToolInvocation {
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-apply-patch".to_string(),
@@ -112,8 +112,9 @@ impl TurnItemEmitter for CoreTurnItemEmitter {
async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
let conversation_history =
ConversationHistory::new(invocation.session.clone_history().await.into_raw_items());
let mut environments = Vec::with_capacity(invocation.turn.environments.turn_environments.len());
for environment in &invocation.turn.environments.turn_environments {
let mut environments =
Vec::with_capacity(invocation.step_context.environments.turn_environments.len());
for environment in &invocation.step_context.environments.turn_environments {
// TODO(anp): Migrate extension ToolEnvironment and granted-permission lookup to PathUri
// so extensions can receive foreign environment cwd values.
let Ok(native_cwd) = environment.cwd().to_abs_path() else {
@@ -175,6 +176,7 @@ mod tests {
use super::CoreTurnItemEmitter;
use super::ExtensionToolAdapter;
use crate::session::step_context::StepContext;
use crate::tools::context::ToolCallSource;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -272,9 +274,11 @@ mod tests {
async fn exposes_generic_hook_payloads() {
let handler = ExtensionToolAdapter::new(Arc::new(StubExtensionExecutor));
let (session, turn) = crate::session::tests::make_session_and_context().await;
let turn = Arc::new(turn);
let invocation = ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call_id: "call-extension".to_string(),
@@ -341,8 +345,10 @@ mod tests {
panic!("expected raw response item event");
};
assert_eq!(raw_history_item.item, expected_history_item);
let step_context = StepContext::for_test(Arc::clone(&turn));
let invocation = ToolInvocation {
session,
step_context,
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
@@ -542,8 +548,10 @@ mod tests {
&session.thread_id.to_string(),
"call-image",
);
let step_context = StepContext::for_test(Arc::clone(&turn));
let invocation = ToolInvocation {
session,
step_context,
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
+14 -4
View File
@@ -140,6 +140,7 @@ impl McpHandler {
};
let started = Instant::now();
// TODO(sayan): Use StepContext for MCP file arguments when MCP follows dynamic environments.
let result = handle_mcp_tool_call(
Arc::clone(&session),
&turn,
@@ -317,6 +318,7 @@ mod search_tests;
#[cfg(test)]
mod tests {
use super::*;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::tools::context::ToolCallSource;
use crate::tools::hook_names::HookToolName;
@@ -340,12 +342,14 @@ mod tests {
.to_string(),
};
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let handler = McpHandler::new(tool_info("memory", "memory", "create_entities"))
.expect("MCP tool spec should build");
assert_eq!(
handler.pre_tool_use_payload(&ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-mcp-pre".to_string(),
@@ -371,13 +375,15 @@ mod tests {
arguments: json!({ "message": "hello" }).to_string(),
};
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let handler = McpHandler::new(tool_info("foo", "mcp__foo", "exec_command"))
.expect("MCP tool spec should build");
assert_eq!(
handler.pre_tool_use_payload(&ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-mcp-pre-builtin-like".to_string(),
@@ -398,6 +404,7 @@ mod tests {
arguments: json!({ "message": "hello" }).to_string(),
};
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let handler = McpHandler::new(tool_info("foo", "mcp__foo", "exec_command"))
.expect("MCP tool spec should build");
@@ -405,7 +412,8 @@ mod tests {
.with_updated_hook_input(
ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-mcp-rewrite-builtin-like".to_string(),
@@ -448,11 +456,13 @@ mod tests {
truncation_policy: codex_utils_output_truncation::TruncationPolicy::Bytes(1024),
};
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let handler = McpHandler::new(tool_info("filesystem", "filesystem", "read_file"))
.expect("MCP tool spec should build");
let invocation = ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-mcp-post".to_string(),
+4 -4
View File
@@ -46,10 +46,10 @@ use serde_json::Map;
use serde_json::Value;
use std::path::Path;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::function_tool::FunctionCallError;
use crate::sandboxing::SandboxPermissions;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::session::turn_context::TurnEnvironment;
pub(crate) use crate::tools::code_mode::CodeModeExecuteHandler;
pub(crate) use crate::tools::code_mode::CodeModeWaitHandler;
@@ -153,13 +153,13 @@ fn resolve_workdir_base_path(
}
fn resolve_tool_environment<'a>(
turn: &'a TurnContext,
environments: &'a TurnEnvironmentSnapshot,
environment_id: Option<&str>,
) -> Result<Option<&'a TurnEnvironment>, FunctionCallError> {
environment_id.map_or_else(
|| Ok(turn.environments.primary()),
|| Ok(environments.primary()),
|environment_id| {
turn.environments
environments
.turn_environments
.iter()
.find(|environment| environment.environment_id == environment_id)
@@ -4,6 +4,7 @@ use crate::config::AgentRoleConfig;
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
use crate::function_tool::FunctionCallError;
use crate::init_state_db;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::session_prefix::format_inter_agent_completion_message;
use crate::thread_manager::thread_store_from_config;
@@ -73,8 +74,10 @@ fn invocation(
tool_name: &str,
payload: ToolPayload,
) -> ToolInvocation {
let step_context = StepContext::for_test(Arc::clone(&turn));
ToolInvocation {
session,
step_context,
turn,
cancellation_token: CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::default())),
@@ -47,6 +47,7 @@ impl RequestPermissionsHandler {
let ToolInvocation {
session,
turn,
step_context,
cancellation_token,
call_id,
payload,
@@ -63,8 +64,10 @@ impl RequestPermissionsHandler {
};
let environment_args: RequestPermissionsEnvironmentArgs = parse_arguments(&arguments)?;
let Some(turn_environment) =
resolve_tool_environment(turn.as_ref(), environment_args.environment_id.as_deref())?
let Some(turn_environment) = resolve_tool_environment(
&step_context.environments,
environment_args.environment_id.as_deref(),
)?
else {
return Err(FunctionCallError::RespondToModel(
"request_permissions requires a primary environment".to_string(),
@@ -1,4 +1,5 @@
use super::*;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -21,13 +22,15 @@ async fn multi_agent_v2_request_user_input_rejects_subagent_threads() {
agent_nickname: None,
agent_role: None,
});
let turn = Arc::new(turn);
let result = RequestUserInputHandler {
available_modes: Vec::new(),
}
.handle(ToolInvocation {
session: Arc::new(session),
turn: Arc::new(turn),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::default())),
call_id: "call-1".to_string(),
+3 -5
View File
@@ -8,6 +8,7 @@ use crate::exec::ExecParams;
use crate::exec_policy::ExecApprovalRequest;
use crate::function_tool::FunctionCallError;
use crate::session::turn_context::TurnContext;
use crate::session::turn_context::TurnEnvironment;
use crate::shell::ShellType;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolPayload;
@@ -53,6 +54,7 @@ struct RunExecLikeArgs {
prefix_rule: Option<Vec<String>>,
session: Arc<crate::session::session::Session>,
turn: Arc<TurnContext>,
turn_environment: TurnEnvironment,
tracker: crate::tools::context::SharedTurnDiffTracker,
call_id: String,
shell_runtime_backend: ShellRuntimeBackend,
@@ -69,16 +71,12 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
prefix_rule,
session,
turn,
turn_environment,
tracker,
call_id,
shell_runtime_backend,
} = args;
let Some(turn_environment) = turn.environments.primary() else {
return Err(FunctionCallError::RespondToModel(
"shell is unavailable in this session".to_string(),
));
};
let fs = turn_environment.environment.get_filesystem();
let explicit_env_overrides = turn
@@ -1,7 +1,7 @@
use codex_protocol::ThreadId;
use codex_protocol::models::ShellCommandToolCallParams;
use codex_tools::ShellCommandBackendConfig;
use codex_tools::ToolName;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecParams;
@@ -9,6 +9,7 @@ use crate::exec_env::create_env;
use crate::function_tool::FunctionCallError;
use crate::maybe_emit_implicit_skill_invocation;
use crate::session::turn_context::TurnContext;
use crate::session::turn_context::TurnEnvironment;
use crate::shell::Shell;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -86,14 +87,17 @@ impl ShellCommandHandler {
params: &ShellCommandToolCallParams,
session: &crate::session::session::Session,
turn_context: &TurnContext,
thread_id: ThreadId,
turn_environment: &TurnEnvironment,
cwd: AbsolutePathBuf,
allow_login_shell: bool,
) -> Result<ExecParams, FunctionCallError> {
let shell = session.user_shell();
let session_shell = session.user_shell();
let shell = turn_environment
.shell
.as_ref()
.unwrap_or(session_shell.as_ref());
let use_login_shell = Self::resolve_use_login_shell(params.login, allow_login_shell)?;
let command = Self::base_command(shell.as_ref(), &params.command, use_login_shell);
#[allow(deprecated)]
let cwd = turn_context.resolve_path(params.workdir.clone());
let command = Self::base_command(shell, &params.command, use_login_shell);
Ok(ExecParams {
command,
@@ -102,13 +106,10 @@ impl ShellCommandHandler {
capture_policy: ExecCapturePolicy::ShellTool,
env: create_env(
&turn_context.config.permissions.shell_environment_policy,
Some(thread_id),
Some(session.thread_id),
),
network: turn_context.network.clone(),
network_environment_id: turn_context
.environments
.primary()
.map(|environment| environment.environment_id.clone()),
network_environment_id: Some(turn_environment.environment_id.clone()),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
windows_sandbox_level: turn_context.windows_sandbox_level,
windows_sandbox_private_desktop: turn_context
@@ -160,6 +161,7 @@ impl ShellCommandHandler {
let ToolInvocation {
session,
turn,
step_context,
cancellation_token,
tracker,
call_id,
@@ -174,16 +176,25 @@ impl ShellCommandHandler {
)));
};
#[allow(deprecated)]
let cwd = resolve_workdir_base_path(&arguments, &turn.cwd)?;
let Some(turn_environment) = step_context.environments.primary().cloned() else {
return Err(FunctionCallError::RespondToModel(
"shell is unavailable in this session".to_string(),
));
};
let environment_cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
FunctionCallError::RespondToModel(format!(
"shell_command cwd `{}` is not native to the Codex host: {err}",
turn_environment.cwd()
))
})?;
let cwd = resolve_workdir_base_path(&arguments, &environment_cwd)?;
let params: ShellCommandToolCallParams = parse_arguments_with_base_path(&arguments, &cwd)?;
#[allow(deprecated)]
let workdir = turn.resolve_path(params.workdir.clone());
maybe_emit_implicit_skill_invocation(
session.as_ref(),
turn.as_ref(),
&params.command,
&workdir,
&cwd,
)
.await;
let prefix_rule = params.prefix_rule.clone();
@@ -191,10 +202,16 @@ impl ShellCommandHandler {
&params,
session.as_ref(),
turn.as_ref(),
session.thread_id,
&turn_environment,
cwd,
turn.config.permissions.allow_login_shell,
)?;
let shell_type = Some(session.user_shell().shell_type);
let shell_type = Some(
turn_environment
.shell
.as_ref()
.map_or_else(|| session.user_shell().shell_type, |shell| shell.shell_type),
);
run_exec_like(RunExecLikeArgs {
tool_name,
exec_params,
@@ -205,6 +222,7 @@ impl ShellCommandHandler {
prefix_rule,
session,
turn,
turn_environment,
tracker,
call_id,
shell_runtime_backend: self.shell_runtime_backend(),
+45 -10
View File
@@ -6,7 +6,9 @@ use pretty_assertions::assert_eq;
use crate::exec_env::create_env;
use crate::sandboxing::SandboxPermissions;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::session::turn_context::TurnEnvironment;
use crate::shell::Shell;
use crate::shell::ShellType;
use crate::tools::context::FunctionToolOutput;
@@ -20,6 +22,7 @@ use crate::turn_diff_tracker::TurnDiffTracker;
use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_shell_command::powershell::try_find_powershell_executable_blocking;
use codex_shell_command::powershell::try_find_pwsh_executable_blocking;
use codex_utils_path_uri::PathUri;
use serde_json::json;
use tokio::sync::Mutex;
@@ -67,7 +70,7 @@ fn assert_safe(shell: &Shell, command: &str) {
}
#[tokio::test]
async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_context() {
async fn shell_command_handler_to_exec_params_uses_selected_environment() {
let (session, turn_context) = make_session_and_context().await;
let command = "echo hello".to_string();
@@ -77,11 +80,25 @@ async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_contex
let sandbox_permissions = SandboxPermissions::RequireEscalated;
let justification = Some("because tests".to_string());
let expected_command = session
.user_shell()
.derive_exec_args(&command, /*use_login_shell*/ true);
#[allow(deprecated)]
let expected_cwd = turn_context.resolve_path(workdir.clone());
let selected_shell = Shell {
shell_type: ShellType::Bash,
shell_path: PathBuf::from("/selected/bin/bash"),
};
let expected_command = selected_shell.derive_exec_args(&command, /*use_login_shell*/ true);
let selected_cwd = turn_context.config.cwd.join("selected-environment");
let expected_cwd = selected_cwd.join("subdir");
let selected_environment = TurnEnvironment::new(
"selected-environment".to_string(),
Arc::clone(
&turn_context
.environments
.primary()
.expect("primary environment")
.environment,
),
PathUri::from_abs_path(&selected_cwd),
Some(selected_shell),
);
let expected_env = create_env(
&turn_context.config.permissions.shell_environment_policy,
Some(session.thread_id),
@@ -102,7 +119,8 @@ async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_contex
&params,
&session,
&turn_context,
session.thread_id,
&selected_environment,
expected_cwd.clone(),
/*allow_login_shell*/ true,
)
.expect("login shells should be allowed");
@@ -112,6 +130,10 @@ async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_contex
assert_eq!(exec_params.cwd, expected_cwd);
assert_eq!(exec_params.env, expected_env);
assert_eq!(exec_params.network, turn_context.network);
assert_eq!(
exec_params.network_environment_id.as_deref(),
Some("selected-environment")
);
assert_eq!(exec_params.expiration.timeout_ms(), timeout_ms);
assert_eq!(exec_params.sandbox_permissions, sandbox_permissions);
assert_eq!(exec_params.justification, justification);
@@ -149,6 +171,14 @@ fn shell_command_handler_respects_explicit_login_flag() {
#[tokio::test]
async fn shell_command_handler_defaults_to_non_login_when_disallowed() {
let (session, turn_context) = make_session_and_context().await;
let turn_environment = turn_context
.environments
.primary()
.expect("primary environment");
let cwd = turn_environment
.cwd()
.to_abs_path()
.expect("native environment cwd");
let params = ShellCommandToolCallParams {
command: "echo hello".to_string(),
workdir: None,
@@ -164,7 +194,8 @@ async fn shell_command_handler_defaults_to_non_login_when_disallowed() {
&params,
&session,
&turn_context,
session.thread_id,
turn_environment,
cwd,
/*allow_login_shell*/ false,
)
.expect("non-login shells should still be allowed");
@@ -196,12 +227,14 @@ async fn shell_command_pre_tool_use_payload_uses_raw_command() {
arguments: json!({ "command": "printf shell command" }).to_string(),
};
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let handler = ShellCommandHandler::from(codex_tools::ShellCommandBackendConfig::Classic);
assert_eq!(
handler.pre_tool_use_payload(&ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-42".to_string(),
@@ -228,9 +261,11 @@ async fn build_post_tool_use_payload_uses_tool_output_wire_value() {
};
let handler = ShellCommandHandler::from(codex_tools::ShellCommandBackendConfig::Classic);
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let invocation = ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-42".to_string(),
@@ -110,6 +110,7 @@ impl ExecCommandHandler {
let ToolInvocation {
session,
turn,
step_context,
tracker,
call_id,
payload,
@@ -128,8 +129,10 @@ impl ExecCommandHandler {
let manager: &UnifiedExecProcessManager = &session.services.unified_exec_manager;
let context = UnifiedExecContext::new(session.clone(), turn.clone(), call_id.clone());
let environment_args: ExecCommandEnvironmentArgs = parse_arguments(&arguments)?;
let Some(turn_environment) =
resolve_tool_environment(turn.as_ref(), environment_args.environment_id.as_deref())?
let Some(turn_environment) = resolve_tool_environment(
&step_context.environments,
environment_args.environment_id.as_deref(),
)?
else {
return Err(FunctionCallError::RespondToModel(
"unified exec is unavailable in this session".to_string(),
@@ -9,6 +9,7 @@ use codex_utils_output_truncation::TruncationPolicy;
use pretty_assertions::assert_eq;
use std::sync::Arc;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::tools::context::ExecCommandToolOutput;
use crate::tools::context::ToolCallSource;
@@ -27,9 +28,11 @@ async fn invocation_for_payload(
payload: ToolPayload,
) -> ToolInvocation {
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: call_id.to_string(),
@@ -236,12 +239,14 @@ async fn exec_command_pre_tool_use_payload_uses_raw_command() {
arguments: serde_json::json!({ "cmd": "printf exec command" }).to_string(),
};
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let handler = ExecCommandHandler::default();
assert_eq!(
handler.pre_tool_use_payload(&ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-43".to_string(),
@@ -262,12 +267,14 @@ async fn exec_command_pre_tool_use_payload_skips_write_stdin() {
arguments: serde_json::json!({ "chars": "echo hi" }).to_string(),
};
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let handler = WriteStdinHandler;
assert_eq!(
handler.pre_tool_use_payload(&ToolInvocation {
session: session.into(),
turn: turn.into(),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-44".to_string(),
+12 -4
View File
@@ -100,6 +100,7 @@ impl ViewImageHandler {
let ToolInvocation {
session,
turn,
step_context,
payload,
call_id,
..
@@ -133,7 +134,7 @@ impl ViewImageHandler {
};
let Some(turn_environment) =
resolve_tool_environment(turn.as_ref(), environment_id.as_deref())?
resolve_tool_environment(&step_context.environments, environment_id.as_deref())?
else {
return Err(FunctionCallError::RespondToModel(
"view_image is unavailable in this session".to_string(),
@@ -245,6 +246,7 @@ impl ToolOutput for ViewImageOutput {
#[cfg(test)]
mod tests {
use super::*;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::session::turn_context::TurnEnvironment;
use crate::tools::context::ToolCallSource;
@@ -314,11 +316,13 @@ mod tests {
let image_path = image_cwd.join("image.png");
std::fs::write(image_path.as_path(), b"not a real image").expect("write test image");
turn.permission_profile = PermissionProfile::read_only();
let turn = Arc::new(turn);
let result = ViewImageHandler::default()
.handle(ToolInvocation {
session: Arc::new(session),
turn: Arc::new(turn),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-view-image".to_string(),
@@ -342,11 +346,13 @@ mod tests {
#[tokio::test]
async fn handle_rejects_unsupported_detail() {
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let result = ViewImageHandler::default()
.handle(ToolInvocation {
session: Arc::new(session),
turn: Arc::new(turn),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-view-image".to_string(),
@@ -377,11 +383,13 @@ mod tests {
let image_path = image_cwd.join("image.png");
std::fs::write(image_path.as_path(), b"not a real image").expect("write test image");
turn.permission_profile = PermissionProfile::Disabled;
let turn = Arc::new(turn);
let result = ViewImageHandler::default()
.handle(ToolInvocation {
session: Arc::new(session),
turn: Arc::new(turn),
step_context: StepContext::for_test(Arc::clone(&turn)),
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-view-image".to_string(),
+13 -8
View File
@@ -14,7 +14,7 @@ use tracing::trace_span;
use crate::function_tool::FunctionCallError;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::session::step_context::StepContext;
use crate::tools::context::AbortedToolOutput;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolPayload;
@@ -31,7 +31,8 @@ use codex_protocol::models::ResponseInputItem;
pub(crate) struct ToolCallRuntime {
router: Arc<ToolRouter>,
session: Arc<Session>,
turn_context: Arc<TurnContext>,
// Tool calls may run later, so retain the step whose tool list advertised them.
step_context: Arc<StepContext>,
tracker: SharedTurnDiffTracker,
parallel_execution: Arc<RwLock<()>>,
}
@@ -40,13 +41,13 @@ impl ToolCallRuntime {
pub(crate) fn new(
router: Arc<ToolRouter>,
session: Arc<Session>,
turn_context: Arc<TurnContext>,
step_context: Arc<StepContext>,
tracker: SharedTurnDiffTracker,
) -> Self {
Self {
router,
session,
turn_context,
step_context,
tracker,
parallel_execution: Arc::new(RwLock::new(())),
}
@@ -88,7 +89,8 @@ impl ToolCallRuntime {
let supports_parallel = self.router.tool_supports_parallel(&call);
let router = Arc::clone(&self.router);
let session = Arc::clone(&self.session);
let turn = Arc::clone(&self.turn_context);
let step_context = Arc::clone(&self.step_context);
let turn = Arc::clone(&step_context.turn);
let tracker = Arc::clone(&self.tracker);
let lock = Arc::clone(&self.parallel_execution);
let invocation_cancellation_token = cancellation_token.clone();
@@ -121,7 +123,7 @@ impl ToolCallRuntime {
router
.dispatch_tool_call_with_terminal_outcome(
session,
turn,
step_context,
invocation_cancellation_token,
tracker,
dispatch_call,
@@ -240,6 +242,7 @@ mod tests {
use super::*;
use std::time::Duration;
use crate::session::step_context::StepContext;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::registry::CoreToolRuntime;
@@ -423,12 +426,13 @@ mod tests {
let handler = Arc::new(ImmediateHandler {
tool_name: tool_name.clone(),
}) as Arc<dyn CoreToolRuntime>;
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let router = Arc::new(ToolRouter::from_parts(
ToolRegistry::from_tools([handler]),
Vec::new(),
));
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let runtime = ToolCallRuntime::new(router, session, turn_context, tracker);
let runtime = ToolCallRuntime::new(router, session, step_context, tracker);
let cancellation_token = CancellationToken::new();
let call = ToolCall {
tool_name,
@@ -495,12 +499,13 @@ mod tests {
cleanup_started: std::sync::Mutex::new(Some(cleanup_started_tx)),
allow_cleanup: Arc::clone(&allow_cleanup),
}) as Arc<dyn CoreToolRuntime>;
let step_context = StepContext::for_test(Arc::clone(&turn_context));
let router = Arc::new(ToolRouter::from_parts(
ToolRegistry::from_tools([handler]),
Vec::new(),
));
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let runtime = ToolCallRuntime::new(router, session, turn_context, tracker);
let runtime = ToolCallRuntime::new(router, session, step_context, tracker);
let cancellation_token = CancellationToken::new();
let call = ToolCall {
tool_name,
@@ -1,4 +1,5 @@
use super::*;
use crate::session::step_context::StepContext;
use pretty_assertions::assert_eq;
struct TestHandler {
@@ -457,8 +458,10 @@ fn test_invocation(
call_id: &str,
tool_name: codex_tools::ToolName,
) -> ToolInvocation {
let step_context = StepContext::for_test(Arc::clone(&turn));
ToolInvocation {
session,
step_context,
turn,
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(
+12 -9
View File
@@ -1,6 +1,6 @@
use crate::function_tool::FunctionCallError;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::session::step_context::StepContext;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -58,12 +58,12 @@ pub(crate) struct ToolSuggestCandidates {
}
impl ToolRouter {
pub(crate) fn from_turn_context(
turn_context: &TurnContext,
pub(crate) fn from_context(
step_context: &StepContext,
params: ToolRouterParams<'_>,
tool_search_handler_cache: &ToolSearchHandlerCache,
) -> Self {
build_tool_router(turn_context, params, tool_search_handler_cache)
build_tool_router(step_context, params, tool_search_handler_cache)
}
pub(crate) fn from_parts(registry: ToolRegistry, model_visible_specs: Vec<ToolSpec>) -> Self {
@@ -164,7 +164,7 @@ impl ToolRouter {
pub async fn dispatch_tool_call_with_code_mode_result(
&self,
session: Arc<Session>,
turn: Arc<TurnContext>,
step_context: Arc<StepContext>,
cancellation_token: CancellationToken,
tracker: SharedTurnDiffTracker,
call: ToolCall,
@@ -172,7 +172,7 @@ impl ToolRouter {
) -> Result<AnyToolResult, FunctionCallError> {
self.dispatch_tool_call_with_code_mode_result_inner(
session,
turn,
step_context,
cancellation_token,
tracker,
call,
@@ -187,7 +187,7 @@ impl ToolRouter {
pub(crate) async fn dispatch_tool_call_with_terminal_outcome(
&self,
session: Arc<Session>,
turn: Arc<TurnContext>,
step_context: Arc<StepContext>,
cancellation_token: CancellationToken,
tracker: SharedTurnDiffTracker,
call: ToolCall,
@@ -196,7 +196,7 @@ impl ToolRouter {
) -> Result<AnyToolResult, FunctionCallError> {
self.dispatch_tool_call_with_code_mode_result_inner(
session,
turn,
step_context,
cancellation_token,
tracker,
call,
@@ -210,7 +210,7 @@ impl ToolRouter {
async fn dispatch_tool_call_with_code_mode_result_inner(
&self,
session: Arc<Session>,
turn: Arc<TurnContext>,
step_context: Arc<StepContext>,
cancellation_token: CancellationToken,
tracker: SharedTurnDiffTracker,
call: ToolCall,
@@ -223,9 +223,12 @@ impl ToolRouter {
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,
+22 -11
View File
@@ -1,6 +1,7 @@
use std::sync::Arc;
use crate::config::Config;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::tools::context::ToolPayload;
use crate::turn_diff_tracker::TurnDiffTracker;
@@ -105,14 +106,16 @@ fn extension_tool_test_registry() -> Arc<ExtensionRegistry<Config>> {
#[tokio::test]
async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow::Result<()> {
let (session, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let step_context = StepContext::for_test(Arc::clone(&turn));
let mcp_tools = session
.services
.mcp_connection_manager
.load_full()
.list_all_tools()
.await;
let router = ToolRouter::from_turn_context(
&turn,
let router = ToolRouter::from_context(
step_context.as_ref(),
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
@@ -179,8 +182,10 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
#[tokio::test]
async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
let (_, turn) = make_session_and_context().await;
let router = ToolRouter::from_turn_context(
&turn,
let turn = Arc::new(turn);
let step_context = StepContext::for_test(Arc::clone(&turn));
let router = ToolRouter::from_context(
step_context.as_ref(),
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
@@ -228,8 +233,10 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
#[tokio::test]
async fn tools_without_handlers_do_not_support_parallel() -> anyhow::Result<()> {
let (_, turn) = make_session_and_context().await;
let router = ToolRouter::from_turn_context(
&turn,
let turn = Arc::new(turn);
let step_context = StepContext::for_test(Arc::clone(&turn));
let router = ToolRouter::from_context(
step_context.as_ref(),
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
@@ -254,6 +261,8 @@ async fn tools_without_handlers_do_not_support_parallel() -> anyhow::Result<()>
#[tokio::test]
async fn specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> {
let (_, turn) = make_session_and_context().await;
let turn = Arc::new(turn);
let step_context = StepContext::for_test(Arc::clone(&turn));
let hidden_tool = "hidden_dynamic_tool";
let visible_tool = "visible_dynamic_tool";
let dynamic_tools = vec![DynamicToolSpec::Namespace(DynamicToolNamespaceSpec {
@@ -283,8 +292,8 @@ async fn specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> {
],
})];
let router = ToolRouter::from_turn_context(
&turn,
let router = ToolRouter::from_context(
step_context.as_ref(),
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
@@ -333,6 +342,8 @@ fn mcp_tool_info(
async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow::Result<()> {
let (mut session, turn) = make_session_and_context().await;
session.services.extensions = extension_tool_test_registry();
let turn = Arc::new(turn);
let step_context = StepContext::for_test(Arc::clone(&turn));
let history_item = ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -348,8 +359,8 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
let mut expected_history_item = history_item.clone();
expected_history_item.set_turn_id_if_missing(&turn.sub_id);
let router = ToolRouter::from_turn_context(
&turn,
let router = ToolRouter::from_context(
step_context.as_ref(),
ToolRouterParams {
tool_suggest_candidates: None,
deferred_mcp_tools: None,
@@ -384,7 +395,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
let result = router
.dispatch_tool_call_with_code_mode_result(
Arc::new(session),
Arc::new(turn),
step_context,
CancellationToken::new(),
Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call,
+30 -18
View File
@@ -1,5 +1,6 @@
use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::next_thread_spawn_depth;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use crate::tools::code_mode::execute_spec::create_code_mode_tool;
use crate::tools::context::ToolInvocation;
@@ -144,7 +145,7 @@ impl PlannedTools {
#[derive(Clone, Copy)]
struct CoreToolPlanContext<'a> {
turn_context: &'a TurnContext,
step_context: &'a StepContext,
mcp_tools: Option<&'a [ToolInfo]>,
deferred_mcp_tools: Option<&'a [ToolInfo]>,
tool_suggest_candidates: Option<&'a crate::tools::router::ToolSuggestCandidates>,
@@ -157,21 +158,22 @@ struct CoreToolPlanContext<'a> {
#[instrument(level = "trace", skip_all)]
pub(crate) fn build_tool_router(
turn_context: &TurnContext,
step_context: &StepContext,
params: ToolRouterParams<'_>,
tool_search_handler_cache: &ToolSearchHandlerCache,
) -> ToolRouter {
let (model_visible_specs, registry) =
build_tool_specs_and_registry(turn_context, params, tool_search_handler_cache);
build_tool_specs_and_registry(step_context, params, tool_search_handler_cache);
ToolRouter::from_parts(registry, model_visible_specs)
}
#[instrument(level = "trace", skip_all)]
fn build_tool_specs_and_registry(
turn_context: &TurnContext,
step_context: &StepContext,
params: ToolRouterParams<'_>,
tool_search_handler_cache: &ToolSearchHandlerCache,
) -> (Vec<ToolSpec>, ToolRegistry) {
let turn_context = step_context.turn.as_ref();
let ToolRouterParams {
mcp_tools,
deferred_mcp_tools,
@@ -182,7 +184,7 @@ fn build_tool_specs_and_registry(
let default_agent_type_description =
crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new());
let context = CoreToolPlanContext {
turn_context,
step_context,
mcp_tools: mcp_tools.as_deref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
tool_suggest_candidates: tool_suggest_candidates.as_ref(),
@@ -290,7 +292,7 @@ fn spec_for_model_request(
#[instrument(level = "trace", skip_all)]
fn hosted_model_tool_specs(context: &CoreToolPlanContext<'_>) -> Vec<ToolSpec> {
let turn_context = context.turn_context;
let turn_context = context.step_context.turn.as_ref();
// Responses Lite accepts schemas for client-executed tools, not hosted Responses tools.
if turn_context.model_info.use_responses_lite {
return Vec::new();
@@ -638,11 +640,15 @@ fn standalone_web_search_enabled(turn_context: &TurnContext) -> bool {
.enabled(Feature::StandaloneWebSearch))
}
fn tool_environment_mode(step_context: &StepContext) -> ToolEnvironmentMode {
ToolEnvironmentMode::from_count(step_context.environments.turn_environments.len())
}
#[instrument(level = "trace", skip_all)]
fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
let turn_context = context.step_context.turn.as_ref();
let features = turn_context.config.features.get();
let environment_mode = turn_context.tool_environment_mode();
let environment_mode = tool_environment_mode(context.step_context);
if !environment_mode.has_environment() {
return;
}
@@ -662,7 +668,10 @@ fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut Planne
allow_login_shell,
exec_permission_approvals_enabled,
include_environment_id,
include_shell_parameter: unified_exec_should_include_shell_parameter(turn_context),
include_shell_parameter: unified_exec_should_include_shell_parameter(
turn_context,
context.step_context,
),
}));
planned_tools.add(WriteStdinHandler);
@@ -679,11 +688,14 @@ fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut Planne
}
}
fn unified_exec_should_include_shell_parameter(turn_context: &TurnContext) -> bool {
fn unified_exec_should_include_shell_parameter(
turn_context: &TurnContext,
step_context: &StepContext,
) -> bool {
!matches!(
&turn_context.unified_exec_shell_mode,
UnifiedExecShellMode::ZshFork(_)
) || turn_context
) || step_context
.environments
.turn_environments
.iter()
@@ -701,9 +713,9 @@ fn add_mcp_resource_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut
#[instrument(level = "trace", skip_all)]
fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
let turn_context = context.step_context.turn.as_ref();
let features = turn_context.config.features.get();
let environment_mode = turn_context.tool_environment_mode();
let environment_mode = tool_environment_mode(context.step_context);
planned_tools.add(PlanHandler);
@@ -716,7 +728,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut
);
}
if features.enabled(Feature::RequestPermissionsTool) {
if environment_mode.has_environment() && features.enabled(Feature::RequestPermissionsTool) {
planned_tools.add(RequestPermissionsHandler);
}
@@ -779,7 +791,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut
#[instrument(level = "trace", skip_all)]
fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
let turn_context = context.step_context.turn.as_ref();
if collab_tools_enabled(turn_context) {
if multi_agent_v2_enabled(turn_context) {
let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only {
@@ -943,7 +955,7 @@ fn add_extension_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut Pl
// Extension ToolContributor implementations are resolved into executors
// before planning. Core only adapts those executors into its runtime set.
append_extension_tool_executors(
context.turn_context,
context.step_context.turn.as_ref(),
context.extension_tool_executors,
planned_tools,
);
@@ -954,7 +966,7 @@ fn append_tool_search_executor(
context: &CoreToolPlanContext<'_>,
planned_tools: &mut PlannedTools,
) {
let turn_context = context.turn_context;
let turn_context = context.step_context.turn.as_ref();
if !search_tool_enabled(turn_context) {
return;
}
@@ -977,7 +989,7 @@ fn prepend_code_mode_executors(
context: &CoreToolPlanContext<'_>,
planned_tools: &mut PlannedTools,
) {
let turn_context = context.turn_context;
let turn_context = context.step_context.turn.as_ref();
let code_mode_executors = build_code_mode_executors(turn_context, planned_tools.runtimes());
planned_tools.runtimes.splice(0..0, code_mode_executors);
}
+48 -7
View File
@@ -31,6 +31,7 @@ use codex_tools::ToolSpec;
use pretty_assertions::assert_eq;
use serde_json::json;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::session::turn_context::TurnContext;
use crate::tools::handlers::ToolSearchHandlerCache;
@@ -181,8 +182,10 @@ async fn probe_with(
) -> ToolPlanProbe {
let (_session, mut turn) = make_session_and_context().await;
configure_turn(&mut turn);
let router = ToolRouter::from_turn_context(
&turn,
let turn = Arc::new(turn);
let step_context = StepContext::for_test(Arc::clone(&turn));
let router = ToolRouter::from_context(
step_context.as_ref(),
ToolRouterParams {
tool_suggest_candidates: inputs.tool_suggest_candidates,
mcp_tools: inputs.mcp_tools,
@@ -628,6 +631,7 @@ async fn environment_count_controls_environment_backed_tools() {
let no_environment = probe(|turn| {
turn.environments.turn_environments.clear();
set_feature(turn, Feature::ShellTool, /*enabled*/ true);
set_feature(turn, Feature::RequestPermissionsTool, /*enabled*/ true);
turn.model_info.apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
})
.await;
@@ -636,22 +640,30 @@ async fn environment_count_controls_environment_backed_tools() {
"exec_command",
"apply_patch",
"view_image",
"request_permissions",
]);
no_environment.assert_registered_lacks(&[
"shell_command",
"exec_command",
"apply_patch",
"view_image",
"request_permissions",
]);
let multiple_environments = probe(|turn| {
duplicate_primary_environment(turn);
set_feature(turn, Feature::ShellTool, /*enabled*/ true);
set_feature(turn, Feature::UnifiedExec, /*enabled*/ true);
set_feature(turn, Feature::RequestPermissionsTool, /*enabled*/ true);
turn.model_info.apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
})
.await;
multiple_environments.assert_visible_contains(&["exec_command", "apply_patch", "view_image"]);
multiple_environments.assert_visible_contains(&[
"exec_command",
"apply_patch",
"view_image",
"request_permissions",
]);
assert!(has_parameter(
multiple_environments.visible_spec("exec_command"),
"environment_id"
@@ -665,6 +677,31 @@ async fn environment_count_controls_environment_backed_tools() {
));
}
#[tokio::test]
async fn environment_tools_follow_the_step_context() {
let (_session, mut turn) = make_session_and_context().await;
set_feature(&mut turn, Feature::UnifiedExec, /*enabled*/ true);
turn.model_info.apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
let environments = turn.environments.clone();
turn.environments.turn_environments.clear();
let step_context = Arc::new(StepContext::new(Arc::new(turn), environments));
let plan = ToolPlanProbe::from_router(ToolRouter::from_context(
step_context.as_ref(),
ToolRouterParams {
mcp_tools: None,
deferred_mcp_tools: None,
tool_suggest_candidates: None,
extension_tool_executors: Vec::new(),
dynamic_tools: &[],
},
&Default::default(),
));
plan.assert_visible_contains(&["exec_command", "apply_patch", "view_image"]);
}
#[tokio::test]
async fn host_context_gates_agent_job_tools() {
let normal_agent_job = probe(|turn| {
@@ -799,8 +836,10 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() {
let (_session, mut first_turn) = make_session_and_context().await;
first_turn.model_info.supports_search_tool = true;
let first_router = ToolRouter::from_turn_context(
&first_turn,
let first_turn = Arc::new(first_turn);
let first_step_context = StepContext::for_test(Arc::clone(&first_turn));
let first_router = ToolRouter::from_context(
first_step_context.as_ref(),
ToolRouterParams {
mcp_tools: None,
deferred_mcp_tools: Some(vec![mcp_tool("first", "mcp__first", "lookup")]),
@@ -814,8 +853,10 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() {
let (_session, mut second_turn) = make_session_and_context().await;
second_turn.model_info.supports_search_tool = true;
let second_router = ToolRouter::from_turn_context(
&second_turn,
let second_turn = Arc::new(second_turn);
let second_step_context = StepContext::for_test(Arc::clone(&second_turn));
let second_router = ToolRouter::from_context(
second_step_context.as_ref(),
ToolRouterParams {
mcp_tools: None,
deferred_mcp_tools: Some(vec![mcp_tool("second", "mcp__second", "lookup")]),
@@ -13,6 +13,7 @@ use tokio_util::sync::CancellationToken;
use crate::function_tool::FunctionCallError;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::session::turn_context::TurnContext;
use crate::tools::code_mode::CodeModeWaitHandler;
@@ -269,8 +270,10 @@ fn test_invocation_with_payload(
source: ToolCallSource,
payload: ToolPayload,
) -> ToolInvocation {
let step_context = StepContext::for_test(Arc::clone(&turn));
ToolInvocation {
session,
step_context,
turn,
cancellation_token: CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
+51 -5
View File
@@ -402,7 +402,7 @@ async fn serve_environment_info(listener: TcpListener) {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn deferred_executor_updates_model_context_after_startup() -> Result<()> {
async fn deferred_executor_updates_context_and_tools_after_startup() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let server = start_mock_server().await;
let user_input_call_id = "wait-for-startup";
@@ -435,11 +435,13 @@ async fn deferred_executor_updates_model_context_after_startup() -> Result<()> {
sse(vec![
ev_response_created("resp-2"),
ev_function_call(
"update-plan",
"update_plan",
"request-permissions",
"request_permissions",
&json!({
"explanation": "Continue after startup.",
"plan": [{"step": "Finish", "status": "completed"}]
"reason": "Verify that the ready environment is used.",
"permissions": {
"network": { "enabled": true }
}
})
.to_string(),
),
@@ -456,7 +458,17 @@ async fn deferred_executor_updates_model_context_after_startup() -> Result<()> {
let mut builder = test_codex()
.with_exec_server_url(format!("ws://{}", listener.local_addr()?))
.with_config(|config| {
config.use_experimental_unified_exec_tool = true;
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.approvals_reviewer = ApprovalsReviewer::User;
assert!(config.features.enable(Feature::DeferredExecutor).is_ok());
assert!(config.features.enable(Feature::UnifiedExec).is_ok());
assert!(
config
.features
.enable(Feature::RequestPermissionsTool)
.is_ok()
);
assert!(
config
.features
@@ -500,6 +512,30 @@ async fn deferred_executor_updates_model_context_after_startup() -> Result<()> {
},
})
.await?;
let event = wait_for_event(&test.codex, |event| {
matches!(
event,
EventMsg::RequestPermissions(_) | EventMsg::TurnComplete(_)
)
})
.await;
let EventMsg::RequestPermissions(permission_request) = event else {
panic!("ready environment should be available to request_permissions: {event:?}");
};
assert_eq!(
permission_request.environment_id.as_deref(),
Some(REMOTE_ENVIRONMENT_ID)
);
test.codex
.submit(Op::RequestPermissionsResponse {
id: permission_request.call_id,
response: RequestPermissionsResponse {
permissions: RequestPermissionProfile::default(),
scope: PermissionGrantScope::Turn,
strict_auto_review: false,
},
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
@@ -507,6 +543,16 @@ async fn deferred_executor_updates_model_context_after_startup() -> Result<()> {
let requests = response_mock.requests();
assert_eq!(requests.len(), 3);
let tool_names = |request_index: usize| {
requests[request_index].body_json()["tools"]
.as_array()
.expect("tools array")
.iter()
.filter_map(|tool| tool.get("name").and_then(Value::as_str).map(str::to_owned))
.collect::<Vec<_>>()
};
assert!(!tool_names(0).contains(&"exec_command".to_string()));
assert!(tool_names(1).contains(&"exec_command".to_string()));
assert!(
requests[0]
.message_input_texts("user")