mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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?;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(), ¶ms.command, use_login_shell);
|
||||
#[allow(deprecated)]
|
||||
let cwd = turn_context.resolve_path(params.workdir.clone());
|
||||
let command = Self::base_command(shell, ¶ms.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(),
|
||||
¶ms.command,
|
||||
&workdir,
|
||||
&cwd,
|
||||
)
|
||||
.await;
|
||||
let prefix_rule = params.prefix_rule.clone();
|
||||
@@ -191,10 +202,16 @@ impl ShellCommandHandler {
|
||||
¶ms,
|
||||
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(),
|
||||
|
||||
@@ -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
|
||||
¶ms,
|
||||
&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() {
|
||||
¶ms,
|
||||
&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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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())),
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user