Unify turn input submission and routing (#38275)

## What changed

- Add `TurnInputRequest` and typed submission results for atomically starting a turn, steering the active turn, or declining input with a specific reason.
- Expose `start_or_steer_turn`, `start_turn_if_idle`, and `steer_turn` on `CodexThread`, and migrate Core consumers to these APIs.
- Make app-server `turn/start` steer an active regular turn and return that turn's ID. Reject incompatible output schemas and non-steerable turns without applying settings or enqueueing input.

## Testing

- Cover concurrent start-or-steer submissions, accepted and rejected settings updates, output-schema compatibility, idle-start rejection, and app-server steering.

GitOrigin-RevId: dd9b5528d76ec650c019e97af420bc13190ea86a
This commit is contained in:
Owen Lin authored and copyberry committed 2026-08-13 00:00:16 +00:00
1 parent 4b07886d59
commit cbb7e82a8b
137 files changed
+5217 -6812

No files matched your search

-1
View File
@@ -3603,7 +3603,6 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid",
"wiremock", "wiremock",
] ]
@@ -688,7 +688,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
&& span_attr(span, "rpc.method") == Some("turn/start") && span_attr(span, "rpc.method") == Some("turn/start")
&& span.span_context.trace_id() == remote_trace_id && span.span_context.trace_id() == remote_trace_id
}) && spans.iter().any(|span| { }) && spans.iter().any(|span| {
span_attr(span, "codex.op") == Some("user_input") span_attr(span, "codex.op") == Some("turn_input")
&& span.span_context.trace_id() == remote_trace_id && span.span_context.trace_id() == remote_trace_id
}) })
}) })
@@ -697,8 +697,8 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
let server_request_span = let server_request_span =
find_rpc_span_with_trace(&spans, SpanKind::Server, "turn/start", remote_trace_id); find_rpc_span_with_trace(&spans, SpanKind::Server, "turn/start", remote_trace_id);
let core_turn_span = let core_turn_span =
find_span_with_trace(&spans, remote_trace_id, "codex.op=user_input", |span| { find_span_with_trace(&spans, remote_trace_id, "codex.op=turn_input", |span| {
span_attr(span, "codex.op") == Some("user_input") span_attr(span, "codex.op") == Some("turn_input")
}); });
assert_eq!(server_request_span.parent_span_id, remote_parent_span_id); assert_eq!(server_request_span.parent_span_id, remote_parent_span_id);
@@ -333,12 +333,17 @@ use codex_core::CodexThreadSettingsOverrides;
use codex_core::ForkSnapshot; use codex_core::ForkSnapshot;
use codex_core::McpManager; use codex_core::McpManager;
use codex_core::NewThread; use codex_core::NewThread;
use codex_core::NotSubmittedReason;
#[cfg(test)] #[cfg(test)]
use codex_core::SessionMeta; use codex_core::SessionMeta;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::SteerInputError; use codex_core::SteerSubmission;
use codex_core::ThreadConfigSnapshot; use codex_core::ThreadConfigSnapshot;
use codex_core::ThreadManager; use codex_core::ThreadManager;
use codex_core::TurnInput;
use codex_core::TurnInputRequest;
use codex_core::TurnInputSubmission;
use codex_core::TurnStartOptions;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core::config::ConfigOverrides; use codex_core::config::ConfigOverrides;
use codex_core::config::NetworkProxyAuditMetadata; use codex_core::config::NetworkProxyAuditMetadata;
@@ -554,28 +554,38 @@ impl TurnRequestProcessor {
.map(PermissionProfile::from_legacy_sandbox_policy) .map(PermissionProfile::from_legacy_sandbox_policy)
}); });
// Start the turn by submitting the user input. Return its submission id as turn_id. let submission = thread
let turn_op = Op::UserInput { .start_or_steer_turn(
items: mapped_items, TurnInputRequest::new(TurnInput::UserInput {
final_output_json_schema: params.output_schema, content: mapped_items,
responsesapi_client_metadata: params.responsesapi_client_metadata, client_id: client_user_message_id,
additional_context, })
thread_settings, .with_thread_settings(thread_settings)
}; .on_start(TurnStartOptions {
let turn_id = thread final_output_json_schema: params.output_schema,
.submit_user_input_with_client_user_message_id( ..Default::default()
turn_op, })
self.request_trace_context(&request_id).await, .with_additional_context(additional_context)
client_user_message_id, .with_responses_metadata(params.responsesapi_client_metadata)
.with_trace(self.request_trace_context(&request_id).await),
) )
.await .await
.map_err(|err| { .map_err(|err| {
let error = internal_error(format!("failed to start turn: {err}")); let error = internal_error(format!("failed to submit turn input: {err}"));
self.track_error_response(&request_id, &error, /*error_type*/ None); self.track_error_response(&request_id, &error, /*error_type*/ None);
error error
})?; })?;
let (turn_id, started) = match submission {
TurnInputSubmission::Started { turn_id } => (turn_id, true),
TurnInputSubmission::Steered { turn_id } => (turn_id, false),
TurnInputSubmission::NotSubmitted { reason } => {
let error = internal_error(format!("failed to submit turn input: {reason:?}"));
self.track_error_response(&request_id, &error, /*error_type*/ None);
return Err(error);
}
};
if turn_has_input { if turn_has_input && started {
let config_snapshot = thread.config_snapshot().await; let config_snapshot = thread.config_snapshot().await;
let parent_permission_profile = let parent_permission_profile =
parent_permission_profile_override.unwrap_or(config_snapshot.permission_profile); parent_permission_profile_override.unwrap_or(config_snapshot.permission_profile);
@@ -943,32 +953,41 @@ impl TurnRequestProcessor {
.collect(); .collect();
let additional_context = map_additional_context(params.additional_context); let additional_context = map_additional_context(params.additional_context);
let turn_id = thread let submission = thread
.steer_input( .steer_turn(
mapped_items, TurnInputRequest::new(TurnInput::UserInput {
additional_context, content: mapped_items,
Some(&params.expected_turn_id), client_id: params.client_user_message_id,
params.client_user_message_id, })
params.responsesapi_client_metadata, .with_additional_context(additional_context)
.with_responses_metadata(params.responsesapi_client_metadata),
params.expected_turn_id,
) )
.await .await
.map_err(|err| { .map_err(|err| {
let (message, data, error_type) = match err { let error = internal_error(format!("failed to steer turn: {err}"));
SteerInputError::NoActiveTurn(_) => ( self.track_error_response(request_id, &error, /*error_type*/ None);
error
})?;
let turn_id = match submission {
SteerSubmission::Steered { turn_id } => turn_id,
SteerSubmission::NotSubmitted { reason } => {
let (message, data, error_type) = match reason {
NotSubmittedReason::NoActiveTurn | NotSubmittedReason::NotIdle => (
"no active turn to steer".to_string(), "no active turn to steer".to_string(),
None, None,
Some(AnalyticsJsonRpcError::TurnSteer( Some(AnalyticsJsonRpcError::TurnSteer(
TurnSteerRequestError::NoActiveTurn, TurnSteerRequestError::NoActiveTurn,
)), )),
), ),
SteerInputError::ExpectedTurnMismatch { expected, actual } => ( NotSubmittedReason::ExpectedTurnMismatch { expected, actual } => (
format!("expected active turn id `{expected}` but found `{actual}`"), format!("expected active turn id `{expected}` but found `{actual}`"),
None, None,
Some(AnalyticsJsonRpcError::TurnSteer( Some(AnalyticsJsonRpcError::TurnSteer(
TurnSteerRequestError::ExpectedTurnMismatch, TurnSteerRequestError::ExpectedTurnMismatch,
)), )),
), ),
SteerInputError::ActiveTurnNotSteerable { turn_kind } => { NotSubmittedReason::ActiveTurnNotSteerable { turn_kind } => {
let (message, turn_steer_error) = match turn_kind { let (message, turn_steer_error) = match turn_kind {
codex_protocol::protocol::NonSteerableTurnKind::Review => ( codex_protocol::protocol::NonSteerableTurnKind::Review => (
"cannot steer a review turn".to_string(), "cannot steer a review turn".to_string(),
@@ -1002,17 +1021,30 @@ impl TurnRequestProcessor {
Some(AnalyticsJsonRpcError::TurnSteer(turn_steer_error)), Some(AnalyticsJsonRpcError::TurnSteer(turn_steer_error)),
) )
} }
SteerInputError::EmptyInput => ( NotSubmittedReason::EmptyInput => (
"input must not be empty".to_string(), "input must not be empty".to_string(),
None, None,
Some(AnalyticsJsonRpcError::Input(InputError::Empty)), Some(AnalyticsJsonRpcError::Input(InputError::Empty)),
), ),
NotSubmittedReason::ActiveTurnOutputSchemaMismatch => (
"active turn uses a different output schema".to_string(),
None,
None,
),
NotSubmittedReason::PendingTriggerTurn | NotSubmittedReason::PlanMode => (
"no active turn to steer".to_string(),
None,
Some(AnalyticsJsonRpcError::TurnSteer(
TurnSteerRequestError::NoActiveTurn,
)),
),
}; };
let mut error = invalid_request(message); let mut error = invalid_request(message);
error.data = data; error.data = data;
self.track_error_response(request_id, &error, error_type); self.track_error_response(request_id, &error, error_type);
error return Err(error);
})?; }
};
Ok(TurnSteerResponse { turn_id }) Ok(TurnSteerResponse { turn_id })
} }
@@ -81,12 +81,15 @@ use core_test_support::responses;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::skip_if_remote; use core_test_support::skip_if_remote;
use core_test_support::skip_if_wine_exec; use core_test_support::skip_if_wine_exec;
use core_test_support::streaming_sse::StreamingSseChunk;
use core_test_support::streaming_sse::start_streaming_sse_server;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::Value; use serde_json::Value;
use serde_json::json; use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use tempfile::TempDir; use tempfile::TempDir;
use tokio::sync::oneshot;
use tokio::time::timeout; use tokio::time::timeout;
use wiremock::ResponseTemplate; use wiremock::ResponseTemplate;
@@ -288,6 +291,89 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn turn_start_steers_active_turn_and_returns_active_turn_id() -> Result<()> {
let (release_response, response_gate) = oneshot::channel();
let (server, _completions) = start_streaming_sse_server(vec![
vec![
StreamingSseChunk {
gate: None,
body: responses::sse(vec![responses::ev_response_created("resp-1")]),
},
StreamingSseChunk {
gate: Some(response_gate),
body: responses::sse(vec![responses::ev_completed("resp-1")]),
},
],
vec![StreamingSseChunk {
gate: None,
body: responses::sse(vec![
responses::ev_response_created("resp-2"),
responses::ev_completed("resp-2"),
]),
}],
])
.await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(server.uri()).write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
let ThreadStartResponse { thread, .. } = mcp
.start_thread(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let TurnStartResponse { turn: active_turn } = mcp
.request(|request_id| ClientRequest::TurnStart {
request_id,
params: TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "start".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
},
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/started"),
)
.await??;
let TurnStartResponse { turn: steered_turn } = mcp
.request(|request_id| ClientRequest::TurnStart {
request_id,
params: TurnStartParams {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![V2UserInput::Text {
text: "steer".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
},
})
.await?;
assert_eq!(steered_turn.id, active_turn.id);
release_response
.send(())
.expect("active response gate should remain open");
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
Ok(())
}
#[tokio::test] #[tokio::test]
async fn turn_start_additional_context_flows_to_model_input() -> Result<()> { async fn turn_start_additional_context_flows_to_model_input() -> Result<()> {
let responses = vec![create_final_assistant_message_sse_response("Done")?]; let responses = vec![create_final_assistant_message_sse_response("Done")?];
+7 -1
View File
@@ -32,11 +32,17 @@ pub use codex_core::ForkSnapshot;
pub use codex_core::LoadedAgentsMd; pub use codex_core::LoadedAgentsMd;
pub use codex_core::McpManager; pub use codex_core::McpManager;
pub use codex_core::NewThread; pub use codex_core::NewThread;
pub use codex_core::NotSubmittedReason;
pub use codex_core::StartIfIdleSubmission;
pub use codex_core::StartThreadOptions; pub use codex_core::StartThreadOptions;
pub use codex_core::StateDbHandle; pub use codex_core::StateDbHandle;
pub use codex_core::SteerSubmission;
pub use codex_core::ThreadManager; pub use codex_core::ThreadManager;
pub use codex_core::ThreadShutdownReport; pub use codex_core::ThreadShutdownReport;
pub use codex_core::UserMessageAdmission; pub use codex_core::TurnInput;
pub use codex_core::TurnInputRequest;
pub use codex_core::TurnInputSubmission;
pub use codex_core::TurnStartOptions;
pub use codex_core::WaitForEnvironmentToolConfig; pub use codex_core::WaitForEnvironmentToolConfig;
pub use codex_core::build_models_manager; pub use codex_core::build_models_manager;
pub use codex_core::config::Config; pub use codex_core::config::Config;
+34 -23
View File
@@ -1,3 +1,6 @@
use crate::TurnInputRequest;
use crate::TurnInputSubmission;
use crate::TurnStartOptions;
use crate::agent::AgentStatus; use crate::agent::AgentStatus;
use crate::agent::registry::AgentMetadata; use crate::agent::registry::AgentMetadata;
use crate::agent::registry::AgentRegistry; use crate::agent::registry::AgentRegistry;
@@ -51,6 +54,7 @@ use std::sync::Arc;
use std::sync::Weak; use std::sync::Weak;
use tokio::sync::watch; use tokio::sync::watch;
use tracing::warn; use tracing::warn;
use uuid::Uuid;
pub(crate) use self::execution::AgentExecutionGuard; pub(crate) use self::execution::AgentExecutionGuard;
use self::execution::AgentExecutionLimiter; use self::execution::AgentExecutionLimiter;
@@ -173,28 +177,32 @@ impl AgentControl {
root_turn_id: Option<String>, root_turn_id: Option<String>,
) -> CodexResult<String> { ) -> CodexResult<String> {
let state = self.upgrade()?; let state = self.upgrade()?;
self.ensure_execution_capacity_for_turn_start(agent_id, /*starts_turn*/ true) let thread = state.get_thread(agent_id).await?;
.await?; let result = match thread
self.send_input_after_capacity_check(agent_id, &state, input, parent_turn_id, root_turn_id) .start_or_steer_turn(
TurnInputRequest::user_input(input).on_start(TurnStartOptions {
parent_turn_id,
root_turn_id,
..Default::default()
}),
)
.await
{
Ok(TurnInputSubmission::Started { turn_id }) => Ok(turn_id),
Ok(TurnInputSubmission::Steered { .. }) => {
// MAv1 exposes an opaque `submission_id` to the model. The legacy
// `Op::UserInput` path returned a fresh ID for every steer, while the
// turn-input API returns the active turn ID. Keep the tool-visible ID
// unique without adding a submission receipt back to Core.
Ok(Uuid::now_v7().to_string())
}
Ok(TurnInputSubmission::NotSubmitted { reason }) => Err(CodexErr::InvalidRequest(
format!("turn input was not submitted: {reason:?}"),
)),
Err(err) => Err(err),
};
self.handle_thread_request_result(agent_id, &state, result)
.await .await
}
async fn send_input_after_capacity_check(
&self,
agent_id: ThreadId,
state: &Arc<ThreadManagerState>,
input: Vec<UserInput>,
parent_turn_id: Option<String>,
root_turn_id: Option<String>,
) -> CodexResult<String> {
self.handle_thread_request_result(
agent_id,
state,
state
.send_op(agent_id, input.into(), parent_turn_id, root_turn_id)
.await,
)
.await
} }
pub(crate) async fn send_inter_agent_communication( pub(crate) async fn send_inter_agent_communication(
@@ -206,8 +214,11 @@ impl AgentControl {
root_turn_id: Option<String>, root_turn_id: Option<String>,
) -> CodexResult<String> { ) -> CodexResult<String> {
let state = self.upgrade()?; let state = self.upgrade()?;
self.ensure_execution_capacity_for_turn_start(agent_id, communication.trigger_turn) if communication.trigger_turn {
.await?; let thread = state.get_thread(agent_id).await?;
self.ensure_execution_capacity_for_turn_start(&thread)
.await?;
}
self.send_inter_agent_communication_after_capacity_check( self.send_inter_agent_communication_after_capacity_check(
agent_id, agent_id,
&state, &state,
+3 -24
View File
@@ -1,10 +1,9 @@
use super::AgentControl; use super::AgentControl;
use codex_protocol::ThreadId; use crate::codex_thread::CodexThread;
use codex_protocol::error::CodexErr; use codex_protocol::error::CodexErr;
use codex_protocol::error::CodexErrorDetails; use codex_protocol::error::CodexErrorDetails;
use codex_protocol::error::Result as CodexResult; use codex_protocol::error::Result as CodexResult;
use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SessionSource;
use std::sync::Arc; use std::sync::Arc;
use std::sync::OnceLock; use std::sync::OnceLock;
@@ -28,25 +27,10 @@ impl Drop for AgentExecutionGuard {
} }
impl AgentControl { impl AgentControl {
pub(crate) async fn ensure_execution_capacity_for_op( pub(crate) async fn ensure_execution_capacity_for_turn_start(
&self, &self,
thread_id: ThreadId, thread: &CodexThread,
op: &Op,
) -> CodexResult<()> { ) -> CodexResult<()> {
self.ensure_execution_capacity_for_turn_start(thread_id, op_starts_turn(op))
.await
}
pub(super) async fn ensure_execution_capacity_for_turn_start(
&self,
thread_id: ThreadId,
starts_turn: bool,
) -> CodexResult<()> {
if !starts_turn {
return Ok(());
}
let state = self.upgrade()?;
let thread = state.get_thread(thread_id).await?;
if thread.session.active_turn.lock().await.is_some() { if thread.session.active_turn.lock().await.is_some() {
return Ok(()); return Ok(());
} }
@@ -104,11 +88,6 @@ impl AgentExecutionLimiter {
} }
} }
fn op_starts_turn(op: &Op) -> bool {
matches!(op, Op::UserInput { .. })
|| matches!(op, Op::InterAgentCommunication { communication } if communication.trigger_turn)
}
fn is_execution_limited( fn is_execution_limited(
multi_agent_version: MultiAgentVersion, multi_agent_version: MultiAgentVersion,
session_source: &SessionSource, session_source: &SessionSource,
+1 -2
View File
@@ -544,9 +544,8 @@ impl AgentControl {
match initial_input { match initial_input {
SpawnInitialInput::UserInput(input) => { SpawnInitialInput::UserInput(input) => {
self.send_input_after_capacity_check( self.send_input(
new_thread.thread_id, new_thread.thread_id,
&state,
input, input,
options.parent_turn_id, options.parent_turn_id,
options.root_turn_id, options.root_turn_id,
+35 -81
View File
@@ -98,28 +98,6 @@ fn captured_op_matches(actual: &(ThreadId, Op), expected: &(ThreadId, Op)) -> bo
return false; return false;
} }
match (&actual.1, &expected.1) { match (&actual.1, &expected.1) {
(
Op::UserInput {
items: actual_items,
final_output_json_schema: actual_schema,
responsesapi_client_metadata: actual_metadata,
additional_context: actual_context,
thread_settings: actual_settings,
},
Op::UserInput {
items: expected_items,
final_output_json_schema: expected_schema,
responsesapi_client_metadata: expected_metadata,
additional_context: expected_context,
thread_settings: expected_settings,
},
) => {
actual_items == expected_items
&& actual_schema == expected_schema
&& actual_metadata == expected_metadata
&& actual_context == expected_context
&& actual_settings == expected_settings
}
( (
Op::InterAgentCommunication { Op::InterAgentCommunication {
communication: actual, communication: actual,
@@ -314,6 +292,29 @@ fn history_contains_text<'a>(
}) })
} }
async fn wait_for_recorded_user_message(thread: &CodexThread, needle: &str) {
timeout(Duration::from_secs(5), async {
loop {
let event = thread
.next_event()
.await
.expect("event stream should stay open");
if let EventMsg::ItemCompleted(ItemCompletedEvent {
item: TurnItem::UserMessage(item),
..
}) = event.msg
&& item.content.iter().any(
|input| matches!(input, UserInput::Text { text, .. } if text.contains(needle)),
)
{
return;
}
}
})
.await
.expect("timed out waiting for user message recording");
}
fn history_contains_assistant_inter_agent_communication<'a>( fn history_contains_assistant_inter_agent_communication<'a>(
history_items: impl IntoIterator<Item = &'a ResponseItem>, history_items: impl IntoIterator<Item = &'a ResponseItem>,
expected: &InterAgentCommunication, expected: &InterAgentCommunication,
@@ -355,6 +356,13 @@ async fn wait_for_subagent_notification(parent_thread: &Arc<CodexThread>) -> boo
} }
async fn persist_thread_for_tree_resume(thread: &Arc<CodexThread>, message: &str) { async fn persist_thread_for_tree_resume(thread: &Arc<CodexThread>, message: &str) {
// These tests only need a durable resume fixture. Stop the child prompt
// first so this marker records directly instead of waiting behind an
// unrelated active turn.
thread
.session
.abort_all_tasks(TurnAbortReason::Interrupted)
.await;
thread thread
.inject_user_message_without_turn(message.to_string()) .inject_user_message_without_turn(message.to_string())
.await; .await;
@@ -597,7 +605,7 @@ async fn subscribe_status_updates_on_shutdown() {
#[tokio::test] #[tokio::test]
async fn send_input_submits_user_message() { async fn send_input_submits_user_message() {
let harness = AgentControlHarness::new().await; let harness = AgentControlHarness::new().await;
let (thread_id, _thread) = harness.start_thread().await; let (thread_id, thread) = harness.start_thread().await;
let submission_id = harness let submission_id = harness
.control .control
@@ -613,25 +621,7 @@ async fn send_input_submits_user_message() {
.await .await
.expect("send_input should succeed"); .expect("send_input should succeed");
assert!(!submission_id.is_empty()); assert!(!submission_id.is_empty());
let expected = ( wait_for_recorded_user_message(thread.as_ref(), "hello from tests").await;
thread_id,
Op::UserInput {
items: vec![UserInput::Text {
text: "hello from tests".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
},
);
let captured = harness
.manager
.captured_ops()
.into_iter()
.find(|entry| captured_op_matches(entry, &expected));
assert!(captured.is_some());
} }
#[tokio::test] #[tokio::test]
@@ -968,30 +958,12 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
) )
.await .await
.expect("spawn_agent should succeed"); .expect("spawn_agent should succeed");
let _thread = harness let thread = harness
.manager .manager
.get_thread(thread_id) .get_thread(thread_id)
.await .await
.expect("thread should be registered"); .expect("thread should be registered");
let expected = ( wait_for_recorded_user_message(thread.as_ref(), "spawned").await;
thread_id,
Op::UserInput {
items: vec![UserInput::Text {
text: "spawned".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
},
);
let captured = harness
.manager
.captured_ops()
.into_iter()
.find(|entry| captured_op_matches(entry, &expected));
assert!(captured.is_some());
} }
#[tokio::test] #[tokio::test]
@@ -1587,25 +1559,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
"empty child developer instructions should preserve unrelated developer fragments" "empty child developer instructions should preserve unrelated developer fragments"
); );
let expected = ( wait_for_recorded_user_message(child_thread.as_ref(), "child task").await;
child_thread_id,
Op::UserInput {
items: vec![UserInput::Text {
text: "child task".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
},
);
let captured = harness
.manager
.captured_ops()
.into_iter()
.find(|entry| captured_op_matches(entry, &expected));
assert!(captured.is_some());
let _ = harness let _ = harness
.control .control
+22 -14
View File
@@ -34,6 +34,10 @@ use codex_login::AuthManager;
use codex_models_manager::manager::SharedModelsManager; use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::error::CodexErr; use codex_protocol::error::CodexErr;
use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::turn_input::TurnInputMode;
use codex_protocol::turn_input::TurnInputRequest;
use codex_protocol::turn_input::TurnInputSubmission;
use codex_protocol::turn_input::TurnStartOptions;
#[cfg(test)] #[cfg(test)]
use crate::session::completed_session_loop_termination; use crate::session::completed_session_loop_termination;
@@ -190,19 +194,24 @@ pub(crate) async fn run_codex_thread_one_shot(
.await?; .await?;
// Send the initial input to kick off the one-shot turn. // Send the initial input to kick off the one-shot turn.
io.submit_with_trace( let submission = io
Op::UserInput { .submit_turn_input(
items: input, TurnInputRequest::user_input(input).on_start(TurnStartOptions {
final_output_json_schema, final_output_json_schema,
responsesapi_client_metadata: None, parent_turn_id: Some(parent_turn_id),
additional_context: Default::default(), root_turn_id,
thread_settings: Default::default(), }),
}, TurnInputMode::StartIfIdle,
/*trace*/ None, )
Some(parent_turn_id), .await?;
root_turn_id, match submission {
) TurnInputSubmission::Started { .. } => {}
.await?; submission => {
return Err(CodexErr::InvalidRequest(format!(
"delegate turn input was not started: {submission:?}"
)));
}
}
// Bridge events so we can observe completion and shut down automatically. // Bridge events so we can observe completion and shut down automatically.
let (tx_bridge, rx_bridge) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_bridge, rx_bridge) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
@@ -222,7 +231,6 @@ pub(crate) async fn run_codex_thread_one_shot(
.send(Submission { .send(Submission {
id: "shutdown".to_string(), id: "shutdown".to_string(),
op: Op::Shutdown {}, op: Op::Shutdown {},
client_user_message_id: None,
trace: None, trace: None,
parent_turn_id: None, parent_turn_id: None,
root_turn_id: None, root_turn_id: None,
@@ -128,7 +128,6 @@ async fn forward_ops_preserves_submission_trace_context() {
let submission = Submission { let submission = Submission {
id: "sub-1".to_string(), id: "sub-1".to_string(),
op: Op::Interrupt, op: Op::Interrupt,
client_user_message_id: None,
trace: Some(codex_protocol::protocol::W3cTraceContext { trace: Some(codex_protocol::protocol::W3cTraceContext {
traceparent: Some( traceparent: Some(
"00-1234567890abcdef1234567890abcdef-1234567890abcdef-01".to_string(), "00-1234567890abcdef1234567890abcdef-1234567890abcdef-01".to_string(),
+69 -144
View File
@@ -4,10 +4,7 @@ use crate::elicitation::ElicitationRegistration;
use crate::environment_config::EnvironmentConfig; use crate::environment_config::EnvironmentConfig;
use crate::session::SessionIo; use crate::session::SessionIo;
use crate::session::SessionSettingsUpdate; use crate::session::SessionSettingsUpdate;
use crate::session::SteerInputError;
use crate::session::TurnInput;
use crate::session::session::Session; use crate::session::session::Session;
use crate::user_message_admission::UserMessageAdmission;
use codex_diagnostics::Gauge; use codex_diagnostics::Gauge;
use codex_diagnostics::GaugeGuard; use codex_diagnostics::GaugeGuard;
use codex_exec_server::SelectedCapabilityRootsStatus; use codex_exec_server::SelectedCapabilityRootsStatus;
@@ -29,7 +26,6 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AdditionalContextEntry;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::Event; use codex_protocol::protocol::Event;
use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::MultiAgentVersion;
@@ -37,7 +33,6 @@ use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent; use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::Submission;
use codex_protocol::protocol::ThreadHistoryMode; use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::ThreadMemoryMode; use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::ThreadSettingsSnapshot; use codex_protocol::protocol::ThreadSettingsSnapshot;
@@ -46,7 +41,11 @@ use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::protocol::W3cTraceContext; use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::user_input::UserInput; use codex_protocol::turn_input::StartIfIdleSubmission;
use codex_protocol::turn_input::SteerSubmission;
use codex_protocol::turn_input::TurnInputMode;
use codex_protocol::turn_input::TurnInputRequest;
use codex_protocol::turn_input::TurnInputSubmission;
use codex_thread_store::PersistContext; use codex_thread_store::PersistContext;
use codex_thread_store::StoredThread; use codex_thread_store::StoredThread;
use codex_thread_store::StoredThreadHistory; use codex_thread_store::StoredThreadHistory;
@@ -57,8 +56,6 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::LegacyAppPathString;
use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUri;
use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceRequestParams;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -94,46 +91,6 @@ pub struct ThreadConfigSnapshot {
pub originator: String, pub originator: String,
} }
/// Explains why `CodexThread::try_start_turn_if_idle` rejected an automatic
/// idle turn.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TryStartTurnIfIdleRejectionReason {
/// User/client-triggered mailbox work is already queued and must take
/// priority over extension-initiated idle work.
PendingTriggerTurn,
/// The thread is in Plan mode, where idle work without user input must not
/// start a new model turn.
PlanMode,
/// Another turn or task is active, or the idle reservation was lost before
/// the automatic turn could start.
Busy,
}
/// Rejection returned when an extension asks to start automatic idle work but
/// the thread is not eligible to run it.
#[derive(Debug)]
pub struct TryStartTurnIfIdleError {
reason: TryStartTurnIfIdleRejectionReason,
input: Vec<TurnInput>,
}
impl TryStartTurnIfIdleError {
pub(crate) fn new(reason: TryStartTurnIfIdleRejectionReason, input: Vec<TurnInput>) -> Self {
Self { reason, input }
}
/// Returns the stable reason the automatic idle turn was rejected.
pub fn reason(&self) -> TryStartTurnIfIdleRejectionReason {
self.reason
}
/// Consumes the rejection and returns the original turn input
/// unchanged, so callers can retry, drop, or log it explicitly.
pub fn into_input(self) -> Vec<TurnInput> {
self.input
}
}
impl ThreadConfigSnapshot { impl ThreadConfigSnapshot {
pub fn cwd(&self) -> &AbsolutePathBuf { pub fn cwd(&self) -> &AbsolutePathBuf {
&self.environments.legacy_fallback_cwd &self.environments.legacy_fallback_cwd
@@ -300,88 +257,83 @@ impl CodexThread {
.await .await
} }
pub async fn submit_user_input_with_client_user_message_id( /// Submits turn input without requiring the caller to inspect thread state.
///
/// The result describes whether Core started a turn, steered an active
/// turn, or declined it without recording or enqueueing the input. Only
/// user input is accepted.
pub async fn start_or_steer_turn(
&self, &self,
op: Op, request: TurnInputRequest,
trace: Option<W3cTraceContext>, ) -> CodexResult<TurnInputSubmission> {
client_user_message_id: Option<String>, self.submit_turn_input_with_mode(request, TurnInputMode::StartOrSteer)
) -> CodexResult<String> {
self.session
.services
.agent_control
.ensure_execution_capacity_for_op(self.session.thread_id(), &op)
.await?;
self.io
.submit_user_input_with_client_user_message_id(op, trace, client_user_message_id)
.await .await
} }
/// Waits until Core has started a turn or steered the active turn. /// Starts a regular turn only when the thread is idle.
pub async fn submit_user_input_and_wait_for_admission( ///
/// Core declines the input without recording or enqueueing it when idle
/// work cannot start.
pub async fn start_turn_if_idle(
&self, &self,
op: Op, request: TurnInputRequest,
trace: Option<W3cTraceContext>, ) -> CodexResult<StartIfIdleSubmission> {
client_user_message_id: Option<String>, match self
) -> CodexResult<UserMessageAdmission> { .submit_turn_input_with_mode(request, TurnInputMode::StartIfIdle)
let Op::UserInput { .. } = &op else { .await?
return Err(CodexErr::InvalidRequest( {
"user message admission requires user input".to_string(), TurnInputSubmission::Started { turn_id } => {
)); Ok(StartIfIdleSubmission::Started { turn_id })
}; }
self.session TurnInputSubmission::NotSubmitted { reason } => {
.services Ok(StartIfIdleSubmission::NotSubmitted { reason })
.agent_control }
.ensure_execution_capacity_for_op(self.session.thread_id(), &op) TurnInputSubmission::Steered { .. } => {
.await?; unreachable!("start-if-idle submission cannot steer")
let submission_id = crate::session::new_submission_id(); }
let (_pending_admission, admission) = self
.session
.pending_user_message_admissions
.register(submission_id.clone());
self.io
.submit_with_id(Submission {
id: submission_id.clone(),
op,
client_user_message_id,
trace,
parent_turn_id: None,
root_turn_id: None,
})
.await?;
tokio::select! {
biased;
result = admission => result.unwrap_or(Err(CodexErr::InternalAgentDied)),
() = self.io.session_loop_termination.clone() => {
Err(CodexErr::InternalAgentDied)
},
} }
} }
/// Steers only if `expected_turn_id` is still the active regular turn.
pub async fn steer_turn(
&self,
request: TurnInputRequest,
expected_turn_id: String,
) -> CodexResult<SteerSubmission> {
match self
.submit_turn_input_with_mode(request, TurnInputMode::Steer { expected_turn_id })
.await?
{
TurnInputSubmission::Steered { turn_id } => Ok(SteerSubmission::Steered { turn_id }),
TurnInputSubmission::NotSubmitted { reason } => {
Ok(SteerSubmission::NotSubmitted { reason })
}
TurnInputSubmission::Started { .. } => {
unreachable!("steer-only submission cannot start a turn")
}
}
}
async fn submit_turn_input_with_mode(
&self,
request: TurnInputRequest,
mode: TurnInputMode,
) -> CodexResult<TurnInputSubmission> {
if !matches!(mode, TurnInputMode::Steer { .. }) {
self.session
.services
.agent_control
.ensure_execution_capacity_for_turn_start(self)
.await?;
}
self.io.submit_turn_input(request, mode).await
}
/// Persist whether this thread is eligible for future memory generation. /// Persist whether this thread is eligible for future memory generation.
pub async fn set_thread_memory_mode(&self, mode: ThreadMemoryMode) -> anyhow::Result<()> { pub async fn set_thread_memory_mode(&self, mode: ThreadMemoryMode) -> anyhow::Result<()> {
self.session.set_thread_memory_mode(mode).await self.session.set_thread_memory_mode(mode).await
} }
pub async fn steer_input(
&self,
input: Vec<UserInput>,
additional_context: BTreeMap<String, AdditionalContextEntry>,
expected_turn_id: Option<&str>,
client_user_message_id: Option<String>,
responsesapi_client_metadata: Option<HashMap<String, String>>,
) -> Result<String, SteerInputError> {
self.session
.steer_input(
input,
additional_context,
expected_turn_id,
client_user_message_id,
responsesapi_client_metadata,
/*incoming_turn_metadata*/ None,
)
.await
}
/// Injects model-visible items into the currently active turn. /// Injects model-visible items into the currently active turn.
/// ///
/// This is the thread-level bridge to `Session::inject_if_running` for /// This is the thread-level bridge to `Session::inject_if_running` for
@@ -394,26 +346,6 @@ impl CodexThread {
self.session.inject_if_running(items).await self.session.inject_if_running(items).await
} }
/// Starts an automatic regular turn with response items or user input only
/// when idle work is allowed for this thread.
///
/// This is the required entry point for extensions that want to launch
/// model-visible work from `ThreadLifecycleContributor::on_thread_idle`.
/// The call succeeds only if no user/client-triggered turn is queued and no
/// task is currently active. Work without user input is also rejected in
/// Plan mode. Active Review tasks are rejected by the active-task check
/// because Review turns are not steerable.
///
/// On rejection, the returned error includes a stable reason and carries
/// the original `items` unchanged so the caller can decide whether to drop
/// them, retry later, or log why no automatic turn was started.
pub async fn try_start_turn_if_idle(
&self,
items: Vec<TurnInput>,
) -> Result<(), TryStartTurnIfIdleError> {
self.session.try_start_turn_if_idle(items).await
}
pub async fn set_app_server_client_info( pub async fn set_app_server_client_info(
&self, &self,
app_server_client_name: Option<String>, app_server_client_name: Option<String>,
@@ -484,13 +416,6 @@ impl CodexThread {
} }
} }
/// Use sparingly: this is intended to be removed soon.
pub async fn submit_with_id(&self, mut sub: Submission) -> CodexResult<()> {
sub.parent_turn_id = None;
sub.root_turn_id = None;
self.io.submit_with_id(sub).await
}
pub async fn next_event(&self) -> CodexResult<Event> { pub async fn next_event(&self) -> CodexResult<Event> {
self.io.next_event().await self.io.next_event().await
} }
+48 -21
View File
@@ -51,6 +51,10 @@ use crate::session::turn_context::TurnContext;
use codex_config::types::McpServerConfig; use codex_config::types::McpServerConfig;
use codex_features::Feature; use codex_features::Feature;
use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::turn_input::TurnInputMode;
use codex_protocol::turn_input::TurnInputRequest;
use codex_protocol::turn_input::TurnInputSubmission;
use codex_protocol::turn_input::TurnStartOptions;
use codex_thread_store::PersistContext; use codex_thread_store::PersistContext;
use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUri;
@@ -888,13 +892,9 @@ async fn run_review_on_session(
.unwrap_or_else(|| params.parent_context.turn().config.cwd.clone()); .unwrap_or_else(|| params.parent_context.turn().config.cwd.clone());
let parent_turn = params.parent_context.turn(); let parent_turn = params.parent_context.turn();
let submission = review_session.io.submit_with_trace( let submission = review_session.io.submit_turn_input(
Op::UserInput { TurnInputRequest::user_input(prompt_items.items)
items: prompt_items.items, .with_thread_settings(codex_protocol::protocol::ThreadSettingsOverrides {
final_output_json_schema: Some(params.schema.clone()),
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new( environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new(
parent_turn_legacy_fallback_cwd, parent_turn_legacy_fallback_cwd,
parent_turn_environments, parent_turn_environments,
@@ -913,11 +913,13 @@ async fn run_review_on_session(
}, },
}), }),
..Default::default() ..Default::default()
}, })
}, .on_start(TurnStartOptions {
/*trace*/ None, final_output_json_schema: Some(params.schema.clone()),
Some(parent_turn.sub_id.clone()), parent_turn_id: Some(parent_turn.sub_id.clone()),
parent_turn.turn_metadata_state.root_turn_id(), root_turn_id: parent_turn.turn_metadata_state.root_turn_id(),
}),
TurnInputMode::StartIfIdle,
); );
let submit_result = run_before_review_deadline( let submit_result = run_before_review_deadline(
deadline, deadline,
@@ -926,7 +928,17 @@ async fn run_review_on_session(
) )
.await; .await;
let child_turn_id = match submit_result { let child_turn_id = match submit_result {
Ok(Ok(child_turn_id)) => child_turn_id, Ok(Ok(TurnInputSubmission::Started { turn_id })) => turn_id,
Ok(Ok(submission)) => {
return (
GuardianReviewSessionOutcome::SessionFailed {
error: anyhow!("guardian review input was not started: {submission:?}"),
error_info: None,
},
false,
analytics_result,
);
}
Ok(Err(err)) => { Ok(Err(err)) => {
return ( return (
GuardianReviewSessionOutcome::SessionFailed { GuardianReviewSessionOutcome::SessionFailed {
@@ -1879,16 +1891,21 @@ mod tests {
.await .await
}); });
let submission = rx_sub.recv().await.expect("guardian submission"); let submission = rx_sub.recv().await.expect("guardian submission");
let id = submission.id;
let Op::TurnInput { reply, .. } = submission.op else {
panic!("expected turn-input submission");
};
reply
.send(Ok(TurnInputSubmission::Started {
turn_id: id.clone(),
}))
.expect("reply to guardian submission");
tx_event tx_event
.send(turn_complete_event("prior-turn", Some("stale"), Some(9))) .send(turn_complete_event("prior-turn", Some("stale"), Some(9)))
.await .await
.expect("queue prior turn completion"); .expect("queue prior turn completion");
tx_event tx_event
.send(turn_complete_event( .send(turn_complete_event(id.as_str(), Some("fresh"), Some(42)))
submission.id.as_str(),
Some("fresh"),
Some(42),
))
.await .await
.expect("queue submitted turn completion"); .expect("queue submitted turn completion");
@@ -1904,7 +1921,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn run_review_removes_trunk_when_event_stream_is_broken() { async fn run_review_removes_trunk_when_event_stream_is_broken() {
let (mut review_session, tx_event, _rx_sub) = test_review_session().await; let (mut review_session, tx_event, rx_sub) = test_review_session().await;
let params = test_review_params().await; let params = test_review_params().await;
review_session.reuse_key = GuardianReviewSessionReuseKey::from_spawn_config( review_session.reuse_key = GuardianReviewSessionReuseKey::from_spawn_config(
&params.spawn_config, &params.spawn_config,
@@ -1915,16 +1932,26 @@ mod tests {
.await .await
.history_version(), .history_version(),
); );
let manager = GuardianReviewSessionManager { let manager = Arc::new(GuardianReviewSessionManager {
state: Arc::new(Mutex::new(GuardianReviewSessionState { state: Arc::new(Mutex::new(GuardianReviewSessionState {
trunk: Some(Arc::new(review_session)), trunk: Some(Arc::new(review_session)),
ephemeral_reviews: Vec::new(), ephemeral_reviews: Vec::new(),
})), })),
..Default::default() ..Default::default()
});
let manager_for_review = Arc::clone(&manager);
let review = tokio::spawn(async move { manager_for_review.run_review(params).await });
let submission = rx_sub.recv().await.expect("guardian submission");
let id = submission.id;
let Op::TurnInput { reply, .. } = submission.op else {
panic!("expected turn-input submission");
}; };
reply
.send(Ok(TurnInputSubmission::Started { turn_id: id }))
.expect("reply to guardian submission");
drop(tx_event); drop(tx_event);
let (outcome, _) = manager.run_review(params).await; let (outcome, _) = review.await.expect("review task should complete");
assert!(matches!( assert!(matches!(
outcome, outcome,
+7 -6
View File
@@ -15,9 +15,14 @@ mod realtime_prompt;
mod responses_metadata; mod responses_metadata;
mod responses_retry; mod responses_retry;
pub(crate) mod session; pub(crate) mod session;
pub use codex_protocol::turn_input::NotSubmittedReason;
pub use codex_protocol::turn_input::StartIfIdleSubmission;
pub use codex_protocol::turn_input::SteerSubmission;
pub use codex_protocol::turn_input::TurnInput;
pub use codex_protocol::turn_input::TurnInputRequest;
pub use codex_protocol::turn_input::TurnInputSubmission;
pub use codex_protocol::turn_input::TurnStartOptions;
pub use responses_metadata::CodexResponsesMetadata; pub use responses_metadata::CodexResponsesMetadata;
pub use session::SteerInputError;
pub use session::TurnInput;
pub use turn_metadata::detached_memory_responses_metadata; pub use turn_metadata::detached_memory_responses_metadata;
mod codex_thread; mod codex_thread;
mod compact_model_fallback; mod compact_model_fallback;
@@ -25,16 +30,12 @@ mod compact_remote;
mod compact_remote_history; mod compact_remote_history;
mod compact_remote_v2; mod compact_remote_v2;
mod compact_token_budget; mod compact_token_budget;
mod user_message_admission;
pub use codex_thread::BackgroundTerminalInfo; pub use codex_thread::BackgroundTerminalInfo;
pub use codex_thread::CodexThread; pub use codex_thread::CodexThread;
pub use codex_thread::CodexThreadSettingsOverrides; pub use codex_thread::CodexThreadSettingsOverrides;
pub use codex_thread::ThreadConfigSnapshot; pub use codex_thread::ThreadConfigSnapshot;
pub use codex_thread::TryStartTurnIfIdleError;
pub use codex_thread::TryStartTurnIfIdleRejectionReason;
pub use environment_config::EnvironmentConfig; pub use environment_config::EnvironmentConfig;
pub use session::turn_context::TurnContext; pub use session::turn_context::TurnContext;
pub use user_message_admission::UserMessageAdmission;
mod agent; mod agent;
mod agent_communication; mod agent_communication;
mod attestation; mod attestation;
+10 -244
View File
@@ -10,10 +10,9 @@ use tracing::Instrument;
use tracing::debug_span; use tracing::debug_span;
use tracing::info_span; use tracing::info_span;
use crate::session::SteerInputError;
use crate::session::TurnInput;
use crate::session::session::Session; use crate::session::session::Session;
use crate::session::session::SessionSettingsUpdate; use crate::session::thread_settings;
use crate::session::turn_input;
use crate::config::Config; use crate::config::Config;
use crate::review_prompts::resolve_review_request; use crate::review_prompts::resolve_review_request;
@@ -22,10 +21,7 @@ use crate::tasks::CompactTask;
use crate::tasks::UserShellCommandMode; use crate::tasks::UserShellCommandMode;
use crate::tasks::UserShellCommandTask; use crate::tasks::UserShellCommandTask;
use crate::tasks::execute_user_shell_command; use crate::tasks::execute_user_shell_command;
use crate::user_message_admission::UserMessageAdmission;
use codex_history::RolloutItem; use codex_history::RolloutItem;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
@@ -43,8 +39,6 @@ use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::ThreadMemoryMode; use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::protocol::ThreadRolledBackEvent;
use codex_protocol::protocol::ThreadSettingsAppliedEvent;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::WarningEvent; use codex_protocol::protocol::WarningEvent;
use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -82,231 +76,6 @@ pub async fn realtime_conversation_list_voices(sess: &Session, sub_id: String) {
.await; .await;
} }
pub async fn user_input_or_turn(
sess: &Arc<Session>,
sub_id: String,
op: Op,
client_user_message_id: Option<String>,
parent_turn_id: Option<String>,
root_turn_id: Option<String>,
) {
let admission = user_input_or_turn_inner(
sess,
sub_id.clone(),
op,
client_user_message_id,
parent_turn_id,
root_turn_id,
)
.await;
sess.pending_user_message_admissions
.complete(&sub_id, admission);
}
pub async fn update_thread_settings(
sess: &Arc<Session>,
sub_id: String,
thread_settings: ThreadSettingsOverrides,
) {
let updates = thread_settings_update(sess, thread_settings).await;
match sess.update_settings(updates).await {
Ok(()) => {
sess.send_event_raw_without_materializing_rollout(Event {
id: sub_id,
msg: thread_settings_applied_event(sess).await,
})
.await;
}
Err(err) => {
sess.send_event_raw(Event {
id: sub_id,
msg: EventMsg::Error(ErrorEvent {
message: format!("invalid thread settings override: {err}"),
codex_error_info: Some(CodexErrorInfo::BadRequest),
}),
})
.await;
}
}
}
async fn thread_settings_update(
sess: &Session,
thread_settings: ThreadSettingsOverrides,
) -> SessionSettingsUpdate {
let ThreadSettingsOverrides {
environments,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
model,
effort,
summary,
service_tier,
collaboration_mode,
personality,
} = thread_settings;
let collaboration_mode = match collaboration_mode {
Some(collaboration_mode) => collaboration_mode,
None => {
let state = sess.state.lock().await;
// Model and reasoning effort live in CollaborationMode settings today, so
// partial thread-settings updates refresh those fields on the active mode.
state
.session_configuration
.collaboration_mode
.with_updates(model, effort, /*developer_instructions*/ None)
}
};
SessionSettingsUpdate {
environments,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
collaboration_mode: Some(collaboration_mode),
reasoning_summary: summary,
service_tier,
personality,
..Default::default()
}
}
pub(super) async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
let snapshot = {
let state = sess.state.lock().await;
state.session_configuration.thread_config_snapshot()
};
EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent {
thread_settings: snapshot.into_thread_settings_snapshot(),
})
}
pub(super) async fn user_input_or_turn_inner(
sess: &Arc<Session>,
sub_id: String,
op: Op,
client_user_message_id: Option<String>,
parent_turn_id: Option<String>,
root_turn_id: Option<String>,
) -> CodexResult<UserMessageAdmission> {
let Op::UserInput {
items,
final_output_json_schema,
responsesapi_client_metadata,
additional_context,
thread_settings,
} = op
else {
unreachable!();
};
let emit_thread_settings_applied = thread_settings != ThreadSettingsOverrides::default();
let mut updates = if emit_thread_settings_applied {
thread_settings_update(sess, thread_settings).await
} else {
SessionSettingsUpdate::default()
};
updates.final_output_json_schema = Some(final_output_json_schema);
// new_turn_with_sub_id already emits an error event when settings are invalid.
let current_context = sess.new_turn_with_sub_id(sub_id.clone(), updates).await?;
if emit_thread_settings_applied {
sess.send_event_raw_without_materializing_rollout(Event {
id: sub_id.clone(),
msg: thread_settings_applied_event(sess).await,
})
.await;
}
sess.maybe_emit_model_warnings_for_turn(current_context.as_ref())
.await;
if let Some(id) = root_turn_id.as_ref() {
current_context
.turn_metadata_state
.set_root_turn_id(id.clone());
}
let incoming_turn_metadata = parent_turn_id
.as_ref()
.map(|_| current_context.turn_metadata_state.as_ref());
match sess
.steer_input(
items.clone(),
additional_context.clone(),
/*expected_turn_id*/ None,
client_user_message_id.clone(),
responsesapi_client_metadata.clone(),
incoming_turn_metadata,
)
.await
{
Ok(turn_id) => {
current_context.session_telemetry.user_prompt(&items);
Ok(UserMessageAdmission::Steered { turn_id })
}
Err(SteerInputError::NoActiveTurn(items)) => {
if root_turn_id.is_none()
&& parent_turn_id.is_none()
&& !items.is_empty()
&& current_context
.turn_metadata_state
.can_start_root_turn(&current_context.session_source)
{
current_context
.turn_metadata_state
.set_root_turn_id(sub_id.clone());
}
if let Some(id) = parent_turn_id {
current_context.turn_metadata_state.set_parent_turn_id(id);
}
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
current_context
.turn_metadata_state
.set_responsesapi_client_metadata(responsesapi_client_metadata);
}
current_context.session_telemetry.user_prompt(&items);
let additional_context_input = {
let mut state = sess.state.lock().await;
state.additional_context.merge(additional_context)
};
let mut task_input = additional_context_input
.into_iter()
.map(ResponseItem::from)
.map(|item| sess.annotate_client_response_item(item))
.map(TurnInput::ResponseItem)
.collect::<Vec<_>>();
if !items.is_empty() {
task_input.push(TurnInput::UserInput {
content: items,
client_id: client_user_message_id,
});
}
sess.spawn_task(
Arc::clone(&current_context),
task_input,
crate::tasks::RegularTask::new(),
)
.await;
Ok(UserMessageAdmission::Started { turn_id: sub_id })
}
Err(err) => {
sess.send_event_raw(Event {
id: sub_id.clone(),
msg: EventMsg::Error(err.to_error_event()),
})
.await;
Err(CodexErr::InvalidRequest(format!(
"failed to admit user message: {err:?}"
)))
}
}
}
/// Queues an inter-agent message, then lets the shared pending-work scheduler /// Queues an inter-agent message, then lets the shared pending-work scheduler
/// decide whether an idle session should start a regular turn. /// decide whether an idle session should start a regular turn.
pub async fn inter_agent_communication( pub async fn inter_agent_communication(
@@ -787,20 +556,17 @@ pub(super) async fn submission_loop(
realtime_conversation_list_voices(&sess, sub.id.clone()).await; realtime_conversation_list_voices(&sess, sub.id.clone()).await;
false false
} }
op @ Op::UserInput { .. } => { Op::TurnInput {
user_input_or_turn( request,
&sess, mode,
sub.id.clone(), reply,
op, } => {
sub.client_user_message_id, let result = turn_input::handle(&sess, *request, mode, sub.id.clone()).await;
sub.parent_turn_id, let _ = reply.send(result);
sub.root_turn_id,
)
.await;
false false
} }
Op::ThreadSettings { thread_settings } => { Op::ThreadSettings { thread_settings } => {
update_thread_settings(&sess, sub.id.clone(), thread_settings).await; thread_settings::update(&sess, sub.id.clone(), thread_settings).await;
false false
} }
Op::InterAgentCommunication { communication } => { Op::InterAgentCommunication { communication } => {
+6 -141
View File
@@ -1,18 +1,10 @@
use super::input_queue::TurnInput; use super::TurnInput as PendingTurnInput;
use super::session::Session; use super::session::Session;
use super::turn_context::TurnContext; use super::turn_context::TurnContext;
use crate::codex_thread::TryStartTurnIfIdleError;
use crate::codex_thread::TryStartTurnIfIdleRejectionReason;
use crate::state::ActiveTurn;
use crate::state::TurnState;
use crate::tasks::MailboxParentProvenance;
use crate::tasks::RegularTask;
use codex_features::Feature; use codex_features::Feature;
use codex_history::CodexHarnessMetadata; use codex_history::CodexHarnessMetadata;
use codex_history::ResponseItemEnvelope; use codex_history::ResponseItemEnvelope;
use codex_protocol::config_types::ModeKind;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
use std::sync::Arc;
impl Session { impl Session {
/// Returns the input if there is no active turn to inject into. /// Returns the input if there is no active turn to inject into.
@@ -33,7 +25,7 @@ impl Session {
input input
.into_iter() .into_iter()
.map(ResponseItemEnvelope::new) .map(ResponseItemEnvelope::new)
.map(TurnInput::ResponseItem) .map(PendingTurnInput::ResponseItem)
.collect(), .collect(),
) )
.await; .await;
@@ -62,7 +54,10 @@ impl Session {
self.input_queue self.input_queue
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state( .extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
active_turn.turn_state.as_ref(), active_turn.turn_state.as_ref(),
items.into_iter().map(TurnInput::ResponseItem).collect(), items
.into_iter()
.map(PendingTurnInput::ResponseItem)
.collect(),
) )
.await; .await;
return; return;
@@ -119,136 +114,6 @@ impl Session {
.await; .await;
} }
/// Starts a regular turn with the provided input only if automatic idle work
/// is allowed for the current session state.
///
/// This is the shared gate for extension-initiated idle work. It refuses to
/// start a turn when user/client-triggered work is queued or any task is
/// still active. Work without user input is also rejected in Plan mode.
/// Active Review tasks are covered by the active-task check because Review
/// turns are not steerable.
pub(crate) async fn try_start_turn_if_idle(
self: &Arc<Self>,
input: Vec<TurnInput>,
) -> Result<(), TryStartTurnIfIdleError> {
if input.is_empty() {
return Ok(());
}
let has_user_input = input.iter().any(
|item| matches!(item, TurnInput::UserInput { content, .. } if !content.is_empty()),
);
if self.input_queue.has_trigger_turn_mailbox_items().await {
return Err(TryStartTurnIfIdleError::new(
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
input,
));
}
if !has_user_input && self.collaboration_mode().await.mode == ModeKind::Plan {
return Err(TryStartTurnIfIdleError::new(
TryStartTurnIfIdleRejectionReason::PlanMode,
input,
));
}
let turn_state = {
let mut active_turn = self.active_turn.lock().await;
if active_turn.is_some() {
return Err(TryStartTurnIfIdleError::new(
TryStartTurnIfIdleRejectionReason::Busy,
input,
));
}
let active_turn = active_turn.get_or_insert_with(ActiveTurn::default);
Arc::clone(&active_turn.turn_state)
};
if self.input_queue.has_trigger_turn_mailbox_items().await {
self.clear_reserved_idle_turn(&turn_state).await;
self.maybe_start_turn_for_pending_work().await;
return Err(TryStartTurnIfIdleError::new(
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
input,
));
}
let turn_context = self
.new_default_turn_with_sub_id(uuid::Uuid::new_v4().to_string())
.await;
if !has_user_input && turn_context.mode == ModeKind::Plan {
self.clear_reserved_idle_turn(&turn_state).await;
self.maybe_start_turn_for_pending_work().await;
return Err(TryStartTurnIfIdleError::new(
TryStartTurnIfIdleRejectionReason::PlanMode,
input,
));
}
self.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
if self.input_queue.has_trigger_turn_mailbox_items().await {
self.clear_reserved_idle_turn(&turn_state).await;
self.maybe_start_turn_for_pending_work().await;
return Err(TryStartTurnIfIdleError::new(
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
input,
));
}
let still_reserved = {
let active_turn = self.active_turn.lock().await;
active_turn.as_ref().is_some_and(|active_turn| {
active_turn.task.is_none() && Arc::ptr_eq(&active_turn.turn_state, &turn_state)
})
};
if !still_reserved {
self.clear_reserved_idle_turn(&turn_state).await;
return Err(TryStartTurnIfIdleError::new(
TryStartTurnIfIdleRejectionReason::Busy,
input,
));
}
if has_user_input
&& turn_context
.turn_metadata_state
.can_start_root_turn(&turn_context.session_source)
{
turn_context
.turn_metadata_state
.set_root_turn_id(turn_context.sub_id.clone());
}
let task_input = if has_user_input {
self.clear_connector_selection().await;
for item in &input {
if let TurnInput::UserInput { content, .. } = item {
turn_context.session_telemetry.user_prompt(content);
}
}
input
} else {
self.input_queue
.extend_pending_input_for_turn_state(turn_state.as_ref(), input)
.await;
Vec::new()
};
self.start_task(
turn_context,
task_input,
RegularTask::new(),
MailboxParentProvenance::Ignore,
)
.await;
Ok(())
}
async fn clear_reserved_idle_turn(&self, turn_state: &Arc<tokio::sync::Mutex<TurnState>>) {
let mut active_turn_guard = self.active_turn.lock().await;
if let Some(active_turn) = active_turn_guard.as_ref()
&& active_turn.task.is_none()
&& Arc::ptr_eq(&active_turn.turn_state, turn_state)
{
*active_turn_guard = None;
}
}
/// Injects items into active work, or records them without starting a turn. /// Injects items into active work, or records them without starting a turn.
pub(crate) async fn inject_no_new_turn( pub(crate) async fn inject_no_new_turn(
&self, &self,
+1
View File
@@ -376,6 +376,7 @@ mod tests {
use super::*; use super::*;
use codex_history::CodexHarnessMetadata; use codex_history::CodexHarnessMetadata;
use codex_protocol::AgentPath; use codex_protocol::AgentPath;
use codex_protocol::user_input::UserInput;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
+36 -182
View File
@@ -1,5 +1,4 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::Debug; use std::fmt::Debug;
@@ -92,9 +91,7 @@ use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction; use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::AutoCompactTokenLimitScope; use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE; use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchMode;
use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolResponse;
use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::dynamic_tools::DynamicToolSpec;
@@ -111,7 +108,6 @@ use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelPreset;
use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AdditionalContextEntry;
use codex_protocol::protocol::FileChange; use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::HasLegacyEvent; use codex_protocol::protocol::HasLegacyEvent;
use codex_protocol::protocol::HistoryPosition; use codex_protocol::protocol::HistoryPosition;
@@ -221,17 +217,19 @@ mod rollout_reconstruction;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
pub(crate) mod session; pub(crate) mod session;
pub(crate) mod step_context; pub(crate) mod step_context;
mod thread_settings;
pub(crate) mod time_reminder; pub(crate) mod time_reminder;
mod token_budget; mod token_budget;
pub(crate) mod turn; pub(crate) mod turn;
pub(crate) mod turn_context; pub(crate) mod turn_context;
mod turn_input;
mod world_state; mod world_state;
use self::code_mode_warning::unsupported_code_mode_warning; use self::code_mode_warning::unsupported_code_mode_warning;
#[cfg(test)] #[cfg(test)]
use self::handlers::submission_dispatch_span; use self::handlers::submission_dispatch_span;
use self::handlers::submission_loop; use self::handlers::submission_loop;
pub(crate) use self::input_queue::InputQueueActivity; pub(crate) use self::input_queue::InputQueueActivity;
pub use self::input_queue::TurnInput; pub(crate) use self::input_queue::TurnInput;
pub(crate) use self::input_queue::TurnInputQueue; pub(crate) use self::input_queue::TurnInputQueue;
use self::review::spawn_review_thread; use self::review::spawn_review_thread;
use self::session::AppServerClientMetadata; use self::session::AppServerClientMetadata;
@@ -248,45 +246,6 @@ use self::turn_context::TurnContext;
#[cfg(test)] #[cfg(test)]
mod rollout_reconstruction_tests; mod rollout_reconstruction_tests;
#[derive(Debug, PartialEq)]
pub enum SteerInputError {
NoActiveTurn(Vec<UserInput>),
ExpectedTurnMismatch { expected: String, actual: String },
ActiveTurnNotSteerable { turn_kind: NonSteerableTurnKind },
EmptyInput,
}
impl SteerInputError {
fn to_error_event(&self) -> ErrorEvent {
match self {
Self::NoActiveTurn(_) => ErrorEvent {
message: "no active turn to steer".to_string(),
codex_error_info: Some(CodexErrorInfo::BadRequest),
},
Self::ExpectedTurnMismatch { expected, actual } => ErrorEvent {
message: format!("expected active turn id `{expected}` but found `{actual}`"),
codex_error_info: Some(CodexErrorInfo::BadRequest),
},
Self::ActiveTurnNotSteerable { turn_kind } => {
let turn_kind_label = match turn_kind {
NonSteerableTurnKind::Review => "review",
NonSteerableTurnKind::Compact => "compact",
};
ErrorEvent {
message: format!("cannot steer a {turn_kind_label} turn"),
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
turn_kind: *turn_kind,
}),
}
}
Self::EmptyInput => ErrorEvent {
message: "input must not be empty".to_string(),
codex_error_info: Some(CodexErrorInfo::BadRequest),
},
}
}
}
/// Notes from the previous real user turn. /// Notes from the previous real user turn.
/// ///
/// Conceptually this is the same role that `previous_model` used to fill, but /// Conceptually this is the same role that `previous_model` used to fill, but
@@ -344,8 +303,10 @@ use codex_otel::THREAD_STARTED_METRIC;
use codex_otel::TelemetryAuthMode; use codex_otel::TelemetryAuthMode;
use codex_protocol::ResponseItemId; use codex_protocol::ResponseItemId;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality; use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::mcp::ClientMcpExtensions;
use codex_protocol::models::LocalImagePreparation; use codex_protocol::models::LocalImagePreparation;
@@ -356,7 +317,6 @@ use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::DeprecationNoticeEvent; use codex_protocol::protocol::DeprecationNoticeEvent;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::Event; use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecApprovalRequestEvent; use codex_protocol::protocol::ExecApprovalRequestEvent;
@@ -365,7 +325,6 @@ use codex_protocol::protocol::ModelRerouteReason;
use codex_protocol::protocol::ModelVerification; use codex_protocol::protocol::ModelVerification;
use codex_protocol::protocol::ModelVerificationEvent; use codex_protocol::protocol::ModelVerificationEvent;
use codex_protocol::protocol::NetworkApprovalContext; use codex_protocol::protocol::NetworkApprovalContext;
use codex_protocol::protocol::NonSteerableTurnKind;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::RateLimitSnapshot; use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RequestUserInputEvent; use codex_protocol::protocol::RequestUserInputEvent;
@@ -381,6 +340,9 @@ use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo; use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::TurnModerationMetadataEvent; use codex_protocol::protocol::TurnModerationMetadataEvent;
use codex_protocol::protocol::WarningEvent; use codex_protocol::protocol::WarningEvent;
use codex_protocol::turn_input::TurnInputMode;
use codex_protocol::turn_input::TurnInputRequest;
use codex_protocol::turn_input::TurnInputSubmission;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_skills_extension::HostSkillsService; use codex_skills_extension::HostSkillsService;
use codex_tools::UnifiedExecShellMode; use codex_tools::UnifiedExecShellMode;
@@ -847,7 +809,6 @@ impl SessionIo {
let sub = Submission { let sub = Submission {
id: id.clone(), id: id.clone(),
op, op,
client_user_message_id: None,
trace, trace,
parent_turn_id, parent_turn_id,
root_turn_id, root_turn_id,
@@ -856,26 +817,6 @@ impl SessionIo {
Ok(id) Ok(id)
} }
pub(crate) async fn submit_user_input_with_client_user_message_id(
&self,
op: Op,
trace: Option<W3cTraceContext>,
client_user_message_id: Option<String>,
) -> CodexResult<String> {
debug_assert!(matches!(op, Op::UserInput { .. }));
let id = new_submission_id();
let sub = Submission {
id: id.clone(),
op,
client_user_message_id,
trace,
parent_turn_id: None,
root_turn_id: None,
};
self.submit_with_id(sub).await?;
Ok(id)
}
/// Use sparingly: prefer `submit()` so submission IDs are generated consistently. /// Use sparingly: prefer `submit()` so submission IDs are generated consistently.
pub(crate) async fn submit_with_id(&self, mut sub: Submission) -> CodexResult<()> { pub(crate) async fn submit_with_id(&self, mut sub: Submission) -> CodexResult<()> {
if sub.trace.is_none() { if sub.trace.is_none() {
@@ -888,6 +829,33 @@ impl SessionIo {
Ok(()) Ok(())
} }
/// Submits an ordered turn-input call and waits only for Core's routing decision.
///
/// Once queued, dropping the waiter does not retract the call. If the
/// session loop exits before replying, the caller gets `InternalAgentDied`.
pub(crate) async fn submit_turn_input(
&self,
mut request: TurnInputRequest,
mode: TurnInputMode,
) -> CodexResult<TurnInputSubmission> {
let id = new_submission_id();
let (reply_tx, reply_rx) = oneshot::channel();
let trace = request.trace.take();
self.submit_with_id(Submission {
id,
op: Op::TurnInput {
request: Box::new(request),
mode,
reply: reply_tx,
},
trace,
parent_turn_id: None,
root_turn_id: None,
})
.await?;
reply_rx.await.unwrap_or(Err(CodexErr::InternalAgentDied))
}
pub(crate) async fn shutdown_and_wait(&self) -> CodexResult<()> { pub(crate) async fn shutdown_and_wait(&self) -> CodexResult<()> {
let session_loop_termination = self.session_loop_termination.clone(); let session_loop_termination = self.session_loop_termination.clone();
match self.submit(Op::Shutdown).await { match self.submit(Op::Shutdown).await {
@@ -1248,27 +1216,6 @@ impl Session {
format!("auto-compact-{id}") format!("auto-compact-{id}")
} }
pub(crate) async fn route_realtime_text_input(self: &Arc<Self>, text: String) {
handlers::user_input_or_turn(
self,
Uuid::now_v7().to_string(),
Op::UserInput {
items: vec![UserInput::Text {
text,
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
},
/*client_user_message_id*/ None,
/*parent_turn_id*/ None,
/*root_turn_id*/ None,
)
.await;
}
pub(crate) async fn get_total_token_usage(&self) -> i64 { pub(crate) async fn get_total_token_usage(&self) -> i64 {
let state = self.state.lock().await; let state = self.state.lock().await;
state.get_total_token_usage(state.server_reasoning_included()) state.get_total_token_usage(state.server_reasoning_included())
@@ -1434,7 +1381,7 @@ impl Session {
} }
let thread_settings_applied = let thread_settings_applied =
RolloutItem::EventMsg(handlers::thread_settings_applied_event(self).await); RolloutItem::EventMsg(thread_settings::applied_event(self).await);
match &self.fork_persistence { match &self.fork_persistence {
ForkPersistence::Referenced { ForkPersistence::Referenced {
inherited_item_count, inherited_item_count,
@@ -4043,99 +3990,6 @@ impl Session {
self.send_event(turn_context, event).await; self.send_event(turn_context, event).await;
} }
/// Inject additional user input into the currently active turn.
///
/// Returns the active turn id when accepted.
#[expect(
clippy::await_holding_invalid_type,
reason = "active turn checks and turn state updates must remain atomic"
)]
pub async fn steer_input(
&self,
input: Vec<UserInput>,
additional_context: BTreeMap<String, AdditionalContextEntry>,
expected_turn_id: Option<&str>,
client_user_message_id: Option<String>,
responsesapi_client_metadata: Option<HashMap<String, String>>,
incoming_turn_metadata: Option<&TurnMetadataState>,
) -> Result<String, SteerInputError> {
let mut active = self.active_turn.lock().await;
let Some(active_turn) = active.as_mut() else {
return Err(SteerInputError::NoActiveTurn(input));
};
let Some(active_task) = active_turn.task.as_ref() else {
return Err(SteerInputError::NoActiveTurn(input));
};
let active_turn_id = &active_task.turn_context.sub_id;
if let Some(expected_turn_id) = expected_turn_id
&& expected_turn_id != active_turn_id
{
return Err(SteerInputError::ExpectedTurnMismatch {
expected: expected_turn_id.to_string(),
actual: active_turn_id.clone(),
});
}
match active_task.kind {
crate::state::TaskKind::Regular => {}
crate::state::TaskKind::Review => {
return Err(SteerInputError::ActiveTurnNotSteerable {
turn_kind: NonSteerableTurnKind::Review,
});
}
crate::state::TaskKind::Compact => {
return Err(SteerInputError::ActiveTurnNotSteerable {
turn_kind: NonSteerableTurnKind::Compact,
});
}
}
if input.is_empty() {
return Err(SteerInputError::EmptyInput);
}
let additional_context_input = {
let mut state = self.state.lock().await;
state.additional_context.merge(additional_context)
};
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
active_task
.turn_context
.turn_metadata_state
.set_responsesapi_client_metadata(responsesapi_client_metadata);
}
let mut pending_input = additional_context_input
.into_iter()
.map(ResponseItem::from)
.map(|item| self.annotate_client_response_item(item))
.map(TurnInput::ResponseItem)
.collect::<Vec<_>>();
pending_input.push(TurnInput::UserInput {
content: input,
client_id: client_user_message_id.clone(),
});
if let Some(incoming_turn_metadata) = incoming_turn_metadata
&& active_task.turn_context.turn_metadata_state.root_turn_id()
!= incoming_turn_metadata.root_turn_id()
{
active_task
.turn_context
.turn_metadata_state
.mark_root_turn_ambiguous();
}
self.input_queue
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
active_turn.turn_state.as_ref(),
pending_input,
)
.await;
Ok(active_turn_id.clone())
}
pub(crate) async fn record_memory_citation_for_turn(&self, sub_id: &str) { pub(crate) async fn record_memory_citation_for_turn(&self, sub_id: &str) {
let turn_state = self let turn_state = self
.input_queue .input_queue
-3
View File
@@ -57,8 +57,6 @@ pub(crate) struct Session {
pub(crate) conversation: Arc<RealtimeConversationManager>, pub(crate) conversation: Arc<RealtimeConversationManager>,
pub(crate) active_turn: Mutex<Option<ActiveTurn>>, pub(crate) active_turn: Mutex<Option<ActiveTurn>>,
pub(crate) async_hook_results: async_channel::Receiver<HookCompletedEvent>, pub(crate) async_hook_results: async_channel::Receiver<HookCompletedEvent>,
pub(crate) pending_user_message_admissions:
crate::user_message_admission::PendingUserMessageAdmissions,
pub(crate) input_queue: InputQueue, pub(crate) input_queue: InputQueue,
pub(crate) guardian_review_session: GuardianReviewSessionManager, pub(crate) guardian_review_session: GuardianReviewSessionManager,
pub(crate) services: SessionServices, pub(crate) services: SessionServices,
@@ -1312,7 +1310,6 @@ impl Session {
conversation: Arc::new(RealtimeConversationManager::new()), conversation: Arc::new(RealtimeConversationManager::new()),
active_turn: Mutex::new(None), active_turn: Mutex::new(None),
async_hook_results, async_hook_results,
pending_user_message_admissions: Default::default(),
input_queue: InputQueue::new(), input_queue: InputQueue::new(),
guardian_review_session: GuardianReviewSessionManager::default(), guardian_review_session: GuardianReviewSessionManager::default(),
services, services,
+62 -482
View File
@@ -2,7 +2,6 @@ use super::mcp_refresh::McpRefresh;
use super::turn_context::TurnEnvironment; use super::turn_context::TurnEnvironment;
use super::*; use super::*;
use crate::agents_md_manager::AgentsMdManager; use crate::agents_md_manager::AgentsMdManager;
use crate::codex_thread::TryStartTurnIfIdleRejectionReason;
use crate::config::ConfigBuilder; use crate::config::ConfigBuilder;
use crate::config::ConfigOverrides; use crate::config::ConfigOverrides;
use crate::config::test_config; use crate::config::test_config;
@@ -30,7 +29,7 @@ use codex_config::loader::project_trust_key;
use codex_config::types::McpServerConfig; use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig; use codex_config::types::McpServerTransportConfig;
use codex_config::types::ToolSuggestDisabledTool; use codex_config::types::ToolSuggestDisabledTool;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::TurnInputRequest as ExternalTurnInputRequest;
use codex_features::Feature; use codex_features::Feature;
use codex_http_client::ClientRouteClass; use codex_http_client::ClientRouteClass;
@@ -67,13 +66,18 @@ use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::NonSteerableTurnKind; use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::turn_input::TurnInput as SubmittedTurnInput;
use codex_protocol::turn_input::TurnInputMode;
use codex_protocol::turn_input::TurnInputRequest;
use codex_protocol::turn_input::TurnInputSubmission;
use codex_tools::ToolSpec; use codex_tools::ToolSpec;
use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUri;
use std::collections::BTreeMap;
use tracing::Span; use tracing::Span;
use crate::connectors::AppInfo; use crate::connectors::AppInfo;
@@ -3388,16 +3392,12 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
initial initial
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(ExternalTurnInputRequest::user_input(vec![
items: vec![UserInput::Text { UserInput::Text {
text: "fork seed".into(), text: "fork seed".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], },
final_output_json_schema: None, ]))
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
// Forking reads the persisted rollout JSONL, so force the completed source turn to disk // Forking reads the persisted rollout JSONL, so force the completed source turn to disk
@@ -3433,20 +3433,17 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
}; };
forked forked
.thread .thread
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { ExternalTurnInputRequest::user_input(vec![UserInput::Text {
text: "after fork".into(), text: "after fork".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
collaboration_mode: Some(collaboration_mode), collaboration_mode: Some(collaboration_mode),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -5981,7 +5978,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
conversation: Arc::new(RealtimeConversationManager::new()), conversation: Arc::new(RealtimeConversationManager::new()),
active_turn: Mutex::new(None), active_turn: Mutex::new(None),
async_hook_results, async_hook_results,
pending_user_message_admissions: Default::default(),
input_queue: super::input_queue::InputQueue::new(), input_queue: super::input_queue::InputQueue::new(),
guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(), guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(),
services, services,
@@ -6929,7 +6925,7 @@ async fn request_permissions_is_auto_denied_when_granular_policy_blocks_tool_req
} }
#[tokio::test] #[tokio::test]
async fn submit_with_id_captures_current_span_trace_context() { async fn submit_with_trace_captures_current_span_trace_context() {
let (_session, _turn_context) = make_session_and_context().await; let (_session, _turn_context) = make_session_and_context().await;
let (tx_sub, rx_sub) = async_channel::bounded(1); let (tx_sub, rx_sub) = async_channel::bounded(1);
let (_tx_event, rx_event) = async_channel::unbounded(); let (_tx_event, rx_event) = async_channel::unbounded();
@@ -6955,14 +6951,12 @@ async fn submit_with_id_captures_current_span_trace_context() {
let expected_trace = async { let expected_trace = async {
let expected_trace = let expected_trace =
current_span_w3c_trace_context().expect("current span should have trace context"); current_span_w3c_trace_context().expect("current span should have trace context");
io.submit_with_id(Submission { io.submit_with_trace(
id: "sub-1".into(), Op::Interrupt,
op: Op::Interrupt, /*trace*/ None,
client_user_message_id: None, /*parent_turn_id*/ None,
parent_turn_id: None, /*root_turn_id*/ None,
root_turn_id: None, )
trace: None,
})
.await .await
.expect("submit should succeed"); .expect("submit should succeed");
expected_trace expected_trace
@@ -7032,7 +7026,6 @@ fn submission_dispatch_span_prefers_submission_trace_context() {
submission_dispatch_span(&Submission { submission_dispatch_span(&Submission {
id: "sub-1".into(), id: "sub-1".into(),
op: Op::Interrupt, op: Op::Interrupt,
client_user_message_id: None,
parent_turn_id: None, parent_turn_id: None,
root_turn_id: None, root_turn_id: None,
trace: Some(submission_trace), trace: Some(submission_trace),
@@ -7061,7 +7054,6 @@ fn submission_dispatch_span_uses_debug_for_realtime_audio() {
item_id: None, item_id: None,
}, },
}), }),
client_user_message_id: None,
parent_turn_id: None, parent_turn_id: None,
root_turn_id: None, root_turn_id: None,
trace: None, trace: None,
@@ -7073,78 +7065,6 @@ fn submission_dispatch_span_uses_debug_for_realtime_audio() {
); );
} }
#[test]
fn op_kind_for_input_and_context_ops() {
assert_eq!(
Op::UserInput {
items: vec![],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
}
.kind(),
"user_input"
);
assert_eq!(
Op::ThreadSettings {
thread_settings: ThreadSettingsOverrides::default(),
}
.kind(),
"thread_settings"
);
}
#[tokio::test]
async fn user_turn_updates_approvals_reviewer() {
let (session, turn_context, _rx) = make_session_and_context_with_rx().await;
let config = session.get_config().await;
handlers::user_input_or_turn(
&session,
"sub-1".to_string(),
Op::UserInput {
items: vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(config.cwd.clone())),
approval_policy: Some(config.permissions.approval_policy.value()),
approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::AutoReview),
sandbox_policy: Some(config.legacy_sandbox_policy()),
summary: config.model_reasoning_summary,
personality: config.personality,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: turn_context.model_info.slug.clone(),
reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
..Default::default()
},
},
/*client_user_message_id*/ None,
/*parent_turn_id*/ None,
/*root_turn_id*/ None,
)
.await;
let state = session.state.lock().await;
assert_eq!(
state.session_configuration.approvals_reviewer,
codex_config::types::ApprovalsReviewer::AutoReview
);
assert!(
session.mcp_refresh.is_pending(),
"server elicitation authority changes must refresh MCP state"
);
}
#[tokio::test] #[tokio::test]
async fn turn_environments_set_primary_environment() { async fn turn_environments_set_primary_environment() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await; let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
@@ -7426,7 +7346,6 @@ async fn spawn_task_turn_span_inherits_dispatch_trace_context() {
let dispatch_span = submission_dispatch_span(&Submission { let dispatch_span = submission_dispatch_span(&Submission {
id: "sub-1".into(), id: "sub-1".into(),
op: Op::Interrupt, op: Op::Interrupt,
client_user_message_id: None,
parent_turn_id: None, parent_turn_id: None,
root_turn_id: None, root_turn_id: None,
trace: Some(submission_trace.clone()), trace: Some(submission_trace.clone()),
@@ -7749,10 +7668,10 @@ async fn submission_loop_channel_close_aborts_active_turn_before_thread_stop_lif
#[tokio::test] #[tokio::test]
async fn shutdown_and_wait_allows_multiple_waiters() { async fn shutdown_and_wait_allows_multiple_waiters() {
let (_session, _turn_context) = make_session_and_context().await; let (_session, _turn_context) = make_session_and_context().await;
let (tx_sub, rx_sub) = async_channel::bounded(4); let (tx_sub, rx_sub) = async_channel::bounded::<Submission>(4);
let (_tx_event, rx_event) = async_channel::unbounded(); let (_tx_event, rx_event) = async_channel::unbounded();
let session_loop_handle = tokio::spawn(async move { let session_loop_handle = tokio::spawn(async move {
let shutdown: Submission = rx_sub.recv().await.expect("shutdown submission"); let shutdown = rx_sub.recv().await.expect("shutdown submission");
assert!(matches!(shutdown.op, Op::Shutdown)); assert!(matches!(shutdown.op, Op::Shutdown));
tokio::time::sleep(StdDuration::from_millis(50)).await; tokio::time::sleep(StdDuration::from_millis(50)).await;
}); });
@@ -7836,11 +7755,11 @@ async fn shutdown_and_wait_shuts_down_cached_guardian_subagent() {
}; };
let (child_session, _child_turn_context) = make_session_and_context().await; let (child_session, _child_turn_context) = make_session_and_context().await;
let (child_tx_sub, child_rx_sub) = async_channel::bounded(4); let (child_tx_sub, child_rx_sub) = async_channel::bounded::<Submission>(4);
let (_child_tx_event, child_rx_event) = async_channel::unbounded(); let (_child_tx_event, child_rx_event) = async_channel::unbounded();
let (child_shutdown_tx, child_shutdown_rx) = tokio::sync::oneshot::channel(); let (child_shutdown_tx, child_shutdown_rx) = tokio::sync::oneshot::channel();
let child_session_loop_handle = tokio::spawn(async move { let child_session_loop_handle = tokio::spawn(async move {
let shutdown: Submission = child_rx_sub let shutdown = child_rx_sub
.recv() .recv()
.await .await
.expect("child shutdown submission"); .expect("child shutdown submission");
@@ -7921,11 +7840,11 @@ async fn shutdown_and_wait_shuts_down_tracked_ephemeral_guardian_review() {
}; };
let (child_session, _child_turn_context) = make_session_and_context().await; let (child_session, _child_turn_context) = make_session_and_context().await;
let (child_tx_sub, child_rx_sub) = async_channel::bounded(4); let (child_tx_sub, child_rx_sub) = async_channel::bounded::<Submission>(4);
let (_child_tx_event, child_rx_event) = async_channel::unbounded(); let (_child_tx_event, child_rx_event) = async_channel::unbounded();
let (child_shutdown_tx, child_shutdown_rx) = tokio::sync::oneshot::channel(); let (child_shutdown_tx, child_shutdown_rx) = tokio::sync::oneshot::channel();
let child_session_loop_handle = tokio::spawn(async move { let child_session_loop_handle = tokio::spawn(async move {
let shutdown: Submission = child_rx_sub let shutdown = child_rx_sub
.recv() .recv()
.await .await
.expect("child shutdown submission"); .expect("child shutdown submission");
@@ -8242,7 +8161,6 @@ where
conversation: Arc::new(RealtimeConversationManager::new()), conversation: Arc::new(RealtimeConversationManager::new()),
active_turn: Mutex::new(None), active_turn: Mutex::new(None),
async_hook_results, async_hook_results,
pending_user_message_admissions: Default::default(),
input_queue: super::input_queue::InputQueue::new(), input_queue: super::input_queue::InputQueue::new(),
guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(), guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(),
services, services,
@@ -10425,6 +10343,26 @@ async fn abort_gracefully_emits_marker_before_turn_aborted() {
assert!(rx.try_recv().is_err()); assert!(rx.try_recv().is_err());
} }
async fn submit_steer_only(
sess: &Arc<Session>,
input: Vec<UserInput>,
expected_turn_id: &str,
) -> TurnInputSubmission {
super::turn_input::handle(
sess,
TurnInputRequest::new(SubmittedTurnInput::UserInput {
content: input,
client_id: None,
}),
TurnInputMode::Steer {
expected_turn_id: expected_turn_id.to_string(),
},
"test-submission".to_string(),
)
.await
.expect("steer-only submission should be valid")
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() { async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() {
let (sess, tc, rx) = make_session_and_context_with_rx().await; let (sess, tc, rx) = make_session_and_context_with_rx().await;
@@ -10455,16 +10393,8 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
text: "late pending input".to_string(), text: "late pending input".to_string(),
text_elements: vec![text_element.clone()], text_elements: vec![text_element.clone()],
}]; }];
sess.steer_input( let submission = submit_steer_only(&sess, pending_user_input.clone(), &tc.sub_id).await;
pending_user_input.clone(), assert!(matches!(submission, TurnInputSubmission::Steered { .. }));
/*additional_context*/ Default::default(),
Some(&tc.sub_id),
/*client_user_message_id*/ None,
/*responsesapi_client_metadata*/ None,
/*incoming_turn_metadata*/ None,
)
.await
.expect("steer pending input into active turn");
sess.on_task_finished(Arc::clone(&tc), /*task_result*/ Ok(None)) sess.on_task_finished(Arc::clone(&tc), /*task_result*/ Ok(None))
.await; .await;
@@ -10644,350 +10574,6 @@ async fn thread_idle_lifecycle_waits_for_trigger_turn_mailbox_work() {
assert_eq!(0, calls.load(std::sync::atomic::Ordering::SeqCst)); assert_eq!(0, calls.load(std::sync::atomic::Ordering::SeqCst));
} }
#[tokio::test]
async fn try_start_turn_if_idle_rejects_active_turn_without_injecting() {
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
sess.spawn_task(
Arc::clone(&tc),
Vec::new(),
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: true,
},
)
.await;
let item = TurnInput::ResponseItem(user_message("synthetic idle input").into());
let err = sess
.try_start_turn_if_idle(vec![item.clone()])
.await
.expect_err("active turn should reject idle-only input");
assert_eq!(TryStartTurnIfIdleRejectionReason::Busy, err.reason());
assert_eq!(vec![item], err.into_input());
assert_eq!(
(Vec::<TurnInput>::new(), None, None),
sess.input_queue.get_pending_input(&sess.active_turn).await
);
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
}
#[tokio::test]
async fn try_start_turn_if_idle_rejects_plan_mode_without_injecting() {
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
let mut collaboration_mode = sess.collaboration_mode().await;
collaboration_mode.mode = ModeKind::Plan;
{
let mut state = sess.state.lock().await;
state.session_configuration.collaboration_mode = collaboration_mode;
}
let item = TurnInput::ResponseItem(user_message("synthetic idle input").into());
let err = sess
.try_start_turn_if_idle(vec![item.clone()])
.await
.expect_err("plan mode should reject automatic idle input");
assert_eq!(TryStartTurnIfIdleRejectionReason::PlanMode, err.reason());
assert_eq!(vec![item], err.into_input());
assert!(sess.active_turn.lock().await.is_none());
assert_eq!(
(Vec::<TurnInput>::new(), None, None),
sess.input_queue.get_pending_input(&sess.active_turn).await
);
}
#[tokio::test]
async fn try_start_turn_if_idle_accepts_user_input_in_plan_mode() {
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
let mut collaboration_mode = sess.collaboration_mode().await;
collaboration_mode.mode = ModeKind::Plan;
{
let mut state = sess.state.lock().await;
state.session_configuration.collaboration_mode = collaboration_mode;
state.merge_connector_selection(["calendar".to_string()]);
}
sess.try_start_turn_if_idle(vec![TurnInput::UserInput {
content: vec![UserInput::Text {
text: "queued user input".to_string(),
text_elements: Vec::new(),
}],
client_id: Some("queued-user-message".to_string()),
}])
.await
.expect("plan mode should accept user-authored idle input");
assert!(sess.state.lock().await.get_connector_selection().is_empty());
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
}
#[tokio::test]
async fn try_start_turn_if_idle_rejects_empty_user_input_in_plan_mode() {
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
let mut collaboration_mode = sess.collaboration_mode().await;
collaboration_mode.mode = ModeKind::Plan;
{
let mut state = sess.state.lock().await;
state.session_configuration.collaboration_mode = collaboration_mode;
}
let input = vec![
TurnInput::UserInput {
content: Vec::new(),
client_id: Some("empty-queued-user-message".to_string()),
},
TurnInput::ResponseItem(user_message("automatic idle input").into()),
];
let error = sess
.try_start_turn_if_idle(input.clone())
.await
.expect_err("empty user input should not bypass plan mode");
assert_eq!(TryStartTurnIfIdleRejectionReason::PlanMode, error.reason());
assert_eq!(input, error.into_input());
assert!(sess.active_turn.lock().await.is_none());
}
#[tokio::test]
async fn try_start_turn_if_idle_rejects_pending_trigger_turn_without_injecting() {
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
sess.input_queue
.enqueue_mailbox_communication(
InterAgentCommunication::new(
AgentPath::root(),
AgentPath::root(),
Vec::new(),
"pending trigger".to_string(),
/*trigger_turn*/ true,
),
/*parent_turn_id*/ None,
/*root_turn_id*/ None,
)
.await;
let item = TurnInput::ResponseItem(user_message("synthetic idle input").into());
let err = sess
.try_start_turn_if_idle(vec![item.clone()])
.await
.expect_err("pending trigger-turn mail should reject automatic idle input");
assert_eq!(
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
err.reason()
);
assert_eq!(vec![item], err.into_input());
assert!(sess.active_turn.lock().await.is_none());
assert!(sess.input_queue.has_trigger_turn_mailbox_items().await);
}
#[tokio::test]
async fn try_start_turn_if_idle_rejects_active_review_turn_without_injecting() {
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
sess.spawn_task(
Arc::clone(&tc),
Vec::new(),
NeverEndingTask {
kind: TaskKind::Review,
listen_to_cancellation_token: true,
},
)
.await;
let item = TurnInput::ResponseItem(user_message("synthetic idle input").into());
let err = sess
.try_start_turn_if_idle(vec![item.clone()])
.await
.expect_err("active review turn should reject automatic idle input");
assert_eq!(TryStartTurnIfIdleRejectionReason::Busy, err.reason());
assert_eq!(vec![item], err.into_input());
assert_eq!(
(Vec::<TurnInput>::new(), None, None),
sess.input_queue.get_pending_input(&sess.active_turn).await
);
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
}
#[tokio::test]
async fn steer_input_requires_active_turn() {
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
text: "steer".to_string(),
text_elements: Vec::new(),
}];
let err = sess
.steer_input(
input,
/*additional_context*/ Default::default(),
/*expected_turn_id*/ None,
/*client_user_message_id*/ None,
/*responsesapi_client_metadata*/ None,
/*incoming_turn_metadata*/ None,
)
.await
.expect_err("steering without active turn should fail");
assert!(matches!(err, SteerInputError::NoActiveTurn(_)));
}
#[tokio::test]
async fn steer_input_enforces_expected_turn_id() {
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
let input = vec![TurnInput::UserInput {
content: vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
client_id: None,
}];
sess.spawn_task(
Arc::clone(&tc),
input,
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: false,
},
)
.await;
let steer_input = vec![UserInput::Text {
text: "steer".to_string(),
text_elements: Vec::new(),
}];
let err = sess
.steer_input(
steer_input,
/*additional_context*/ Default::default(),
Some("different-turn-id"),
/*client_user_message_id*/ None,
/*responsesapi_client_metadata*/ None,
/*incoming_turn_metadata*/ None,
)
.await
.expect_err("mismatched expected turn id should fail");
match err {
SteerInputError::ExpectedTurnMismatch { expected, actual } => {
assert_eq!(
(expected, actual),
("different-turn-id".to_string(), tc.sub_id.clone())
);
}
other => panic!("unexpected error: {other:?}"),
}
}
#[tokio::test]
async fn steer_input_rejects_non_regular_turns() {
for (task_kind, turn_kind) in [
(TaskKind::Review, NonSteerableTurnKind::Review),
(TaskKind::Compact, NonSteerableTurnKind::Compact),
] {
let (sess, incoming_turn_context, _rx) = make_session_and_context_with_rx().await;
incoming_turn_context
.turn_metadata_state
.set_root_turn_id("incoming-root".to_string());
let input = vec![TurnInput::UserInput {
content: vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
client_id: None,
}];
let turn_context = sess.new_default_turn_with_sub_id("turn".to_string()).await;
turn_context
.turn_metadata_state
.set_root_turn_id("active-root".to_string());
sess.spawn_task(
Arc::clone(&turn_context),
input,
NeverEndingTask {
kind: task_kind,
listen_to_cancellation_token: true,
},
)
.await;
let steer_input = vec![UserInput::Text {
text: "steer".to_string(),
text_elements: Vec::new(),
}];
let err = sess
.steer_input(
steer_input,
/*additional_context*/ Default::default(),
/*expected_turn_id*/ None,
/*client_user_message_id*/ None,
/*responsesapi_client_metadata*/ None,
Some(incoming_turn_context.turn_metadata_state.as_ref()),
)
.await
.expect_err("steering a non-regular turn should fail");
assert_eq!(err, SteerInputError::ActiveTurnNotSteerable { turn_kind });
assert_eq!(
turn_context.turn_metadata_state.root_turn_id().as_deref(),
Some("active-root")
);
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
}
}
#[tokio::test]
async fn steer_input_returns_active_turn_id() {
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
tc.turn_metadata_state
.set_root_turn_id("active-root".to_string());
let incoming_turn_context = sess
.new_default_turn_with_sub_id("incoming".to_string())
.await;
incoming_turn_context
.turn_metadata_state
.set_root_turn_id("incoming-root".to_string());
let input = vec![TurnInput::UserInput {
content: vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
client_id: None,
}];
sess.spawn_task(
Arc::clone(&tc),
input,
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: false,
},
)
.await;
let steer_input = vec![UserInput::Text {
text: "steer".to_string(),
text_elements: Vec::new(),
}];
let turn_id = sess
.steer_input(
steer_input,
/*additional_context*/ Default::default(),
Some(&tc.sub_id),
/*client_user_message_id*/ None,
/*responsesapi_client_metadata*/ None,
Some(incoming_turn_context.turn_metadata_state.as_ref()),
)
.await
.expect("steering with matching expected turn id should succeed");
assert_eq!(turn_id, tc.sub_id);
assert_eq!(tc.turn_metadata_state.root_turn_id(), None);
assert!(sess.input_queue.has_pending_input(&sess.active_turn).await);
}
#[tokio::test] #[tokio::test]
async fn abort_empty_active_turn_preserves_pending_input() { async fn abort_empty_active_turn_preserves_pending_input() {
let (sess, _tc, _rx) = make_session_and_context_with_rx().await; let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
@@ -11150,19 +10736,16 @@ async fn steered_input_reopens_mailbox_delivery_for_current_turn() {
/*root_turn_id*/ None, /*root_turn_id*/ None,
) )
.await; .await;
sess.steer_input( let submission = submit_steer_only(
&sess,
vec![UserInput::Text { vec![UserInput::Text {
text: "follow up".to_string(), text: "follow up".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }],
/*additional_context*/ Default::default(), &tc.sub_id,
Some(&tc.sub_id),
/*client_user_message_id*/ None,
/*responsesapi_client_metadata*/ None,
/*incoming_turn_metadata*/ None,
) )
.await .await;
.expect("steered input should be accepted"); assert!(matches!(submission, TurnInputSubmission::Steered { .. }));
assert_eq!( assert_eq!(
(sess.input_queue.get_pending_input(&sess.active_turn).await).0, (sess.input_queue.get_pending_input(&sess.active_turn).await).0,
@@ -11209,19 +10792,16 @@ async fn stale_defer_mailbox_delivery_does_not_override_steered_input() {
/*root_turn_id*/ None, /*root_turn_id*/ None,
) )
.await; .await;
sess.steer_input( let submission = submit_steer_only(
&sess,
vec![UserInput::Text { vec![UserInput::Text {
text: "follow up".to_string(), text: "follow up".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }],
/*additional_context*/ Default::default(), &tc.sub_id,
Some(&tc.sub_id),
/*client_user_message_id*/ None,
/*responsesapi_client_metadata*/ None,
/*incoming_turn_metadata*/ None,
) )
.await .await;
.expect("steered input should be accepted"); assert!(matches!(submission, TurnInputSubmission::Steered { .. }));
sess.input_queue sess.input_queue
.defer_mailbox_delivery_to_next_turn(&sess.active_turn, &tc.sub_id) .defer_mailbox_delivery_to_next_turn(&sess.active_turn, &tc.sub_id)
@@ -0,0 +1,118 @@
//! Handles persistent thread-settings updates shared by standalone settings
//! submissions and turn-input submission.
use super::session::Session;
use super::session::SessionSettingsUpdate;
use crate::config::ConstraintResult;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ThreadSettingsAppliedEvent;
use codex_protocol::protocol::ThreadSettingsOverrides;
use std::sync::Arc;
/// Applies standalone thread settings and reports invalid overrides through the
/// normal event stream.
pub(super) async fn update(
session: &Arc<Session>,
submission_id: String,
overrides: ThreadSettingsOverrides,
) {
let updates = prepare_update(session, overrides).await;
if let Err(error) = apply_update(session, submission_id.clone(), updates).await {
session
.send_event_raw(Event {
id: submission_id,
msg: EventMsg::Error(ErrorEvent {
message: format!("invalid thread settings override: {error}"),
codex_error_info: Some(CodexErrorInfo::BadRequest),
}),
})
.await;
}
}
/// Converts protocol overrides into the internal settings update shape.
pub(super) async fn prepare_update(
session: &Session,
overrides: ThreadSettingsOverrides,
) -> SessionSettingsUpdate {
let ThreadSettingsOverrides {
environments,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
model,
effort,
summary,
service_tier,
collaboration_mode,
personality,
} = overrides;
let collaboration_mode = match collaboration_mode {
Some(collaboration_mode) => collaboration_mode,
None => {
let state = session.state.lock().await;
// Model and reasoning effort live in CollaborationMode settings today, so
// partial thread-settings updates refresh those fields on the active mode.
state
.session_configuration
.collaboration_mode
.with_updates(model, effort, /*developer_instructions*/ None)
}
};
SessionSettingsUpdate {
environments,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
collaboration_mode: Some(collaboration_mode),
reasoning_summary: summary,
service_tier,
personality,
..Default::default()
}
}
/// Applies persistent settings and emits the resulting effective snapshot.
pub(super) async fn apply_update(
session: &Session,
submission_id: String,
updates: SessionSettingsUpdate,
) -> ConstraintResult<()> {
session.update_settings(updates).await?;
emit_applied(session, submission_id).await;
Ok(())
}
/// Emits the effective thread settings after a successful update.
pub(super) async fn emit_applied(session: &Session, submission_id: String) {
let msg = applied_event(session).await;
session
.send_event_raw_without_materializing_rollout(Event {
id: submission_id,
msg,
})
.await;
}
/// Builds the effective thread-settings event used by live updates and
/// synthesized fork history.
pub(super) async fn applied_event(session: &Session) -> EventMsg {
let snapshot = {
let state = session.state.lock().await;
state.session_configuration.thread_config_snapshot()
};
EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent {
thread_settings: snapshot.into_thread_settings_snapshot(),
})
}
+581
View File
@@ -0,0 +1,581 @@
//! Handles reply-bearing turn-input operations.
//!
//! This is the one place Core decides whether submitted input starts a turn,
//! steers an active turn, or is rejected. It replies after that decision; it
//! does not wait for user-prompt hooks, updating the in-memory model context,
//! rollout persistence, or sampling.
//!
//! Persistent thread settings apply on Started and Steered. Turn start
//! options only apply on Started.
use super::TurnInput;
use super::session::Session;
use super::session::SessionSettingsUpdate;
use super::thread_settings;
use super::turn_context::TurnContext;
use crate::state::ActiveTurn;
use crate::state::TurnState;
use crate::tasks::MailboxParentProvenance;
use crate::tasks::RegularTask;
use codex_protocol::config_types::ModeKind;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AdditionalContextEntry;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::NonSteerableTurnKind;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::turn_input::NotSubmittedReason;
use codex_protocol::turn_input::TurnInput as SubmittedTurnInput;
use codex_protocol::turn_input::TurnInputMode;
use codex_protocol::turn_input::TurnInputRequest;
use codex_protocol::turn_input::TurnInputSubmission;
use codex_protocol::turn_input::TurnStartOptions;
use codex_protocol::user_input::UserInput;
use serde_json::Value;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
#[cfg(test)]
#[path = "turn_input_tests.rs"]
mod tests;
/// Thread settings and start-only options prepared before Core knows whether
/// turn input starts or steers.
///
/// Thread settings are validated up front but only applied after Core accepts
/// the input. Start-only options are only consumed by `apply_started`.
struct PreparedTurnInputSettings {
thread_settings_update: Option<SessionSettingsUpdate>,
start_options: TurnStartOptions,
}
impl PreparedTurnInputSettings {
/// Validates turn-input settings without applying them so rejected input
/// leaves the thread unchanged.
async fn prepare(
session: &Session,
thread_settings: ThreadSettingsOverrides,
start_options: TurnStartOptions,
) -> CodexResult<Self> {
let thread_settings_update = if thread_settings == ThreadSettingsOverrides::default() {
None
} else {
let updates = thread_settings::prepare_update(session, thread_settings).await;
session
.preview_settings(&updates)
.await
.map_err(|error| CodexErr::InvalidRequest(error.to_string()))?;
Some(updates)
};
Ok(Self {
thread_settings_update,
start_options,
})
}
fn required_active_final_output_json_schema(&self) -> Option<&Value> {
self.start_options.final_output_json_schema.as_ref()
}
fn would_enter_plan_mode(&self) -> bool {
self.thread_settings_update
.as_ref()
.and_then(|updates| updates.collaboration_mode.as_ref())
.is_some_and(|collaboration_mode| collaboration_mode.mode == ModeKind::Plan)
}
/// Applies persistent settings and start-only options before creating a
/// new turn context.
async fn apply_started(
self,
session: &Arc<Session>,
submission_id: String,
) -> CodexResult<Arc<TurnContext>> {
let TurnStartOptions {
final_output_json_schema,
parent_turn_id,
root_turn_id,
} = self.start_options;
let emit_thread_settings_applied = self.thread_settings_update.is_some();
let mut updates = self.thread_settings_update.unwrap_or_default();
updates.final_output_json_schema = Some(final_output_json_schema);
// new_turn_with_sub_id already emits an error event when settings are invalid.
let turn_context = session
.new_turn_with_sub_id(submission_id.clone(), updates)
.await?;
if emit_thread_settings_applied {
thread_settings::emit_applied(session, submission_id).await;
}
if let Some(parent_turn_id) = parent_turn_id {
turn_context
.turn_metadata_state
.set_parent_turn_id(parent_turn_id);
}
if let Some(root_turn_id) = root_turn_id {
turn_context
.turn_metadata_state
.set_root_turn_id(root_turn_id);
}
Ok(turn_context)
}
/// Applies only persistent settings after steering succeeds. The active
/// turn keeps its existing context; subsequent turns see the update.
async fn apply_steered(self, session: &Session, submission_id: String) -> CodexResult<()> {
let Some(thread_settings_update) = self.thread_settings_update else {
return Ok(());
};
thread_settings::apply_update(session, submission_id, thread_settings_update)
.await
.map_err(|error| CodexErr::InvalidRequest(error.to_string()))
}
}
pub(super) async fn handle(
session: &Arc<Session>,
request: TurnInputRequest,
mode: TurnInputMode,
submission_id: String,
) -> CodexResult<TurnInputSubmission> {
match mode {
TurnInputMode::StartOrSteer => start_or_steer(session, request, submission_id).await,
TurnInputMode::StartIfIdle => start_if_idle(session, request, submission_id).await,
TurnInputMode::Steer { expected_turn_id } => {
steer(session, request, expected_turn_id, submission_id).await
}
}
}
async fn start_or_steer(
session: &Arc<Session>,
request: TurnInputRequest,
submission_id: String,
) -> CodexResult<TurnInputSubmission> {
let TurnInputRequest {
input,
thread_settings,
start,
additional_context,
responsesapi_client_metadata,
..
} = request;
let SubmittedTurnInput::UserInput {
content: mut items,
client_id,
} = input
else {
return Err(CodexErr::InvalidRequest(
"only user input can steer a turn".to_string(),
));
};
let can_start_root_turn = start.parent_turn_id.is_none() && start.root_turn_id.is_none();
let incoming_root_turn_id = start
.parent_turn_id
.as_ref()
.map(|_| start.root_turn_id.clone());
let settings = PreparedTurnInputSettings::prepare(session, thread_settings, start).await?;
match session
.steer_input(
&mut items,
additional_context.clone(),
/*expected_turn_id*/ None,
settings.required_active_final_output_json_schema(),
client_id.clone(),
responsesapi_client_metadata.clone(),
incoming_root_turn_id,
)
.await
{
Ok(turn_id) => {
settings.apply_steered(session, submission_id).await?;
Ok(TurnInputSubmission::Steered { turn_id })
}
Err(NotSubmittedReason::NoActiveTurn) => {
let turn_context = settings
.apply_started(session, submission_id.clone())
.await?;
if can_start_root_turn
&& !items.is_empty()
&& turn_context
.turn_metadata_state
.can_start_root_turn(&turn_context.session_source)
{
turn_context
.turn_metadata_state
.set_root_turn_id(submission_id.clone());
}
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
turn_context
.turn_metadata_state
.set_responsesapi_client_metadata(responsesapi_client_metadata);
}
session
.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
turn_context.session_telemetry.user_prompt(&items);
let mut task_input = merge_additional_context_input(session, additional_context).await;
if !items.is_empty() {
task_input.push(TurnInput::UserInput {
content: items,
client_id,
});
}
session
.spawn_task(turn_context, task_input, RegularTask::new())
.await;
Ok(TurnInputSubmission::Started {
turn_id: submission_id,
})
}
Err(reason) => Ok(TurnInputSubmission::NotSubmitted { reason }),
}
}
async fn start_if_idle(
session: &Arc<Session>,
request: TurnInputRequest,
submission_id: String,
) -> CodexResult<TurnInputSubmission> {
let TurnInputRequest {
input,
thread_settings,
start,
additional_context,
responsesapi_client_metadata,
..
} = request;
let has_user_input = has_nonempty_user_input(&input);
let can_start_root_turn = start.parent_turn_id.is_none() && start.root_turn_id.is_none();
if session.input_queue.has_trigger_turn_mailbox_items().await {
return Ok(TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::PendingTriggerTurn,
});
}
if !has_user_input && session.collaboration_mode().await.mode == ModeKind::Plan {
return Ok(TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::PlanMode,
});
}
let turn_state = {
let mut active_turn = session.active_turn.lock().await;
if active_turn.is_some() {
return Ok(TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::NotIdle,
});
}
let active_turn = active_turn.get_or_insert_with(ActiveTurn::default);
Arc::clone(&active_turn.turn_state)
};
if session.input_queue.has_trigger_turn_mailbox_items().await {
session.clear_reserved_idle_turn(&turn_state).await;
session.maybe_start_turn_for_pending_work().await;
return Ok(TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::PendingTriggerTurn,
});
}
let settings = match PreparedTurnInputSettings::prepare(session, thread_settings, start).await {
Ok(settings) => settings,
Err(error) => {
session.clear_reserved_idle_turn(&turn_state).await;
return Err(error);
}
};
// Automatic work must not use persistent settings to start a turn
// whose effective collaboration mode is Plan.
if !has_user_input && settings.would_enter_plan_mode() {
session.clear_reserved_idle_turn(&turn_state).await;
return Ok(TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::PlanMode,
});
}
let turn_context = match settings.apply_started(session, submission_id.clone()).await {
Ok(turn_context) => turn_context,
Err(error) => {
session.clear_reserved_idle_turn(&turn_state).await;
return Err(error);
}
};
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
turn_context
.turn_metadata_state
.set_responsesapi_client_metadata(responsesapi_client_metadata);
}
if has_user_input
&& can_start_root_turn
&& turn_context
.turn_metadata_state
.can_start_root_turn(&turn_context.session_source)
{
turn_context
.turn_metadata_state
.set_root_turn_id(submission_id.clone());
}
session
.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
let mut task_input = merge_additional_context_input(session, additional_context).await;
if has_user_input {
session.clear_connector_selection().await;
if let SubmittedTurnInput::UserInput { content, .. } = &input {
turn_context.session_telemetry.user_prompt(content);
}
task_input.push(pending_turn_input(input));
} else {
session
.input_queue
.extend_pending_input_for_turn_state(
turn_state.as_ref(),
vec![pending_turn_input(input)],
)
.await;
}
session
.start_task(
turn_context,
task_input,
RegularTask::new(),
MailboxParentProvenance::Ignore,
)
.await;
Ok(TurnInputSubmission::Started {
turn_id: submission_id,
})
}
async fn steer(
session: &Arc<Session>,
request: TurnInputRequest,
expected_turn_id: String,
submission_id: String,
) -> CodexResult<TurnInputSubmission> {
let TurnInputRequest {
input,
thread_settings,
start,
additional_context,
responsesapi_client_metadata,
..
} = request;
let SubmittedTurnInput::UserInput {
content: mut items,
client_id,
} = input
else {
return Err(CodexErr::InvalidRequest(
"only user input can steer a turn".to_string(),
));
};
let incoming_root_turn_id = start
.parent_turn_id
.as_ref()
.map(|_| start.root_turn_id.clone());
let settings = PreparedTurnInputSettings::prepare(session, thread_settings, start).await?;
match session
.steer_input(
&mut items,
additional_context,
Some(expected_turn_id.as_str()),
settings.required_active_final_output_json_schema(),
client_id,
responsesapi_client_metadata,
incoming_root_turn_id,
)
.await
{
Ok(turn_id) => {
settings.apply_steered(session, submission_id).await?;
Ok(TurnInputSubmission::Steered { turn_id })
}
Err(reason) => Ok(TurnInputSubmission::NotSubmitted { reason }),
}
}
impl Session {
pub(crate) async fn route_realtime_text_input(self: &Arc<Self>, text: String) {
let submission_id = Uuid::now_v7().to_string();
let submission = handle(
self,
TurnInputRequest::user_input(vec![UserInput::Text {
text,
text_elements: Vec::new(),
}]),
TurnInputMode::StartOrSteer,
submission_id.clone(),
)
.await;
match submission {
Ok(TurnInputSubmission::Started { .. } | TurnInputSubmission::Steered { .. }) => {}
Ok(TurnInputSubmission::NotSubmitted { reason }) => {
self.send_event_raw(Event {
id: submission_id,
msg: EventMsg::Error(ErrorEvent {
message: format!("failed to submit turn input: {reason:?}"),
codex_error_info: Some(CodexErrorInfo::BadRequest),
}),
})
.await;
}
Err(error) => {
self.send_event_raw(Event {
id: submission_id,
msg: EventMsg::Error(error.to_error_event(/*message_prefix*/ None)),
})
.await;
}
}
}
async fn clear_reserved_idle_turn(&self, turn_state: &Arc<tokio::sync::Mutex<TurnState>>) {
let mut active_turn_guard = self.active_turn.lock().await;
if let Some(active_turn) = active_turn_guard.as_ref()
&& active_turn.task.is_none()
&& Arc::ptr_eq(&active_turn.turn_state, turn_state)
{
*active_turn_guard = None;
}
}
/// Inject additional user input into the currently active turn.
///
/// Returns the active turn id when accepted.
#[expect(
clippy::await_holding_invalid_type,
reason = "active turn checks and turn state updates must remain atomic"
)]
#[expect(
clippy::too_many_arguments,
reason = "steering carries the accepted input plus its turn-scoped metadata"
)]
async fn steer_input(
&self,
input: &mut Vec<UserInput>,
additional_context: BTreeMap<String, AdditionalContextEntry>,
expected_turn_id: Option<&str>,
required_final_output_json_schema: Option<&Value>,
client_user_message_id: Option<String>,
responsesapi_client_metadata: Option<HashMap<String, String>>,
incoming_root_turn_id: Option<Option<String>>,
) -> Result<String, NotSubmittedReason> {
let mut active = self.active_turn.lock().await;
let Some(active_turn) = active.as_mut() else {
return Err(NotSubmittedReason::NoActiveTurn);
};
let Some(active_task) = active_turn.task.as_ref() else {
return Err(NotSubmittedReason::NoActiveTurn);
};
let active_turn_id = &active_task.turn_context.sub_id;
if let Some(expected_turn_id) = expected_turn_id
&& expected_turn_id != active_turn_id
{
return Err(NotSubmittedReason::ExpectedTurnMismatch {
expected: expected_turn_id.to_string(),
actual: active_turn_id.clone(),
});
}
match active_task.kind {
crate::state::TaskKind::Regular => {}
crate::state::TaskKind::Review => {
return Err(NotSubmittedReason::ActiveTurnNotSteerable {
turn_kind: NonSteerableTurnKind::Review,
});
}
crate::state::TaskKind::Compact => {
return Err(NotSubmittedReason::ActiveTurnNotSteerable {
turn_kind: NonSteerableTurnKind::Compact,
});
}
}
if input.is_empty() {
return Err(NotSubmittedReason::EmptyInput);
}
// Compare JSON values directly instead of serialized schema text.
// Value equality ignores object key order while preserving array and
// scalar distinctions; broader JSON Schema equivalence is out of scope.
if let Some(required_schema) = required_final_output_json_schema
&& active_task.turn_context.final_output_json_schema.as_ref() != Some(required_schema)
{
return Err(NotSubmittedReason::ActiveTurnOutputSchemaMismatch);
}
active_task
.turn_context
.session_telemetry
.user_prompt(input);
let mut pending_input = merge_additional_context_input(self, additional_context).await;
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
active_task
.turn_context
.turn_metadata_state
.set_responsesapi_client_metadata(responsesapi_client_metadata);
}
pending_input.push(TurnInput::UserInput {
content: std::mem::take(input),
client_id: client_user_message_id,
});
if let Some(incoming_root_turn_id) = incoming_root_turn_id
&& active_task.turn_context.turn_metadata_state.root_turn_id() != incoming_root_turn_id
{
active_task
.turn_context
.turn_metadata_state
.mark_root_turn_ambiguous();
}
self.input_queue
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
active_turn.turn_state.as_ref(),
pending_input,
)
.await;
Ok(active_turn_id.clone())
}
}
fn has_nonempty_user_input(input: &SubmittedTurnInput) -> bool {
matches!(input, SubmittedTurnInput::UserInput { content, .. } if !content.is_empty())
}
async fn merge_additional_context_input(
session: &Session,
additional_context: BTreeMap<String, AdditionalContextEntry>,
) -> Vec<TurnInput> {
let additional_context_input = {
let mut state = session.state.lock().await;
state.additional_context.merge(additional_context)
};
additional_context_input
.into_iter()
.map(ResponseItem::from)
.map(|item| session.annotate_client_response_item(item))
.map(TurnInput::ResponseItem)
.collect()
}
fn pending_turn_input(input: SubmittedTurnInput) -> TurnInput {
match input {
SubmittedTurnInput::UserInput { content, client_id } => {
TurnInput::UserInput { content, client_id }
}
SubmittedTurnInput::ResponseItem(item) => TurnInput::ResponseItem(item.into()),
SubmittedTurnInput::InterAgentCommunication(communication) => {
TurnInput::InterAgentCommunication(communication)
}
}
}
@@ -0,0 +1,433 @@
use super::*;
use crate::session::tests::make_session_and_context_with_rx;
use crate::state::TaskKind;
use crate::tasks::SessionTask;
use crate::tasks::SessionTaskResult;
use codex_protocol::AgentPath;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::turn_input::TurnInput as SubmittedTurnInput;
use codex_protocol::user_input::UserInput;
use core_test_support::test_codex::local_selections;
use pretty_assertions::assert_eq;
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
#[derive(Clone, Copy)]
struct NeverEndingTask {
kind: TaskKind,
listen_to_cancellation_token: bool,
}
impl SessionTask for NeverEndingTask {
fn kind(&self) -> TaskKind {
self.kind
}
fn span_name(&self) -> &'static str {
"session_task.turn_input_test"
}
async fn run(
self: Arc<Self>,
_session: Arc<Session>,
_ctx: Arc<TurnContext>,
_input: Vec<TurnInput>,
cancellation_token: CancellationToken,
) -> SessionTaskResult {
if self.listen_to_cancellation_token {
cancellation_token.cancelled().await;
return Ok(None);
}
loop {
sleep(std::time::Duration::from_secs(60)).await;
}
}
}
fn user_message(text: &str) -> ResponseItem {
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: text.to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
}
}
async fn submit_start_only(
session: &Arc<Session>,
input: SubmittedTurnInput,
) -> TurnInputSubmission {
handle(
session,
TurnInputRequest::new(input),
TurnInputMode::StartIfIdle,
"test-submission".to_string(),
)
.await
.expect("start-only submission should be valid")
}
async fn submit_steer_only(
session: &Arc<Session>,
input: Vec<UserInput>,
expected_turn_id: &str,
) -> TurnInputSubmission {
handle(
session,
TurnInputRequest::new(SubmittedTurnInput::UserInput {
content: input,
client_id: None,
}),
TurnInputMode::Steer {
expected_turn_id: expected_turn_id.to_string(),
},
"test-submission".to_string(),
)
.await
.expect("steer-only submission should be valid")
}
#[tokio::test]
async fn accepted_input_applies_thread_settings() {
let (session, turn_context, _rx) = make_session_and_context_with_rx().await;
let config = session.get_config().await;
handle(
&session,
TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}])
.with_thread_settings(ThreadSettingsOverrides {
environments: Some(local_selections(config.cwd.clone())),
approval_policy: Some(config.permissions.approval_policy.value()),
approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::AutoReview),
sandbox_policy: Some(config.legacy_sandbox_policy()),
summary: config.model_reasoning_summary,
personality: config.personality,
collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: turn_context.model_info.slug.clone(),
reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
..Default::default()
}),
TurnInputMode::StartOrSteer,
"sub-1".to_string(),
)
.await
.expect("submit user turn");
let state = session.state.lock().await;
assert_eq!(
state.session_configuration.approvals_reviewer,
codex_config::types::ApprovalsReviewer::AutoReview
);
assert!(
session.mcp_refresh.is_pending(),
"server elicitation authority changes must refresh MCP state"
);
}
#[tokio::test]
async fn start_only_rejects_active_turn_without_injecting() {
let (session, turn_context, _rx) = make_session_and_context_with_rx().await;
session
.spawn_task(
Arc::clone(&turn_context),
Vec::new(),
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: true,
},
)
.await;
let input = SubmittedTurnInput::ResponseItem(user_message("synthetic idle input"));
let submission = submit_start_only(&session, input).await;
assert_eq!(
TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::NotIdle,
},
submission
);
assert_eq!(
(Vec::<TurnInput>::new(), None, None),
session
.input_queue
.get_pending_input(&session.active_turn)
.await
);
session.abort_all_tasks(TurnAbortReason::Interrupted).await;
}
#[tokio::test]
async fn start_only_rejects_plan_mode_without_injecting() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
let mut collaboration_mode = session.collaboration_mode().await;
collaboration_mode.mode = ModeKind::Plan;
{
let mut state = session.state.lock().await;
state.session_configuration.collaboration_mode = collaboration_mode;
}
let submission = submit_start_only(
&session,
SubmittedTurnInput::ResponseItem(user_message("synthetic idle input")),
)
.await;
assert_eq!(
TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::PlanMode,
},
submission
);
assert!(session.active_turn.lock().await.is_none());
assert_eq!(
(Vec::<TurnInput>::new(), None, None),
session
.input_queue
.get_pending_input(&session.active_turn)
.await
);
}
#[tokio::test]
async fn start_only_accepts_user_input_in_plan_mode() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
let mut collaboration_mode = session.collaboration_mode().await;
collaboration_mode.mode = ModeKind::Plan;
{
let mut state = session.state.lock().await;
state.session_configuration.collaboration_mode = collaboration_mode;
state.merge_connector_selection(["calendar".to_string()]);
}
let submission = submit_start_only(
&session,
SubmittedTurnInput::UserInput {
content: vec![UserInput::Text {
text: "queued user input".to_string(),
text_elements: Vec::new(),
}],
client_id: Some("queued-user-message".to_string()),
},
)
.await;
assert!(matches!(submission, TurnInputSubmission::Started { .. }));
assert!(
session
.state
.lock()
.await
.get_connector_selection()
.is_empty()
);
session.abort_all_tasks(TurnAbortReason::Interrupted).await;
}
#[tokio::test]
async fn start_only_rejects_empty_user_input_in_plan_mode() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
let mut collaboration_mode = session.collaboration_mode().await;
collaboration_mode.mode = ModeKind::Plan;
{
let mut state = session.state.lock().await;
state.session_configuration.collaboration_mode = collaboration_mode;
}
let submission = submit_start_only(
&session,
SubmittedTurnInput::UserInput {
content: Vec::new(),
client_id: Some("empty-queued-user-message".to_string()),
},
)
.await;
assert_eq!(
TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::PlanMode,
},
submission
);
assert!(session.active_turn.lock().await.is_none());
}
#[tokio::test]
async fn start_only_rejects_pending_trigger_turn_without_injecting() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
session
.input_queue
.enqueue_mailbox_communication(
InterAgentCommunication::new(
AgentPath::root(),
AgentPath::root(),
Vec::new(),
"pending trigger".to_string(),
/*trigger_turn*/ true,
),
/*parent_turn_id*/ None,
/*root_turn_id*/ None,
)
.await;
let submission = submit_start_only(
&session,
SubmittedTurnInput::ResponseItem(user_message("synthetic idle input")),
)
.await;
assert_eq!(
TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::PendingTriggerTurn,
},
submission
);
assert!(session.active_turn.lock().await.is_none());
assert!(session.input_queue.has_trigger_turn_mailbox_items().await);
}
#[tokio::test]
async fn steer_only_requires_active_turn() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
let submission = submit_steer_only(
&session,
vec![UserInput::Text {
text: "steer".to_string(),
text_elements: Vec::new(),
}],
"missing-turn-id",
)
.await;
assert_eq!(
TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::NoActiveTurn,
},
submission
);
}
#[tokio::test]
async fn steer_only_enforces_expected_turn_id() {
let (session, turn_context, _rx) = make_session_and_context_with_rx().await;
session
.spawn_task(
Arc::clone(&turn_context),
vec![TurnInput::UserInput {
content: vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
client_id: None,
}],
NeverEndingTask {
kind: TaskKind::Regular,
listen_to_cancellation_token: false,
},
)
.await;
let submission = submit_steer_only(
&session,
vec![UserInput::Text {
text: "steer".to_string(),
text_elements: Vec::new(),
}],
"different-turn-id",
)
.await;
assert_eq!(
TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::ExpectedTurnMismatch {
expected: "different-turn-id".to_string(),
actual: turn_context.sub_id.clone(),
},
},
submission
);
}
#[tokio::test]
async fn rejects_non_regular_turns() {
for (task_kind, turn_kind) in [
(TaskKind::Review, NonSteerableTurnKind::Review),
(TaskKind::Compact, NonSteerableTurnKind::Compact),
] {
let (session, incoming_turn_context, _rx) = make_session_and_context_with_rx().await;
incoming_turn_context
.turn_metadata_state
.set_root_turn_id("incoming-root".to_string());
let turn_context = session
.new_default_turn_with_sub_id("turn".to_string())
.await;
turn_context
.turn_metadata_state
.set_root_turn_id("active-root".to_string());
session
.spawn_task(
Arc::clone(&turn_context),
vec![TurnInput::UserInput {
content: vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
client_id: None,
}],
NeverEndingTask {
kind: task_kind,
listen_to_cancellation_token: true,
},
)
.await;
let steer_input = vec![UserInput::Text {
text: "steer".to_string(),
text_elements: Vec::new(),
}];
let steer_submission = submit_steer_only(&session, steer_input.clone(), "turn").await;
assert_eq!(
TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::ActiveTurnNotSteerable { turn_kind },
},
steer_submission
);
let start_or_steer_submission = handle(
&session,
TurnInputRequest::user_input(steer_input),
TurnInputMode::StartOrSteer,
"test-submission".to_string(),
)
.await
.expect("start-or-steer submission should be valid");
assert_eq!(
TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::ActiveTurnNotSteerable { turn_kind },
},
start_or_steer_submission
);
assert_eq!(
turn_context.turn_metadata_state.root_turn_id().as_deref(),
Some("active-root")
);
session.abort_all_tasks(TurnAbortReason::Interrupted).await;
}
}
-13
View File
@@ -115,19 +115,6 @@ pub(crate) type ThreadIdGenerator = Arc<dyn Fn() -> ThreadId + Send + Sync>;
fn capture_test_op(op: &Op) -> Option<Op> { fn capture_test_op(op: &Op) -> Option<Op> {
match op { match op {
Op::Interrupt => Some(Op::Interrupt), Op::Interrupt => Some(Op::Interrupt),
Op::UserInput {
items,
final_output_json_schema,
responsesapi_client_metadata,
additional_context,
thread_settings,
} => Some(Op::UserInput {
items: items.clone(),
final_output_json_schema: final_output_json_schema.clone(),
responsesapi_client_metadata: responsesapi_client_metadata.clone(),
additional_context: additional_context.clone(),
thread_settings: thread_settings.clone(),
}),
Op::InterAgentCommunication { communication } => Some(Op::InterAgentCommunication { Op::InterAgentCommunication { communication } => Some(Op::InterAgentCommunication {
communication: communication.clone(), communication: communication.clone(),
}), }),
@@ -34,6 +34,7 @@ use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::ShellEnvironmentPolicy; use codex_protocol::config_types::ShellEnvironmentPolicy;
use codex_protocol::items::TurnItem;
use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::mcp::ClientMcpExtensions;
use codex_protocol::models::BaseInstructions; use codex_protocol::models::BaseInstructions;
use codex_protocol::models::BaseInstructionsProvenance; use codex_protocol::models::BaseInstructionsProvenance;
@@ -52,6 +53,7 @@ use codex_protocol::protocol::FileSystemPath;
use codex_protocol::protocol::FileSystemSandboxEntry; use codex_protocol::protocol::FileSystemSandboxEntry;
use codex_protocol::protocol::FileSystemSandboxPolicy; use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::NetworkSandboxPolicy; use codex_protocol::protocol::NetworkSandboxPolicy;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
@@ -104,6 +106,27 @@ fn parse_agent_id(id: &str) -> ThreadId {
ThreadId::from_string(id).expect("agent id should be valid") ThreadId::from_string(id).expect("agent id should be valid")
} }
async fn wait_for_recorded_user_input(thread: &crate::CodexThread, expected: &[UserInput]) {
timeout(Duration::from_secs(5), async {
loop {
let event = thread
.next_event()
.await
.expect("event stream should stay open");
if let EventMsg::ItemCompleted(ItemCompletedEvent {
item: TurnItem::UserMessage(item),
..
}) = event.msg
{
assert_eq!(item.content, expected);
return;
}
}
})
.await
.expect("timed out waiting for recorded user input");
}
fn thread_manager() -> ThreadManager { fn thread_manager() -> ThreadManager {
ThreadManager::with_models_provider_for_tests( ThreadManager::with_models_provider_for_tests(
CodexAuth::from_api_key("dummy"), CodexAuth::from_api_key("dummy"),
@@ -2582,9 +2605,16 @@ async fn send_input_interrupts_before_prompt() {
.iter() .iter()
.filter_map(|(id, op)| (*id == agent_id).then_some(op)) .filter_map(|(id, op)| (*id == agent_id).then_some(op))
.collect(); .collect();
assert_eq!(ops_for_agent.len(), 2); assert_eq!(ops_for_agent.len(), 1);
assert!(matches!(ops_for_agent[0], Op::Interrupt)); assert!(matches!(ops_for_agent[0], Op::Interrupt));
assert!(matches!(ops_for_agent[1], Op::UserInput { .. })); wait_for_recorded_user_input(
thread.thread.as_ref(),
&[UserInput::Text {
text: "hi".to_string(),
text_elements: Vec::new(),
}],
)
.await;
let _ = thread let _ = thread
.thread .thread
@@ -2621,31 +2651,20 @@ async fn send_input_accepts_structured_items() {
.await .await
.expect("send_input should succeed"); .expect("send_input should succeed");
let expected_items = vec![ wait_for_recorded_user_input(
UserInput::Mention { thread.thread.as_ref(),
name: "drive".to_string(), &[
path: "app://google_drive".to_string(), UserInput::Mention {
}, name: "drive".to_string(),
UserInput::Text { path: "app://google_drive".to_string(),
text: "read the folder".to_string(), },
text_elements: Vec::new(), UserInput::Text {
}, text: "read the folder".to_string(),
]; text_elements: Vec::new(),
assert!(manager.captured_ops().iter().any(|(id, op)| { },
*id == agent_id ],
&& matches!( )
op, .await;
Op::UserInput {
items,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context,
thread_settings,
} if items == &expected_items
&& additional_context.is_empty()
&& thread_settings == &Default::default()
)
}));
let _ = thread let _ = thread
.thread .thread
@@ -1,74 +0,0 @@
use codex_protocol::error::Result as CodexResult;
use std::collections::HashMap;
use std::sync::Mutex;
use tokio::sync::oneshot;
/// The turn that accepted a submitted user message.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum UserMessageAdmission {
/// Core installed a new turn for the submitted message.
Started { turn_id: String },
/// Core accepted the submitted message into an already-running turn.
Steered { turn_id: String },
}
#[derive(Default)]
pub(crate) struct PendingUserMessageAdmissions {
pending: Mutex<HashMap<String, oneshot::Sender<CodexResult<UserMessageAdmission>>>>,
}
impl PendingUserMessageAdmissions {
pub(crate) fn register(
&self,
submission_id: String,
) -> (
PendingUserMessageAdmissionGuard<'_>,
oneshot::Receiver<CodexResult<UserMessageAdmission>>,
) {
let (sender, receiver) = oneshot::channel();
self.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(submission_id.clone(), sender);
(
PendingUserMessageAdmissionGuard {
admissions: self,
submission_id,
},
receiver,
)
}
pub(crate) fn complete(
&self,
submission_id: &str,
admission: CodexResult<UserMessageAdmission>,
) {
let sender = self
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(submission_id);
if let Some(sender) = sender {
let _ = sender.send(admission);
}
}
fn remove(&self, submission_id: &str) {
self.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(submission_id);
}
}
pub(crate) struct PendingUserMessageAdmissionGuard<'a> {
admissions: &'a PendingUserMessageAdmissions,
submission_id: String,
}
impl Drop for PendingUserMessageAdmissionGuard<'_> {
fn drop(&mut self) {
self.admissions.remove(&self.submission_id);
}
}
+17 -23
View File
@@ -18,6 +18,7 @@ use codex_core::CodexThread;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::ThreadManager; use codex_core::ThreadManager;
use codex_core::TimeProvider; use codex_core::TimeProvider;
pub use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core::resolve_installation_id; use codex_core::resolve_installation_id;
use codex_core::shell::Shell; use codex_core::shell::Shell;
@@ -37,6 +38,9 @@ use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::built_in_model_providers; use codex_model_provider_info::built_in_model_providers;
use codex_models_manager::bundled_models_response; use codex_models_manager::bundled_models_response;
use codex_models_manager::manager::SharedModelsManager; use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::mcp::ClientMcpExtensions;
use codex_protocol::mcp::OPENAI_FORM_EXTENSION_ID; use codex_protocol::mcp::OPENAI_FORM_EXTENSION_ID;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
@@ -44,7 +48,6 @@ use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion; use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent; use codex_protocol::protocol::SessionConfiguredEvent;
@@ -870,16 +873,10 @@ impl TestCodex {
/// Submits a text turn without changing the current thread settings. /// Submits a text turn without changing the current thread settings.
pub async fn submit_text_turn(&self, prompt: &str) -> Result<()> { pub async fn submit_text_turn(&self, prompt: &str) -> Result<()> {
self.codex self.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: prompt.into(),
text: prompt.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides::default(),
})
.await?; .await?;
wait_for_event(&self.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&self.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1007,31 +1004,28 @@ impl TestCodex {
TurnEnvironmentSelections::new(self.config.cwd.clone(), environments) TurnEnvironmentSelections::new(self.config.cwd.clone(), environments)
}); });
self.codex self.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
environments: turn_environment_selections, environments: turn_environment_selections,
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
service_tier, service_tier,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let turn_id = wait_for_event_match(&self.codex, |event| match event { let turn_id = wait_for_event_match(&self.codex, |event| match event {
@@ -4,13 +4,16 @@ use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use codex_exec_server::REMOTE_ENVIRONMENT_ID; use codex_exec_server::REMOTE_ENVIRONMENT_ID;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandStatus; use codex_protocol::protocol::ExecCommandStatus;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
@@ -21,6 +24,7 @@ use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server; use core_test_support::responses::start_mock_server;
use core_test_support::test_codex::test_codex; use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields; use core_test_support::test_codex::turn_permission_fields;
use core_test_support::test_codex::TurnInputRequest;
use core_test_support::wait_for_event; use core_test_support::wait_for_event;
use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -116,30 +120,24 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
); );
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text {
text: "run the Windows smoke command".to_string(), text: "run the Windows smoke command".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }]).with_thread_settings(ThreadSettingsOverrides {
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(environments), environments: Some(environments),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }))
})
.await?; .await?;
let mut begin = None; let mut begin = None;
+21 -50
View File
@@ -1,4 +1,5 @@
use assert_matches::assert_matches; use assert_matches::assert_matches;
use codex_core::TurnInputRequest;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -45,16 +46,10 @@ async fn interrupt_long_running_tool_emits_turn_aborted() {
// Kick off a turn that triggers the function call. // Kick off a turn that triggers the function call.
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "start sleep".into(),
text: "start sleep".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -102,16 +97,10 @@ async fn interrupt_tool_records_history_entries() {
let codex = Arc::clone(&fixture.codex); let codex = Arc::clone(&fixture.codex);
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "start history recording".into(),
text: "start history recording".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -123,16 +112,10 @@ async fn interrupt_tool_records_history_entries() {
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnAborted(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnAborted(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "follow up".into(),
text: "follow up".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -206,16 +189,10 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
let codex = Arc::clone(&fixture.codex); let codex = Arc::clone(&fixture.codex);
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "start interrupt marker".into(),
text: "start interrupt marker".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -227,16 +204,10 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnAborted(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnAborted(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "follow up".into(),
text: "follow up".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
+48 -78
View File
@@ -1,10 +1,10 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_protocol::items::TurnItem; use codex_protocol::items::TurnItem;
use codex_protocol::protocol::AdditionalContextEntry; use codex_protocol::protocol::AdditionalContextEntry;
use codex_protocol::protocol::AdditionalContextKind; use codex_protocol::protocol::AdditionalContextKind;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::context_snapshot; use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions; use core_test_support::context_snapshot::ContextSnapshotOptions;
@@ -36,14 +36,12 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "inspect the active tab".to_string(), text: "inspect the active tab".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_additional_context(BTreeMap::from([
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
( (
"browser_info".to_string(), "browser_info".to_string(),
AdditionalContextEntry { AdditionalContextEntry {
@@ -58,9 +56,8 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re
kind: AdditionalContextKind::Application, kind: AdditionalContextKind::Application,
}, },
), ),
]), ])),
thread_settings: Default::default(), )
})
.await?; .await?;
let user_item = wait_for_event_match(&test.codex, |event| match event { let user_item = wait_for_event_match(&test.codex, |event| match event {
@@ -134,13 +131,7 @@ async fn external_context_like_user_text_remains_a_user_message_item() -> Result
}; };
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![user_input.clone()]))
items: vec![user_input.clone()],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: BTreeMap::new(),
thread_settings: Default::default(),
})
.await?; .await?;
let user_item = wait_for_event_match(&test.codex, |event| match event { let user_item = wait_for_event_match(&test.codex, |event| match event {
@@ -179,14 +170,12 @@ async fn additional_context_trust_controls_message_role() -> Result<()> {
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "inspect context".to_string(), text: "inspect context".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_additional_context(BTreeMap::from([
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
( (
"browser_info".to_string(), "browser_info".to_string(),
AdditionalContextEntry { AdditionalContextEntry {
@@ -201,9 +190,8 @@ async fn additional_context_trust_controls_message_role() -> Result<()> {
kind: AdditionalContextKind::Application, kind: AdditionalContextKind::Application,
}, },
), ),
]), ])),
thread_settings: Default::default(), )
})
.await?; .await?;
wait_for_event_match(&test.codex, |event| { wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(()) matches!(event, EventMsg::TurnComplete(_)).then_some(())
@@ -259,16 +247,13 @@ async fn additional_context_is_deduplicated_between_turns_while_retained() -> Re
)]); )]);
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "first turn".to_string(), text: "first turn".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_additional_context(additional_context.clone()),
responsesapi_client_metadata: None, )
additional_context: additional_context.clone(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event_match(&test.codex, |event| { wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(()) matches!(event, EventMsg::TurnComplete(_)).then_some(())
@@ -276,16 +261,13 @@ async fn additional_context_is_deduplicated_between_turns_while_retained() -> Re
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "second turn".to_string(), text: "second turn".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_additional_context(additional_context),
responsesapi_client_metadata: None, )
additional_context,
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event_match(&test.codex, |event| { wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(()) matches!(event, EventMsg::TurnComplete(_)).then_some(())
@@ -337,14 +319,12 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<(
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "first turn".to_string(), text: "first turn".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_additional_context(BTreeMap::from([
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
( (
"automation_info".to_string(), "automation_info".to_string(),
AdditionalContextEntry { AdditionalContextEntry {
@@ -359,9 +339,8 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<(
kind: AdditionalContextKind::Untrusted, kind: AdditionalContextKind::Untrusted,
}, },
), ),
]), ])),
thread_settings: Default::default(), )
})
.await?; .await?;
wait_for_event_match(&test.codex, |event| { wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(()) matches!(event, EventMsg::TurnComplete(_)).then_some(())
@@ -369,14 +348,12 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<(
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "second turn".to_string(), text: "second turn".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_additional_context(BTreeMap::from([
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
( (
"automation_info".to_string(), "automation_info".to_string(),
AdditionalContextEntry { AdditionalContextEntry {
@@ -391,9 +368,8 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<(
kind: AdditionalContextKind::Untrusted, kind: AdditionalContextKind::Untrusted,
}, },
), ),
]), ])),
thread_settings: Default::default(), )
})
.await?; .await?;
wait_for_event_match(&test.codex, |event| { wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(()) matches!(event, EventMsg::TurnComplete(_)).then_some(())
@@ -401,14 +377,12 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<(
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "third turn".to_string(), text: "third turn".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_additional_context(BTreeMap::from([
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
( (
"automation_info".to_string(), "automation_info".to_string(),
AdditionalContextEntry { AdditionalContextEntry {
@@ -430,9 +404,8 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<(
kind: AdditionalContextKind::Untrusted, kind: AdditionalContextKind::Untrusted,
}, },
), ),
]), ])),
thread_settings: Default::default(), )
})
.await?; .await?;
wait_for_event_match(&test.codex, |event| { wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(()) matches!(event, EventMsg::TurnComplete(_)).then_some(())
@@ -497,14 +470,12 @@ async fn additional_context_values_are_truncated_before_model_input() -> Result<
format!("<automation_info>{long_automation_value}</automation_info>"); format!("<automation_info>{long_automation_value}</automation_info>");
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "summarize context".to_string(), text: "summarize context".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_additional_context(BTreeMap::from([
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
( (
"automation_info".to_string(), "automation_info".to_string(),
AdditionalContextEntry { AdditionalContextEntry {
@@ -519,9 +490,8 @@ async fn additional_context_values_are_truncated_before_model_input() -> Result<
kind: AdditionalContextKind::Untrusted, kind: AdditionalContextKind::Untrusted,
}, },
), ),
]), ])),
thread_settings: Default::default(), )
})
.await?; .await?;
wait_for_event_match(&test.codex, |event| { wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(()) matches!(event, EventMsg::TurnComplete(_)).then_some(())
+7 -10
View File
@@ -1,9 +1,9 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ToolMode; use codex_protocol::openai_models::ToolMode;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::WebSocketConnectionConfig; use core_test_support::responses::WebSocketConnectionConfig;
@@ -48,19 +48,16 @@ async fn websocket_model_switch_to_responses_lite_omits_top_level_tools() -> Res
test.submit_turn("non-lite turn").await?; test.submit_turn("non-lite turn").await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "lite turn".into(), text: "lite turn".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
model: Some("gpt-5.4".to_string()), model: Some("gpt-5.4".to_string()),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+9 -20
View File
@@ -2,6 +2,7 @@ use anyhow::Result;
use anyhow::anyhow; use anyhow::anyhow;
use codex_core::ForkSnapshot; use codex_core::ForkSnapshot;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::REMOTE_ENVIRONMENT_ID; use codex_exec_server::REMOTE_ENVIRONMENT_ID;
@@ -149,16 +150,10 @@ fn assert_single_instruction_fragment(request: &responses::ResponsesRequest, exp
async fn submit_thread_turn(thread: &Arc<codex_core::CodexThread>, prompt: &str) -> Result<()> { async fn submit_thread_turn(thread: &Arc<codex_core::CodexThread>, prompt: &str) -> Result<()> {
thread thread
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: prompt.to_string(),
text: prompt.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await;
Ok(()) Ok(())
@@ -496,16 +491,10 @@ async fn loads_user_instructions_without_a_primary_environment() -> Result<()> {
no_environment_thread no_environment_thread
.thread .thread
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "inspect global instructions without an environment".to_string(),
text: "inspect global instructions without an environment".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&no_environment_thread.thread, |event| { wait_for_event(&no_environment_thread.thread, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+23 -25
View File
@@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use base64::Engine; use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_core::TurnInputRequest;
use core_test_support::responses::ev_apply_patch_custom_tool_call; use core_test_support::responses::ev_apply_patch_custom_tool_call;
use core_test_support::responses::ev_apply_patch_shell_command_call_via_heredoc; use core_test_support::responses::ev_apply_patch_shell_command_call_via_heredoc;
use core_test_support::responses::ev_shell_command_call; use core_test_support::responses::ev_shell_command_call;
@@ -19,6 +20,9 @@ use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::REMOTE_ENVIRONMENT_ID; use codex_exec_server::REMOTE_ENVIRONMENT_ID;
use codex_exec_server::RemoveOptions; use codex_exec_server::RemoveOptions;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemPath;
@@ -28,8 +32,8 @@ use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -99,29 +103,26 @@ async fn submit_without_wait_with_turn_permissions(
let test = harness.test(); let test = harness.test();
let session_model = test.session_configured.model.clone(); let session_model = test.session_configured.model.clone();
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(()) Ok(())
} }
@@ -1888,15 +1889,12 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
}, },
]; ];
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "apply matching patches to local and remote environments".into(), text: "apply matching patches to local and remote environments".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new( environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new(
test.config.cwd.clone(), test.config.cwd.clone(),
environments, environments,
@@ -1904,17 +1902,17 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let mut last_diff = None; let mut last_diff = None;
+50 -60
View File
@@ -4,6 +4,7 @@ use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use codex_config::types::ApprovalsReviewer; use codex_config::types::ApprovalsReviewer;
use codex_core::CodexThread; use codex_core::CodexThread;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_core::config::ThreadStoreConfig; use codex_core::config::ThreadStoreConfig;
use codex_core::sandboxing::SandboxPermissions; use codex_core::sandboxing::SandboxPermissions;
@@ -12,6 +13,9 @@ use codex_models_manager::bundled_models_response;
use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::approvals::NetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction; use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemAccessMode;
@@ -28,6 +32,7 @@ use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::managed_network_requirements_loader; use core_test_support::managed_network_requirements_loader;
use core_test_support::responses::ev_apply_patch_custom_tool_call; use core_test_support::responses::ev_apply_patch_custom_tool_call;
@@ -658,30 +663,27 @@ async fn submit_turn(
let session_model = test.session_configured.model.clone(); let session_model = test.session_configured.model.clone();
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::User), approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(()) Ok(())
@@ -695,29 +697,26 @@ async fn submit_turn_preserving_active_permission_profile(
let session_model = test.session_configured.model.clone(); let session_model = test.session_configured.model.clone();
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::User), approvals_reviewer: Some(ApprovalsReviewer::User),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(()) Ok(())
@@ -2699,31 +2698,28 @@ async fn env_zsh_script_spawned_by_python_can_request_escalation_under_zsh_fork(
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.cwd.path()); turn_permission_fields(permission_profile, test.cwd.path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run nested env zsh script through python".into(), text: "run nested env zsh script through python".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::User), approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let approval_event = wait_for_event_with_timeout( let approval_event = wait_for_event_with_timeout(
@@ -2843,31 +2839,28 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.cwd.path()); turn_permission_fields(permission_profile, test.cwd.path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run allowed touch under zsh fork".into(), text: "run allowed touch under zsh fork".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::User), approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_completion_without_approval(&test).await; wait_for_completion_without_approval(&test).await;
@@ -3760,31 +3753,28 @@ allow_local_binding = true
turn_permission_fields(permission_profile, test.config.cwd.as_path()); turn_permission_fields(permission_profile, test.config.cwd.as_path());
let session_model = test.session_configured.model.clone(); let session_model = test.session_configured.model.clone();
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "deny-read network retry".into(), text: "deny-read network retry".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::User), approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy: Some(turn_sandbox_policy), sandbox_policy: Some(turn_sandbox_policy),
permission_profile: turn_permission_profile, permission_profile: turn_permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
+5 -10
View File
@@ -2,6 +2,7 @@ use anyhow::Result;
use base64::Engine; use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem;
use codex_protocol::dynamic_tools::DynamicToolFunctionSpec; use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
use codex_protocol::dynamic_tools::DynamicToolNamespaceSpec; use codex_protocol::dynamic_tools::DynamicToolNamespaceSpec;
@@ -105,16 +106,10 @@ async fn dynamic_tool_audio_exceeding_the_output_budget_is_omitted() -> Result<(
test.session_configured = new_thread.session_configured; test.session_configured = new_thread.session_configured;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Return a recording".to_string(),
text: "Return a recording".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let EventMsg::DynamicToolCallRequest(request) = wait_for_event(&test.codex, |event| { let EventMsg::DynamicToolCallRequest(request) = wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::DynamicToolCallRequest(_)) matches!(event, EventMsg::DynamicToolCallRequest(_))
+9 -10
View File
@@ -1,3 +1,4 @@
use codex_core::TurnInputRequest;
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::Result;
@@ -19,6 +20,7 @@ use codex_protocol::openai_models::default_input_modalities;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -165,7 +167,7 @@ async fn remote_model_override_uses_catalog_model_for_strict_auto_review() -> Re
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&codex, &codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some(model.to_string()), model: Some(model.to_string()),
..Default::default() ..Default::default()
}, },
@@ -176,22 +178,19 @@ async fn remote_model_override_uses_catalog_model_for_strict_auto_review() -> Re
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::read_only(), cwd_path.as_path()); turn_permission_fields(PermissionProfile::read_only(), cwd_path.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run the Guardian model override check".into(), text: "run the Guardian model override check".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd_path)), environments: Some(local_selections(cwd_path)),
approval_policy: Some(AskForApproval::OnRequest), approval_policy: Some(AskForApproval::OnRequest),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let permissions_request = wait_for_event(&codex, |event| { let permissions_request = wait_for_event(&codex, |event| {
@@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_models_manager::manager::RefreshStrategy; use codex_models_manager::manager::RefreshStrategy;
@@ -9,7 +10,6 @@ use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::PermissionMessages; use codex_protocol::openai_models::PermissionMessages;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
@@ -82,16 +82,10 @@ async fn catalog_permission_message_loaded_from_remote_models_is_sent() -> Resul
) )
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".to_string(),
text: "hello".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+165 -391
View File
@@ -5,6 +5,7 @@ use codex_core::Prompt;
use codex_core::ResponseEvent; use codex_core::ResponseEvent;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::ThreadManager; use codex_core::ThreadManager;
use codex_core::TurnInputRequest;
use codex_core::X_CODEX_ROUTING_HINT_HEADER; use codex_core::X_CODEX_ROUTING_HINT_HEADER;
use codex_core::resolve_installation_id; use codex_core::resolve_installation_id;
use codex_core::thread_store_from_config; use codex_core::thread_store_from_config;
@@ -53,6 +54,7 @@ use codex_protocol::protocol::Op;
use codex_protocol::protocol::SessionMeta; use codex_protocol::protocol::SessionMeta;
use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::TestCodexResponsesRequestKind; use core_test_support::TestCodexResponsesRequestKind;
use core_test_support::apps_test_server::AppsTestServer; use core_test_support::apps_test_server::AppsTestServer;
@@ -318,16 +320,10 @@ async fn non_openai_responses_requests_include_item_ids_without_passthrough_meta
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -379,15 +375,9 @@ async fn sends_audio_urls_to_responses() {
let audio_url = "data:audio/wav;base64,AAAA"; let audio_url = "data:audio/wav;base64,AAAA";
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Audio {
items: vec![UserInput::Audio { audio_url: audio_url.to_string(),
audio_url: audio_url.to_string(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -430,15 +420,9 @@ async fn sends_local_audio_to_responses() -> anyhow::Result<()> {
std::fs::write(&audio_path, b"audio")?; std::fs::write(&audio_path, b"audio")?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::LocalAudio {
items: vec![UserInput::LocalAudio { path: audio_path.clone(),
path: audio_path.clone(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -953,16 +937,10 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
// 2) Submit new input; the request body must include the prior items, then initial context, then new user input. // 2) Submit new input; the request body must include the prior items, then initial context, then new user input.
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1347,16 +1325,10 @@ async fn includes_session_id_thread_id_and_model_headers_in_request() {
let expected_thread_id = test.session_configured.thread_id; let expected_thread_id = test.session_configured.thread_id;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1613,16 +1585,10 @@ async fn includes_base_instructions_override_in_request() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1670,16 +1636,10 @@ async fn chatgpt_auth_sends_correct_request() {
let expected_thread_id = test.session_configured.thread_id; let expected_thread_id = test.session_configured.thread_id;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1809,16 +1769,10 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
.expect("create new conversation"); .expect("create new conversation");
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1848,16 +1802,10 @@ async fn includes_user_instructions_message_in_request() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1942,16 +1890,10 @@ async fn includes_apps_guidance_as_developer_message_for_chatgpt_auth() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2005,16 +1947,10 @@ async fn omits_apps_guidance_for_api_key_auth_even_when_feature_enabled() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2064,16 +2000,10 @@ async fn omits_apps_guidance_when_configured_off() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2141,16 +2071,10 @@ async fn omits_apps_guidance_when_orchestrator_mcp_is_disabled() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2223,16 +2147,10 @@ async fn omits_environment_context_when_configured_off() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2265,16 +2183,10 @@ async fn includes_configured_max_effort_in_request() -> anyhow::Result<()> {
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2307,16 +2219,10 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> {
let TestCodex { codex, .. } = test_codex().with_model("gpt-5.4").build(&server).await?; let TestCodex { codex, .. } = test_codex().with_model("gpt-5.4").build(&server).await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2350,16 +2256,10 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info
let TestCodex { codex, .. } = test_codex().with_model("gpt-5.4").build(&server).await?; let TestCodex { codex, .. } = test_codex().with_model("gpt-5.4").build(&server).await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2401,15 +2301,12 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
}; };
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello".into(), text: "hello".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(config.cwd.clone())), environments: Some(local_selections(config.cwd.clone())),
approval_policy: Some(config.permissions.approval_policy.value()), approval_policy: Some(config.permissions.approval_policy.value()),
sandbox_policy: Some(config.legacy_sandbox_policy()), sandbox_policy: Some(config.legacy_sandbox_policy()),
@@ -2420,8 +2317,8 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
), ),
collaboration_mode: Some(collaboration_mode), collaboration_mode: Some(collaboration_mode),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -2460,16 +2357,10 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2535,16 +2426,10 @@ async fn model_without_summary_parameter_support_omits_configured_summary() -> a
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -2582,16 +2467,10 @@ async fn sequential_cutoff_is_omitted_for_non_openai_provider() -> anyhow::Resul
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2626,16 +2505,10 @@ async fn responses_lite_sets_all_turns_context_and_disables_parallel_tool_calls(
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -2687,30 +2560,27 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default()
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello".into(), text: "hello".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(config.cwd.clone())), environments: Some(local_selections(config.cwd.clone())),
approval_policy: Some(config.permissions.approval_policy.value()), approval_policy: Some(config.permissions.approval_policy.value()),
sandbox_policy: Some(config.legacy_sandbox_policy()), sandbox_policy: Some(config.legacy_sandbox_policy()),
summary: Some(ReasoningSummary::Concise), summary: Some(ReasoningSummary::Concise),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_configured.model, model: session_configured.model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await .await
.unwrap(); .unwrap();
@@ -2747,16 +2617,10 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> {
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2805,16 +2669,10 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2844,16 +2702,10 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> {
let TestCodex { codex, .. } = test_codex().with_model("gpt-5.4").build(&server).await?; let TestCodex { codex, .. } = test_codex().with_model("gpt-5.4").build(&server).await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2892,16 +2744,10 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2939,16 +2785,10 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> {
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2993,16 +2833,10 @@ async fn includes_developer_instructions_message_in_request() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -3320,16 +3154,10 @@ async fn token_count_includes_rate_limits_snapshot() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -3475,17 +3303,11 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
"rate_limit_reached_type": "workspace_member_usage_limit_reached" "rate_limit_reached_type": "workspace_member_usage_limit_reached"
}); });
let submission_id = codex let submission = codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submission should succeed while emitting usage limit error events"); .expect("submission should succeed while emitting usage limit error events");
@@ -3509,7 +3331,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
}; };
assert!( assert!(
error_event.message.contains("spend cap set by the owner"), error_event.message.contains("spend cap set by the owner"),
"unexpected error message for submission {submission_id}: {}", "unexpected error message for submission {submission:?}: {}",
error_event.message error_event.message
); );
@@ -3553,31 +3375,19 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "seed turn".into(),
text: "seed turn".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "trigger context window".into(),
text: "trigger context window".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let token_event = wait_for_event(&codex, |event| { let token_event = wait_for_event(&codex, |event| {
@@ -3653,16 +3463,10 @@ async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Res
.build(&server) .build(&server)
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "trigger incomplete".into(),
text: "trigger incomplete".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let error_event = wait_for_event(&codex, |ev| matches!(ev, EventMsg::Error(_))).await; let error_event = wait_for_event(&codex, |ev| matches!(ev, EventMsg::Error(_))).await;
@@ -3765,16 +3569,10 @@ async fn azure_overrides_assign_properties_used_for_responses_url() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -3855,16 +3653,10 @@ async fn env_var_overrides_loaded_auth() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -3921,48 +3713,30 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
// Turn 1: user sends U1; wait for completion. // Turn 1: user sends U1; wait for completion.
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "U1".into(),
text: "U1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
// Turn 2: user sends U2; wait for completion. // Turn 2: user sends U2; wait for completion.
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "U2".into(),
text: "U2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
// Turn 3: user sends U3; wait for completion. // Turn 3: user sends U3; wait for completion.
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "U3".into(),
text: "U3".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+13 -25
View File
@@ -6,6 +6,7 @@ use codex_core::ModelClient;
use codex_core::ModelClientSession; use codex_core::ModelClientSession;
use codex_core::Prompt; use codex_core::Prompt;
use codex_core::ResponseEvent; use codex_core::ResponseEvent;
use codex_core::TurnInputRequest;
use codex_core::X_CODEX_ROUTING_HINT_HEADER; use codex_core::X_CODEX_ROUTING_HINT_HEADER;
use codex_core::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER; use codex_core::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER;
use codex_core::test_support::with_parent_turn; use codex_core::test_support::with_parent_turn;
@@ -33,7 +34,6 @@ use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelServiceTier; use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::W3cTraceContext; use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -1609,18 +1609,12 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() {
.await .await
.expect("build websocket codex"); .expect("build websocket codex");
let submission_id = test let submission = test
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submission should succeed while emitting usage limit error events"); .expect("submission should succeed while emitting usage limit error events");
@@ -1667,7 +1661,7 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() {
}; };
assert!( assert!(
error_event.message.contains("spend cap set by the owner"), error_event.message.contains("spend cap set by the owner"),
"unexpected error message for submission {submission_id}: {}", "unexpected error message for submission {submission:?}: {}",
error_event.message error_event.message
); );
@@ -1704,18 +1698,12 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() {
.await .await
.expect("build websocket codex"); .expect("build websocket codex");
let submission_id = test let submission = test
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submission should succeed while emitting invalid request events"); .expect("submission should succeed while emitting invalid request events");
@@ -1728,7 +1716,7 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() {
.message .message
.to_lowercase() .to_lowercase()
.contains("does not support image inputs"), .contains("does not support image inputs"),
"unexpected error message for submission {submission_id}: {}", "unexpected error message for submission {submission:?}: {}",
error_event.message error_event.message
); );
+35 -54
View File
@@ -6,6 +6,7 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_config::types::McpServerConfig; use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig; use codex_config::types::McpServerTransportConfig;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core::config::CurrentTimeReminderConfig; use codex_core::config::CurrentTimeReminderConfig;
use codex_extension_api::ExtensionData; use codex_extension_api::ExtensionData;
@@ -20,6 +21,9 @@ use codex_features::CurrentTimeSource;
use codex_features::Feature; use codex_features::Feature;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_models_manager::bundled_models_response; use codex_models_manager::bundled_models_response;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchMode;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem;
use codex_protocol::dynamic_tools::DynamicToolFunctionSpec; use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
@@ -33,6 +37,7 @@ use codex_protocol::openai_models::ToolMode;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_tools::FreeformTool; use codex_tools::FreeformTool;
use codex_tools::FreeformToolFormat; use codex_tools::FreeformToolFormat;
@@ -283,16 +288,10 @@ async fn run_unavailable_code_mode_turn(
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "list available tools".to_string(),
text: "list available tools".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let mut warnings = Vec::new(); let mut warnings = Vec::new();
@@ -2141,16 +2140,10 @@ async fn code_mode_wait_timeout_reconnects_on_next_exec() -> Result<()> {
.completion; .completion;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "wait for the stalled cell".to_string(),
text: "wait for the stalled cell".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event_match(&test.codex, |event| match event { wait_for_event_match(&test.codex, |event| match event {
EventMsg::RawResponseItem(raw) => match &raw.item { EventMsg::RawResponseItem(raw) => match &raw.item {
@@ -3387,16 +3380,10 @@ async fn code_mode_interrupt_terminates_active_cells_and_nested_tools() -> Resul
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "start a long-running nested tool".to_string(),
text: "start a long-running nested tool".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let active_cell_id = tokio::time::timeout(Duration::from_secs(10), started_rx).await??; let active_cell_id = tokio::time::timeout(Duration::from_secs(10), started_rx).await??;
@@ -5006,15 +4993,12 @@ text(JSON.stringify({
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "inspect and call normalized dynamic tools".to_string(), text: "inspect and call normalized dynamic tools".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new( environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new(
cwd, cwd,
Vec::new(), Vec::new(),
@@ -5022,17 +5006,17 @@ text(JSON.stringify({
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let turn_id = wait_for_event_match(&test.codex, |event| match event { let turn_id = wait_for_event_match(&test.codex, |event| match event {
@@ -5229,15 +5213,12 @@ text(
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "use exec to inspect and call hidden tools".into(), text: "use exec to inspect and call hidden tools".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new( environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new(
cwd, cwd,
Vec::new(), Vec::new(),
@@ -5245,17 +5226,17 @@ text(
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let turn_id = wait_for_event_match(&test.codex, |event| match event { let turn_id = wait_for_event_match(&test.codex, |event| match event {
@@ -3,13 +3,18 @@
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -111,29 +116,26 @@ async fn submit_turn(test: &TestCodex, permission_profile: PermissionProfile) ->
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.config.cwd.as_path()); turn_permission_fields(permission_profile, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run a code-mode tool that needs user input".into(), text: "run a code-mode tool that needs user input".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::OnRequest), approval_policy: Some(AskForApproval::OnRequest),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(wait_for_event_match(&test.codex, |event| match event { Ok(wait_for_event_match(&test.codex, |event| match event {
+8 -9
View File
@@ -1,3 +1,4 @@
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::CreateDirectoryOptions;
use codex_features::Feature; use codex_features::Feature;
@@ -8,6 +9,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::ReviewTarget; use codex_protocol::protocol::ReviewTarget;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUri;
use core_test_support::PathExt; use core_test_support::PathExt;
@@ -410,20 +412,17 @@ async fn guardian_delegate_rejects_escalation_requests_without_prompting() {
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "Trigger Guardian review of an escalated command".to_string(), text: "Trigger Guardian review of an escalated command".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::OnRequest), approval_policy: Some(AskForApproval::OnRequest),
approvals_reviewer: Some(ApprovalsReviewer::AutoReview), approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
..Default::default() ..Default::default()
}, }),
}) )
.await .await
.expect("submit guardian-reviewed command"); .expect("submit guardian-reviewed command");
@@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_models_manager::model_info::model_info_from_slug; use codex_models_manager::model_info::model_info_from_slug;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ModeKind;
@@ -9,7 +10,7 @@ use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::COLLABORATION_MODE_CLOSE_TAG; use codex_protocol::protocol::COLLABORATION_MODE_CLOSE_TAG;
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG; use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created; use core_test_support::responses::ev_response_created;
@@ -113,16 +114,10 @@ async fn no_collaboration_instructions_by_default() -> Result<()> {
let test = test_codex().build(&server).await?; let test = test_codex().build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -173,7 +168,7 @@ async fn catalog_collaboration_messages_track_mode_changes() -> Result<()> {
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model( collaboration_mode: Some(collab_mode_for_model(
ModeKind::Default, ModeKind::Default,
model_slug, model_slug,
@@ -197,7 +192,7 @@ async fn catalog_collaboration_messages_track_mode_changes() -> Result<()> {
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model( collaboration_mode: Some(collab_mode_for_model(
ModeKind::Plan, ModeKind::Plan,
model_slug, model_slug,
@@ -258,7 +253,7 @@ async fn missing_catalog_and_legacy_collaboration_message_clears_prior_instructi
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model( collaboration_mode: Some(collab_mode_for_model(
ModeKind::Default, ModeKind::Default,
model_slug, model_slug,
@@ -272,7 +267,7 @@ async fn missing_catalog_and_legacy_collaboration_message_clears_prior_instructi
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model( collaboration_mode: Some(collab_mode_for_model(
ModeKind::Plan, ModeKind::Plan,
model_slug, model_slug,
@@ -328,7 +323,7 @@ async fn model_change_appends_new_catalog_collaboration_message() -> Result<()>
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model( collaboration_mode: Some(collab_mode_for_model(
ModeKind::Default, ModeKind::Default,
first_slug, first_slug,
@@ -342,7 +337,7 @@ async fn model_change_appends_new_catalog_collaboration_message() -> Result<()>
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some(second_slug.to_string()), model: Some(second_slug.to_string()),
..Default::default() ..Default::default()
}, },
@@ -380,7 +375,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu
let collaboration_mode = collab_mode_with_instructions(Some(collab_text)); let collaboration_mode = collab_mode_with_instructions(Some(collab_text));
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collaboration_mode), collaboration_mode: Some(collaboration_mode),
..Default::default() ..Default::default()
}, },
@@ -388,16 +383,10 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -425,15 +414,12 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> {
let collaboration_mode = collab_mode_with_instructions(Some(collab_text)); let collaboration_mode = collab_mode_with_instructions(Some(collab_text));
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello".into(), text: "hello".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(test.config.permissions.approval_policy.value()), approval_policy: Some(test.config.permissions.approval_policy.value()),
sandbox_policy: Some(test.config.legacy_sandbox_policy()), sandbox_policy: Some(test.config.legacy_sandbox_policy()),
@@ -444,8 +430,8 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> {
), ),
collaboration_mode: Some(collaboration_mode), collaboration_mode: Some(collaboration_mode),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -475,15 +461,12 @@ async fn collaboration_instructions_omitted_when_disabled() -> Result<()> {
let collaboration_mode = collab_mode_with_instructions(Some("turn instructions")); let collaboration_mode = collab_mode_with_instructions(Some("turn instructions"));
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello".into(), text: "hello".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(test.config.permissions.approval_policy.value()), approval_policy: Some(test.config.permissions.approval_policy.value()),
sandbox_policy: Some(test.config.legacy_sandbox_policy()), sandbox_policy: Some(test.config.legacy_sandbox_policy()),
@@ -494,8 +477,8 @@ async fn collaboration_instructions_omitted_when_disabled() -> Result<()> {
), ),
collaboration_mode: Some(collaboration_mode), collaboration_mode: Some(collaboration_mode),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -526,7 +509,7 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collaboration_mode), collaboration_mode: Some(collaboration_mode),
..Default::default() ..Default::default()
}, },
@@ -534,16 +517,10 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -574,7 +551,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(base_mode), collaboration_mode: Some(base_mode),
..Default::default() ..Default::default()
}, },
@@ -582,15 +559,12 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello".into(), text: "hello".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(test.config.permissions.approval_policy.value()), approval_policy: Some(test.config.permissions.approval_policy.value()),
sandbox_policy: Some(test.config.legacy_sandbox_policy()), sandbox_policy: Some(test.config.legacy_sandbox_policy()),
@@ -601,8 +575,8 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
), ),
collaboration_mode: Some(turn_mode), collaboration_mode: Some(turn_mode),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -638,7 +612,7 @@ async fn collaboration_mode_update_ignores_instruction_changes_within_same_mode(
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(first_text))), collaboration_mode: Some(collab_mode_with_instructions(Some(first_text))),
..Default::default() ..Default::default()
}, },
@@ -646,22 +620,16 @@ async fn collaboration_mode_update_ignores_instruction_changes_within_same_mode(
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(second_text))), collaboration_mode: Some(collab_mode_with_instructions(Some(second_text))),
..Default::default() ..Default::default()
}, },
@@ -669,16 +637,10 @@ async fn collaboration_mode_update_ignores_instruction_changes_within_same_mode(
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -713,7 +675,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))), collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))),
..Default::default() ..Default::default()
}, },
@@ -721,22 +683,16 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))), collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))),
..Default::default() ..Default::default()
}, },
@@ -744,16 +700,10 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -787,7 +737,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_mode_and_instructions( collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Default, ModeKind::Default,
Some(default_text), Some(default_text),
@@ -798,22 +748,16 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_mode_and_instructions( collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Plan, ModeKind::Plan,
Some(plan_text), Some(plan_text),
@@ -824,16 +768,10 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -868,7 +806,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_mode_and_instructions( collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Default, ModeKind::Default,
Some(collab_text), Some(collab_text),
@@ -879,22 +817,16 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_mode_and_instructions( collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Default, ModeKind::Default,
Some(collab_text), Some(collab_text),
@@ -905,16 +837,10 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -948,7 +874,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
let collab_text = "resume instructions"; let collab_text = "resume instructions";
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&initial.codex, &initial.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))), collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))),
..Default::default() ..Default::default()
}, },
@@ -957,32 +883,20 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
initial initial
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let resumed = builder.restart(&server, &initial).await?; let resumed = builder.restart(&server, &initial).await?;
resumed resumed
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "after resume".into(),
text: "after resume".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1010,7 +924,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> {
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
collaboration_mode: Some(CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { settings: Settings {
@@ -1025,16 +939,10 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> {
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+188 -370
View File
@@ -1,5 +1,6 @@
use anyhow::Result; use anyhow::Result;
use anyhow::anyhow; use anyhow::anyhow;
use codex_core::TurnInputRequest;
use codex_core::compact::SUMMARIZATION_PROMPT; use codex_core::compact::SUMMARIZATION_PROMPT;
use codex_core::compact::SUMMARY_PREFIX; use codex_core::compact::SUMMARY_PREFIX;
use codex_core::config::Config; use codex_core::config::Config;
@@ -11,6 +12,9 @@ use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::built_in_model_providers; use codex_model_provider_info::built_in_model_providers;
use codex_models_manager::bundled_models_response; use codex_models_manager::bundled_models_response;
use codex_protocol::config_types::AutoCompactTokenLimitScope; use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::items::TurnItem; use codex_protocol::items::TurnItem;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
@@ -22,6 +26,7 @@ use codex_protocol::protocol::HookRunStatus;
use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::ItemStartedEvent; use codex_protocol::protocol::ItemStartedEvent;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::WarningEvent; use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
@@ -102,33 +107,32 @@ fn ev_shell_command_call(call_id: &str, command: &str) -> serde_json::Value {
) )
} }
fn disabled_permission_user_turn(text: impl Into<String>, cwd: PathBuf, model: String) -> Op { fn disabled_permission_user_turn(
text: impl Into<String>,
cwd: PathBuf,
model: String,
) -> TurnInputRequest {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
Op::UserInput { TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.into(),
text: text.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }])
}], .with_thread_settings(ThreadSettingsOverrides {
final_output_json_schema: None, environments: Some(local_selections(cwd.abs())),
responsesapi_client_metadata: None, approval_policy: Some(AskForApproval::Never),
additional_context: Default::default(), sandbox_policy: Some(sandbox_policy),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { permission_profile,
environments: Some(local_selections(cwd.abs())), collaboration_mode: Some(CollaborationMode {
approval_policy: Some(AskForApproval::Never), mode: ModeKind::Default,
sandbox_policy: Some(sandbox_policy), settings: Settings {
permission_profile, model,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { reasoning_effort: None,
mode: codex_protocol::config_types::ModeKind::Default, developer_instructions: None,
settings: codex_protocol::config_types::Settings { },
model, }),
reasoning_effort: None, ..Default::default()
developer_instructions: None, })
},
}),
..Default::default()
},
}
} }
fn auto_summary(summary: &str) -> String { fn auto_summary(summary: &str) -> String {
@@ -528,16 +532,10 @@ async fn summarize_context_three_requests_and_instructions() {
// 1) Normal user input – should hit server once. // 1) Normal user input – should hit server once.
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello world".into(),
text: "hello world".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -553,16 +551,10 @@ async fn summarize_context_three_requests_and_instructions() {
// 3) Next user input – third hit; history should include only the summary. // 3) Next user input – third hit; history should include only the summary.
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: THIRD_USER_MSG.into(),
text: THIRD_USER_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -723,16 +715,10 @@ async fn manual_pre_compact_block_decision_does_not_block_compaction() {
let codex = test.codex.clone(); let codex = test.codex.clone();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello before blocked compact".to_string(),
text: "hello before blocked compact".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit first user turn"); .expect("submit first user turn");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -796,16 +782,10 @@ async fn compact_hooks_respect_matchers_and_post_runs_after_compaction() {
let codex = test.codex.clone(); let codex = test.codex.clone();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello before matched compact".to_string(),
text: "hello before matched compact".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit first user turn"); .expect("submit first user turn");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -866,16 +846,10 @@ async fn manual_compact_uses_custom_prompt() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "USER_ONE".to_string(),
text: "USER_ONE".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit first user turn"); .expect("submit first user turn");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1013,16 +987,10 @@ async fn manual_compact_emits_context_compaction_items() {
let codex = builder.build(&server).await.unwrap().codex; let codex = builder.build(&server).await.unwrap().codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "manual compact".into(),
text: "manual compact".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -1179,16 +1147,10 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
// Start the conversation with the user message // Start the conversation with the user message
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: user_message.into(),
text: user_message.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit user input"); .expect("submit user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1654,48 +1616,30 @@ async fn auto_compact_runs_after_token_limit_hit() {
let codex = builder.build(&server).await.unwrap().codex; let codex = builder.build(&server).await.unwrap().codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: FIRST_AUTO_MSG.into(),
text: FIRST_AUTO_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: SECOND_AUTO_MSG.into(),
text: SECOND_AUTO_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: POST_AUTO_USER_MSG.into(),
text: POST_AUTO_USER_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1857,16 +1801,10 @@ async fn auto_compact_emits_context_compaction_items() {
for user in [FIRST_AUTO_MSG, SECOND_AUTO_MSG, POST_AUTO_USER_MSG] { for user in [FIRST_AUTO_MSG, SECOND_AUTO_MSG, POST_AUTO_USER_MSG] {
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: user.into(),
text: user.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1938,46 +1876,28 @@ async fn auto_compact_starts_after_turn_started() {
let codex = builder.build(&server).await.unwrap().codex; let codex = builder.build(&server).await.unwrap().codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: FIRST_AUTO_MSG.into(),
text: FIRST_AUTO_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: SECOND_AUTO_MSG.into(),
text: SECOND_AUTO_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: POST_AUTO_USER_MSG.into(),
text: POST_AUTO_USER_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2079,7 +1999,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
resumed resumed
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
follow_up_user, follow_up_user,
resumed.cwd.path().to_path_buf(), resumed.cwd.path().to_path_buf(),
resumed.session_configured.model.clone(), resumed.session_configured.model.clone(),
@@ -2158,7 +2078,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
let test = builder.build(&server).await.expect("build test codex"); let test = builder.build(&server).await.expect("build test codex");
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before switch", "before switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
previous_model.to_string(), previous_model.to_string(),
@@ -2171,7 +2091,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
.await; .await;
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after switch", "after switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -2260,7 +2180,7 @@ async fn pre_sampling_compact_runs_when_comp_hash_changes() {
let test = builder.build(&server).await.expect("build test codex"); let test = builder.build(&server).await.expect("build test codex");
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before switch", "before switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
previous_model.to_string(), previous_model.to_string(),
@@ -2273,7 +2193,7 @@ async fn pre_sampling_compact_runs_when_comp_hash_changes() {
.await; .await;
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after switch", "after switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -2368,7 +2288,7 @@ async fn pre_sampling_compact_falls_back_from_retired_previous_model_after_renam
initial initial
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before switch", "before switch",
initial.cwd.path().to_path_buf(), initial.cwd.path().to_path_buf(),
retired_model.to_string(), retired_model.to_string(),
@@ -2405,7 +2325,7 @@ async fn pre_sampling_compact_falls_back_from_retired_previous_model_after_renam
resumed resumed
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after switch", "after switch",
resumed.cwd.path().to_path_buf(), resumed.cwd.path().to_path_buf(),
renamed_model.to_string(), renamed_model.to_string(),
@@ -2507,7 +2427,7 @@ async fn pre_sampling_compact_falls_back_when_previous_model_is_not_found() {
initial initial
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before switch", "before switch",
initial.cwd.path().to_path_buf(), initial.cwd.path().to_path_buf(),
retired_model.to_string(), retired_model.to_string(),
@@ -2545,7 +2465,7 @@ async fn pre_sampling_compact_falls_back_when_previous_model_is_not_found() {
resumed resumed
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after switch", "after switch",
resumed.cwd.path().to_path_buf(), resumed.cwd.path().to_path_buf(),
renamed_model.to_string(), renamed_model.to_string(),
@@ -2640,7 +2560,7 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
let test = builder.build(&server).await.expect("build test codex"); let test = builder.build(&server).await.expect("build test codex");
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before switch", "before switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
retired_model.to_string(), retired_model.to_string(),
@@ -2653,7 +2573,7 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
.await; .await;
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after switch", "after switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -2745,7 +2665,7 @@ async fn pre_sampling_legacy_remote_compact_falls_back_after_previous_model_inva
let test = builder.build(&server).await.expect("build test codex"); let test = builder.build(&server).await.expect("build test codex");
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before switch", "before switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
retired_model.to_string(), retired_model.to_string(),
@@ -2758,7 +2678,7 @@ async fn pre_sampling_legacy_remote_compact_falls_back_after_previous_model_inva
.await; .await;
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after switch", "after switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -2835,7 +2755,7 @@ async fn pre_sampling_compact_keeps_unknown_previous_model_for_api_key_auth_and_
let test = builder.build(&server).await.expect("build test codex"); let test = builder.build(&server).await.expect("build test codex");
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before switch", "before switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
previous_model.to_string(), previous_model.to_string(),
@@ -2848,7 +2768,7 @@ async fn pre_sampling_compact_keeps_unknown_previous_model_for_api_key_auth_and_
.await; .await;
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after switch", "after switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -2925,7 +2845,7 @@ async fn pre_sampling_compact_skips_when_either_comp_hash_is_missing() {
let test = builder.build(&server).await.expect("build test codex"); let test = builder.build(&server).await.expect("build test codex");
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before hash", "before hash",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
model_without_hash.to_string(), model_without_hash.to_string(),
@@ -2938,7 +2858,7 @@ async fn pre_sampling_compact_skips_when_either_comp_hash_is_missing() {
.await; .await;
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"hash introduced", "hash introduced",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
model_with_hash.to_string(), model_with_hash.to_string(),
@@ -2951,7 +2871,7 @@ async fn pre_sampling_compact_skips_when_either_comp_hash_is_missing() {
.await; .await;
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"hash removed", "hash removed",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
next_model_without_hash.to_string(), next_model_without_hash.to_string(),
@@ -3034,7 +2954,7 @@ async fn body_after_prefix_model_switch_budget_compacts_with_next_model() {
let test = builder.build(&server).await.expect("build test codex"); let test = builder.build(&server).await.expect("build test codex");
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before switch", "before switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
previous_model.to_string(), previous_model.to_string(),
@@ -3047,7 +2967,7 @@ async fn body_after_prefix_model_switch_budget_compacts_with_next_model() {
.await; .await;
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after switch", "after switch",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -3134,7 +3054,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
initial initial
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before resume", "before resume",
initial.cwd.path().to_path_buf(), initial.cwd.path().to_path_buf(),
previous_model.to_string(), previous_model.to_string(),
@@ -3171,7 +3091,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
resumed resumed
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after resume", "after resume",
resumed.cwd.path().to_path_buf(), resumed.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -3255,7 +3175,7 @@ async fn pre_sampling_compact_recovers_comp_hash_after_resume() {
initial initial
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before resume", "before resume",
initial.cwd.path().to_path_buf(), initial.cwd.path().to_path_buf(),
previous_model.to_string(), previous_model.to_string(),
@@ -3302,7 +3222,7 @@ async fn pre_sampling_compact_recovers_comp_hash_after_resume() {
resumed resumed
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after resume", "after resume",
resumed.cwd.path().to_path_buf(), resumed.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -3382,7 +3302,7 @@ async fn pre_sampling_compact_skips_missing_comp_hash_after_resume() {
initial initial
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"before resume", "before resume",
initial.cwd.path().to_path_buf(), initial.cwd.path().to_path_buf(),
previous_model.to_string(), previous_model.to_string(),
@@ -3427,7 +3347,7 @@ async fn pre_sampling_compact_skips_missing_comp_hash_after_resume() {
resumed resumed
.codex .codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"after resume", "after resume",
resumed.cwd.path().to_path_buf(), resumed.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -3522,46 +3442,28 @@ async fn auto_compact_persists_rollout_entries() {
let session_configured = test.session_configured; let session_configured = test.session_configured;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: FIRST_AUTO_MSG.into(),
text: FIRST_AUTO_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: SECOND_AUTO_MSG.into(),
text: SECOND_AUTO_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: POST_AUTO_USER_MSG.into(),
text: POST_AUTO_USER_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -3636,16 +3538,10 @@ async fn manual_compact_retries_after_context_window_error() {
let codex = builder.build(&server).await.unwrap().codex; let codex = builder.build(&server).await.unwrap().codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "first turn".into(),
text: "first turn".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -3740,16 +3636,10 @@ async fn manual_compact_non_context_failure_retries_then_emits_task_error() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "first turn".into(),
text: "first turn".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit user input"); .expect("submit user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -3835,16 +3725,10 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
let codex = builder.build(&server).await.unwrap().codex; let codex = builder.build(&server).await.unwrap().codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: first_user_message.into(),
text: first_user_message.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -3853,16 +3737,10 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: second_user_message.into(),
text: second_user_message.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -3871,16 +3749,10 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: final_user_message.into(),
text: final_user_message.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -4079,16 +3951,10 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_
let mut auto_compact_lifecycle_events = Vec::new(); let mut auto_compact_lifecycle_events = Vec::new();
for user in [MULTI_AUTO_MSG, follow_up_user, final_user] { for user in [MULTI_AUTO_MSG, follow_up_user, final_user] {
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: user.into(),
text: user.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -4184,16 +4050,10 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() {
let codex = builder.build(&server).await.unwrap().codex; let codex = builder.build(&server).await.unwrap().codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: FUNCTION_CALL_LIMIT_MSG.into(),
text: FUNCTION_CALL_LIMIT_MSG.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -4619,16 +4479,10 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
.enumerate() .enumerate()
{ {
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: user.into(),
text: user.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -4742,16 +4596,10 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
for user in [first_user, second_user, third_user] { for user in [first_user, second_user, third_user] {
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: user.into(),
text: user.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -4804,23 +4652,17 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
for user in ["USER_ONE", "USER_TWO"] { for user in ["USER_ONE", "USER_TWO"] {
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: user.to_string(),
text: user.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit user input"); .expect("submit user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
} }
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&codex, &codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
environments: Some(local_selections( environments: Some(local_selections(
test_path_buf(PRETURN_CONTEXT_DIFF_CWD).abs(), test_path_buf(PRETURN_CONTEXT_DIFF_CWD).abs(),
)), )),
@@ -4832,22 +4674,16 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==" let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="
.to_string(); .to_string();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![
items: vec![ UserInput::Image {
UserInput::Image { image_url: image_url.clone(),
image_url: image_url.clone(), detail: None,
detail: None, },
}, UserInput::Text {
UserInput::Text { text: "USER_THREE".to_string(),
text: "USER_THREE".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), },
}, ]))
],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit user input"); .expect("submit user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -4930,7 +4766,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
.expect("build codex"); .expect("build codex");
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"BEFORE_SWITCH_USER", "BEFORE_SWITCH_USER",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
previous_model.to_string(), previous_model.to_string(),
@@ -4943,7 +4779,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
.await; .await;
test.codex test.codex
.submit(disabled_permission_user_turn( .start_or_steer_turn(disabled_permission_user_turn(
"AFTER_SWITCH_USER", "AFTER_SWITCH_USER",
test.cwd.path().to_path_buf(), test.cwd.path().to_path_buf(),
next_model.to_string(), next_model.to_string(),
@@ -5027,31 +4863,19 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "USER_ONE".to_string(),
text: "USER_ONE".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit first user"); .expect("submit first user");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "USER_TWO".to_string(),
text: "USER_TWO".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit second user"); .expect("submit second user");
let error_message = wait_for_event_match(&codex, |event| match event { let error_message = wait_for_event_match(&codex, |event| match event {
@@ -5115,16 +4939,10 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages()
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "AFTER_MANUAL_EMPTY_COMPACT".to_string(),
text: "AFTER_MANUAL_EMPTY_COMPACT".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit follow-up user input"); .expect("submit follow-up user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
File diff suppressed because it is too large. Load diff
@@ -1,3 +1,4 @@
use codex_core::TurnInputRequest;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@@ -605,13 +606,7 @@ async fn capture_from_requests(
async fn submit_user_input(codex: &codex_core::CodexThread, items: Vec<UserInput>) -> Result<()> { async fn submit_user_input(codex: &codex_core::CodexThread, items: Vec<UserInput>) -> Result<()> {
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(items))
items,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_turn_complete(codex).await; wait_for_turn_complete(codex).await;
Ok(()) Ok(())
@@ -12,6 +12,7 @@ use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use codex_core::CodexThread; use codex_core::CodexThread;
use codex_core::ThreadManager; use codex_core::ThreadManager;
use codex_core::TurnInputRequest;
use codex_core::compact::SUMMARIZATION_PROMPT; use codex_core::compact::SUMMARIZATION_PROMPT;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
@@ -26,6 +27,7 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::WarningEvent; use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::context_snapshot; use core_test_support::context_snapshot;
@@ -636,7 +638,7 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> {
std::fs::create_dir_all(&override_cwd)?; std::fs::create_dir_all(&override_cwd)?;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&conversation, &conversation,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
environments: Some(local_selections(override_cwd.clone())), environments: Some(local_selections(override_cwd.clone())),
collaboration_mode: Some(CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Default, mode: ModeKind::Default,
@@ -859,16 +861,10 @@ async fn start_test_conversation(
async fn user_turn(conversation: &Arc<CodexThread>, text: &str) { async fn user_turn(conversation: &Arc<CodexThread>, text: &str) {
conversation conversation
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.into(),
text: text.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit user turn"); .expect("submit user turn");
wait_for_event(conversation, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(conversation, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -12,6 +12,7 @@ use chrono::Utc;
use codex_core::SleepFuture; use codex_core::SleepFuture;
use codex_core::TimeFuture; use codex_core::TimeFuture;
use codex_core::TimeProvider; use codex_core::TimeProvider;
use codex_core::TurnInputRequest;
use codex_core::config::CurrentTimeReminderConfig; use codex_core::config::CurrentTimeReminderConfig;
use codex_features::CurrentTimeReminderDeliveryMode; use codex_features::CurrentTimeReminderDeliveryMode;
use codex_features::CurrentTimeSource; use codex_features::CurrentTimeSource;
@@ -428,16 +429,10 @@ async fn time_provider_failure_stops_before_inference() -> Result<()> {
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "fail before inference".into(),
text: "fail before inference".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let EventMsg::Error(error) = let EventMsg::Error(error) =
@@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_core::sandboxing::SandboxPermissions; use codex_core::sandboxing::SandboxPermissions;
@@ -260,20 +261,17 @@ async fn cyber_model_user_approval_never_offers_a_reusable_prefix(
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run a command with one-time approval".to_string(), text: "run a command with one-time approval".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::OnRequest), approval_policy: Some(AskForApproval::OnRequest),
approvals_reviewer: Some(ApprovalsReviewer::User), approvals_reviewer: Some(ApprovalsReviewer::User),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let EventMsg::ExecApprovalRequest(approval) = wait_for_event(&test.codex, |event| { let EventMsg::ExecApprovalRequest(approval) = wait_for_event(&test.codex, |event| {
+20 -24
View File
@@ -2,6 +2,7 @@
use anyhow::Result; use anyhow::Result;
use codex_config::test_support::CloudConfigBundleFixture; use codex_config::test_support::CloudConfigBundleFixture;
use codex_core::TurnInputRequest;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ModeKind;
@@ -12,6 +13,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
@@ -53,23 +55,20 @@ async fn submit_user_turn(
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.config.cwd.as_path()); turn_permission_fields(permission_profile, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: collaboration_mode.or({ collaboration_mode: collaboration_mode.or({
Some(codex_protocol::config_types::CollaborationMode { Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
@@ -77,8 +76,8 @@ async fn submit_user_turn(
}) })
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(()) Ok(())
} }
@@ -402,30 +401,27 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run shell command".into(), text: "run shell command".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let EventMsg::ExecCommandEnd(end) = wait_for_event(&test.codex, |event| { let EventMsg::ExecCommandEnd(end) = wait_for_event(&test.codex, |event| {
+14 -12
View File
@@ -2,6 +2,7 @@ use std::sync::Arc;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_extension_api::ExtensionRegistry; use codex_extension_api::ExtensionRegistry;
@@ -10,6 +11,9 @@ use codex_features::Feature;
use codex_image_generation_extension::install as install_image_generation_extension; use codex_image_generation_extension::install as install_image_generation_extension;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
@@ -22,6 +26,7 @@ use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -244,31 +249,28 @@ async fn extension_tool_uses_granted_turn_permissions_without_local_persistence(
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(base_permission_profile, test.config.cwd.as_path()); turn_permission_fields(base_permission_profile, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "request access and edit the image".to_string(), text: "request access and edit the image".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(AskForApproval::OnRequest), approval_policy: Some(AskForApproval::OnRequest),
approvals_reviewer: Some(ApprovalsReviewer::User), approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let event = wait_for_event(&test.codex, |event| { let event = wait_for_event(&test.codex, |event| {
matches!( matches!(
+13 -31
View File
@@ -3,6 +3,7 @@ use std::sync::Arc;
use codex_core::ForkSnapshot; use codex_core::ForkSnapshot;
use codex_core::NewThread; use codex_core::NewThread;
use codex_core::ThreadConfigSnapshot; use codex_core::ThreadConfigSnapshot;
use codex_core::TurnInputRequest;
use codex_core::parse_turn_item; use codex_core::parse_turn_item;
use codex_history::InitialHistory; use codex_history::InitialHistory;
use codex_history::ResumedHistory; use codex_history::ResumedHistory;
@@ -11,7 +12,6 @@ use codex_history::RolloutLine;
use codex_protocol::items::TurnItem; use codex_protocol::items::TurnItem;
use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::mcp::ClientMcpExtensions;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadHistoryMode; use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::ThreadSettingsAppliedEvent; use codex_protocol::protocol::ThreadSettingsAppliedEvent;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -55,16 +55,10 @@ async fn fork_thread_twice_drops_to_first_message() {
// Send three user messages; wait for three completed turns. // Send three user messages; wait for three completed turns.
for text in ["first", "second", "third"] { for text in ["first", "second", "third"] {
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.to_string(),
text: text.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
let _ = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let _ = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -204,16 +198,10 @@ async fn assert_copied_fork_persists_inherited_history(history_mode: ThreadHisto
let thread_manager = test.thread_manager.clone(); let thread_manager = test.thread_manager.clone();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "fork me from stored history".to_string(),
text: "fork me from stored history".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit initial user turn"); .expect("submit initial user turn");
let _ = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let _ = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -281,16 +269,10 @@ async fn assert_copied_fork_persists_inherited_history(history_mode: ThreadHisto
.expect("resume copied paginated fork") .expect("resume copied paginated fork")
.thread; .thread;
resumed resumed
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "continue after cold resume".to_string(),
text: "continue after cold resume".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("start resumed turn"); .expect("start resumed turn");
wait_for_event(&resumed, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&resumed, |event| matches!(event, EventMsg::TurnComplete(_))).await;
+16 -28
View File
@@ -2,6 +2,7 @@ use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use anyhow::ensure; use anyhow::ensure;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
use codex_core::config::Constrained; use codex_core::config::Constrained;
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
@@ -11,7 +12,6 @@ use codex_protocol::config_types::ApprovalsReviewer;
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSource; use codex_protocol::protocol::ThreadSource;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::PathBufExt; use core_test_support::PathBufExt;
@@ -196,13 +196,10 @@ async fn guardian_prewarm_and_review_skip_redundant_git_enrichment() -> Result<(
std::fs::write(repo.path().join("untracked.txt"), "dirty\n")?; std::fs::write(repo.path().join("untracked.txt"), "dirty\n")?;
test.codex test.codex
.submit( .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
vec![UserInput::Text { text: "run a command that requires Guardian review".into(),
text: "run a command that requires Guardian review".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}]
.into(),
)
.await?; .await?;
let (user_turn, guardian_turn) = tokio::time::timeout(Duration::from_secs(5), async { let (user_turn, guardian_turn) = tokio::time::timeout(Duration::from_secs(5), async {
tokio::join!( tokio::join!(
@@ -295,16 +292,10 @@ async fn ephemeral_system_thread_prewarm_skips_and_turn_observes_fresh_state(
std::fs::write(repo.path().join("untracked.txt"), "dirty\n")?; std::fs::write(repo.path().join("untracked.txt"), "dirty\n")?;
system_thread system_thread
.thread .thread
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "generate a thread title".into(),
text: "generate a thread title".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(system_thread.thread.as_ref(), |event| { wait_for_event(system_thread.thread.as_ref(), |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -423,24 +414,21 @@ async fn concurrent_turns_keep_distinct_worktree_and_repository_metadata() -> Re
std::fs::write(repo.path().join("untracked.txt"), "dirty\n")?; std::fs::write(repo.path().join("untracked.txt"), "dirty\n")?;
std::fs::write(other_repo.path().join("untracked.txt"), "dirty\n")?; std::fs::write(other_repo.path().join("untracked.txt"), "dirty\n")?;
let user_turn = |prompt: &str| Op::UserInput { let user_turn = |prompt: &str| {
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.to_string(), text: prompt.to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
}; };
tokio::try_join!( tokio::try_join!(
test.codex.submit(user_turn("inspect the main worktree")), test.codex
.start_or_steer_turn(user_turn("inspect the main worktree")),
worktree_thread worktree_thread
.thread .thread
.submit(user_turn("inspect the linked worktree")), .start_or_steer_turn(user_turn("inspect the linked worktree")),
other_thread other_thread
.thread .thread
.submit(user_turn("inspect the other repository")) .start_or_steer_turn(user_turn("inspect the other repository"))
)?; )?;
tokio::join!( tokio::join!(
wait_for_event(test.codex.as_ref(), |event| matches!( wait_for_event(test.codex.as_ref(), |event| matches!(
+40 -59
View File
@@ -8,6 +8,7 @@ use chrono::Utc;
use codex_core::SleepFuture; use codex_core::SleepFuture;
use codex_core::TimeFuture; use codex_core::TimeFuture;
use codex_core::TimeProvider; use codex_core::TimeProvider;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_core::config::CurrentTimeReminderConfig; use codex_core::config::CurrentTimeReminderConfig;
use codex_core::sandboxing::SandboxPermissions; use codex_core::sandboxing::SandboxPermissions;
@@ -24,6 +25,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::fs_wait; use core_test_support::fs_wait;
@@ -258,13 +260,10 @@ async fn guardian_session_prewarms_and_is_reused_for_first_review(
.expect("guardian thread id"); .expect("guardian thread id");
test.codex test.codex
.submit( .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
vec![UserInput::Text { text: "run a command that requires Guardian review".into(),
text: "run a command that requires Guardian review".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}]
.into(),
)
.await?; .await?;
let guardian_review = tokio::time::timeout( let guardian_review = tokio::time::timeout(
Duration::from_secs(5), Duration::from_secs(5),
@@ -448,22 +447,19 @@ async fn guardian_session_is_reused_for_consecutive_tool_reviews_without_prewarm
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run two commands that require Guardian review".into(), text: "run two commands that require Guardian review".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::AutoReview), approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -637,22 +633,19 @@ async fn interrupted_guardian_tool_review_aborts_without_executing_the_command()
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "interrupt a Guardian-reviewed command".into(), text: "interrupt a Guardian-reviewed command".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::AutoReview), approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
tokio::time::timeout(Duration::from_secs(5), async { tokio::time::timeout(Duration::from_secs(5), async {
@@ -684,13 +677,10 @@ async fn interrupted_guardian_tool_review_aborts_without_executing_the_command()
) )
.await; .await;
test.codex test.codex
.submit( .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
vec![UserInput::Text { text: "verify Guardian cancellation left the next turn clean".into(),
text: "verify Guardian cancellation left the next turn clean".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}]
.into(),
)
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -784,21 +774,18 @@ async fn guardian_denial_rejects_tool_call_with_rationale() -> Result<()> {
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run a command that Guardian should deny".into(), text: "run a command that Guardian should deny".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::AutoReview), approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -898,21 +885,18 @@ async fn cyber_model_guardian_denial_interrupts_turn_immediately() -> Result<()>
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run a command that Guardian should deny for a cyber model".into(), text: "run a command that Guardian should deny for a cyber model".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::AutoReview), approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let warning = wait_for_event(&test.codex, |event| { let warning = wait_for_event(&test.codex, |event| {
@@ -1030,22 +1014,19 @@ printf '%s\n' "${@: -1}" >> "${payload_path}""#,
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run a command that requires Guardian review".into(), text: "run a command that requires Guardian review".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::AutoReview), approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+44 -65
View File
@@ -4,6 +4,7 @@ use std::path::Path;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_core::config::ThreadStoreConfig; use codex_core::config::ThreadStoreConfig;
@@ -25,6 +26,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -1613,21 +1615,20 @@ async fn async_hook_context_is_injected_into_the_active_turn() -> Result<()> {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "observe async context immediately".to_string(), text: "observe async context immediately".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(
responsesapi_client_metadata: None, codex_protocol::protocol::ThreadSettingsOverrides {
additional_context: Default::default(), approval_policy: Some(AskForApproval::Never),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { sandbox_policy: Some(sandbox_policy),
approval_policy: Some(AskForApproval::Never), permission_profile,
sandbox_policy: Some(sandbox_policy), ..Default::default()
permission_profile, },
..Default::default() ),
}, )
})
.await?; .await?;
let finished_path = test let finished_path = test
@@ -1753,16 +1754,10 @@ async fn async_hook_finishing_while_idle_waits_for_the_next_turn() -> Result<()>
let next_prompt = "observe the buffered async context"; let next_prompt = "observe the buffered async context";
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: next_prompt.to_string(),
text: next_prompt.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let mut warning_event = None; let mut warning_event = None;
@@ -2636,16 +2631,10 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu
let test = builder.build_with_streaming_server(&server).await?; let test = builder.build_with_streaming_server(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "initial prompt".to_string(),
text: "initial prompt".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
@@ -2655,16 +2644,10 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu
for text in ["accepted queued prompt", "blocked queued prompt"] { for text in ["accepted queued prompt", "blocked queued prompt"] {
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.to_string(),
text: text.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
} }
@@ -2906,21 +2889,18 @@ async fn permission_request_hook_allow_bypasses_strict_auto_review() -> Result<(
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "request strict review, then run the shell command".into(), text: "request strict review, then run the shell command".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::OnRequest), approval_policy: Some(AskForApproval::OnRequest),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let request = wait_for_event(&test.codex, |event| { let request = wait_for_event(&test.codex, |event| {
@@ -3595,21 +3575,20 @@ Path(r"{hook_finished_path}").write_text("finished", encoding="utf-8")
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run the original command with async pre-tool hooks".to_string(), text: "run the original command with async pre-tool hooks".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(
responsesapi_client_metadata: None, codex_protocol::protocol::ThreadSettingsOverrides {
additional_context: Default::default(), approval_policy: Some(AskForApproval::Never),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { sandbox_policy: Some(sandbox_policy),
approval_policy: Some(AskForApproval::Never), permission_profile,
sandbox_policy: Some(sandbox_policy), ..Default::default()
permission_profile, },
..Default::default() ),
}, )
})
.await?; .await?;
fs_wait::wait_for_path_exists(hook_finished_path, Duration::from_secs(5)) fs_wait::wait_for_path_exists(hook_finished_path, Duration::from_secs(5))
+27 -34
View File
@@ -1,9 +1,13 @@
use anyhow::Context; use anyhow::Context;
use base64::Engine; use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_core::TurnInputRequest;
use codex_features::Feature; use codex_features::Feature;
use codex_history::RolloutItem; use codex_history::RolloutItem;
use codex_history::RolloutLine; use codex_history::RolloutLine;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItem;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::ImageDetail; use codex_protocol::models::ImageDetail;
@@ -13,6 +17,7 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_utils_image::data_url_from_bytes; use codex_utils_image::data_url_from_bytes;
use core_test_support::TempDirExt; use core_test_support::TempDirExt;
@@ -125,8 +130,8 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
turn_permission_fields(PermissionProfile::Disabled, cwd.path()); turn_permission_fields(PermissionProfile::Disabled, cwd.path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![ TurnInputRequest::user_input(vec![
UserInput::LocalImage { UserInput::LocalImage {
path: abs_path.clone(), path: abs_path.clone(),
detail: None, detail: None,
@@ -135,26 +140,23 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
text: "pasted image".to_string(), text: "pasted image".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}, },
], ])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd.abs())), environments: Some(local_selections(cwd.abs())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -224,8 +226,8 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
turn_permission_fields(PermissionProfile::Disabled, cwd.path()); turn_permission_fields(PermissionProfile::Disabled, cwd.path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![ TurnInputRequest::user_input(vec![
UserInput::Image { UserInput::Image {
image_url: image_url.clone(), image_url: image_url.clone(),
detail: None, detail: None,
@@ -234,26 +236,23 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
text: "dropped image".to_string(), text: "dropped image".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}, },
], ])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd.abs())), environments: Some(local_selections(cwd.abs())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -374,16 +373,10 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R
.await; .await;
resumed resumed
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Image {
items: vec![UserInput::Image { image_url: original_image_url.clone(),
image_url: original_image_url.clone(), detail: Some(ImageDetail::High),
detail: Some(ImageDetail::High), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&resumed.codex, |event| { wait_for_event(&resumed.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -1,3 +1,4 @@
use codex_core::TurnInputRequest;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
@@ -22,7 +23,6 @@ use codex_protocol::error::Result as CoreResult;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelVisibility; use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses; use core_test_support::responses;
@@ -184,16 +184,10 @@ async fn run_agent_with_model(models_manager: SharedModelsManager, model_slug: &
) )
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".to_string(),
text: "hello".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
loop { loop {
if matches!( if matches!(
+53 -111
View File
@@ -1,6 +1,7 @@
#![cfg(not(target_os = "windows"))] #![cfg(not(target_os = "windows"))]
use anyhow::Ok; use anyhow::Ok;
use codex_core::TurnInputRequest;
use codex_features::Feature; use codex_features::Feature;
use codex_history::RolloutItem; use codex_history::RolloutItem;
use codex_history::RolloutLine; use codex_history::RolloutLine;
@@ -15,8 +16,8 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::ItemStartedEvent; use codex_protocol::protocol::ItemStartedEvent;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadHistoryMode; use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::ByteRange; use codex_protocol::user_input::ByteRange;
use codex_protocol::user_input::TextElement; use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -48,27 +49,22 @@ fn disabled_plan_turn(
text: &str, text: &str,
_model: String, _model: String,
collaboration_mode: CollaborationMode, collaboration_mode: CollaborationMode,
) -> anyhow::Result<Op> { ) -> anyhow::Result<TurnInputRequest> {
let cwd = std::env::current_dir()?.abs(); let cwd = std::env::current_dir()?.abs();
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
Ok(Op::UserInput { Ok(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.into(),
text: text.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }])
}], .with_thread_settings(ThreadSettingsOverrides {
final_output_json_schema: None, environments: Some(local_selections(cwd)),
responsesapi_client_metadata: None, approval_policy: Some(AskForApproval::Never),
additional_context: Default::default(), sandbox_policy: Some(sandbox_policy),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { permission_profile,
environments: Some(local_selections(cwd)), collaboration_mode: Some(collaboration_mode),
approval_policy: Some(AskForApproval::Never), ..Default::default()
sandbox_policy: Some(sandbox_policy), }))
permission_profile,
collaboration_mode: Some(collaboration_mode),
..Default::default()
},
})
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -92,13 +88,7 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> {
}; };
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![expected_input.clone()]))
items: vec![expected_input.clone()],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let started_item = wait_for_event_match(&codex, |ev| match ev { let started_item = wait_for_event_match(&codex, |ev| match ev {
@@ -148,16 +138,10 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> {
mount_sse_once(&server, first_response).await; mount_sse_once(&server, first_response).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "please summarize results".into(),
text: "please summarize results".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let started = wait_for_event_match(&codex, |ev| match ev { let started = wait_for_event_match(&codex, |ev| match ev {
@@ -209,16 +193,10 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> {
mount_sse_once(&server, first_response).await; mount_sse_once(&server, first_response).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "explain your reasoning".into(),
text: "explain your reasoning".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let started = wait_for_event_match(&codex, |ev| match ev { let started = wait_for_event_match(&codex, |ev| match ev {
@@ -280,16 +258,10 @@ async fn missing_streamed_reasoning_id_is_reused_for_completion() -> anyhow::Res
.await; .await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "explain your reasoning".into(),
text: "explain your reasoning".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let started_id = wait_for_event_match(&codex, |ev| match ev { let started_id = wait_for_event_match(&codex, |ev| match ev {
@@ -339,16 +311,10 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
mount_sse_once(&server, first_response).await; mount_sse_once(&server, first_response).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "find the weather".into(),
text: "find the weather".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let started = wait_for_event_match(&codex, |ev| match ev { let started = wait_for_event_match(&codex, |ev| match ev {
@@ -434,16 +400,10 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> {
mount_sse_once(&server, stream).await; mount_sse_once(&server, stream).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "please stream text".into(),
text: "please stream text".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let (started_turn_id, started_item) = wait_for_event_match(&codex, |ev| match ev { let (started_turn_id, started_item) = wait_for_event_match(&codex, |ev| match ev {
@@ -513,7 +473,7 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result<
}; };
codex codex
.submit(disabled_plan_turn( .start_or_steer_turn(disabled_plan_turn(
"please plan", "please plan",
session_configured.model.clone(), session_configured.model.clone(),
collaboration_mode, collaboration_mode,
@@ -578,7 +538,7 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> {
}; };
codex codex
.submit(disabled_plan_turn( .start_or_steer_turn(disabled_plan_turn(
"please plan", "please plan",
session_configured.model.clone(), session_configured.model.clone(),
collaboration_mode, collaboration_mode,
@@ -675,7 +635,7 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done
}; };
codex codex
.submit(disabled_plan_turn( .start_or_steer_turn(disabled_plan_turn(
"please plan with citations", "please plan with citations",
session_configured.model.clone(), session_configured.model.clone(),
collaboration_mode, collaboration_mode,
@@ -850,7 +810,7 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p
}; };
codex codex
.submit(disabled_plan_turn( .start_or_steer_turn(disabled_plan_turn(
"please plan", "please plan",
session_configured.model.clone(), session_configured.model.clone(),
collaboration_mode, collaboration_mode,
@@ -952,7 +912,7 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> {
}; };
codex codex
.submit(disabled_plan_turn( .start_or_steer_turn(disabled_plan_turn(
"please plan", "please plan",
session_configured.model.clone(), session_configured.model.clone(),
collaboration_mode, collaboration_mode,
@@ -1018,16 +978,10 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> {
mount_sse_once(&server, stream).await; mount_sse_once(&server, stream).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "reason through it".into(),
text: "reason through it".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let reasoning_item = wait_for_event_match(&codex, |ev| match ev { let reasoning_item = wait_for_event_match(&codex, |ev| match ev {
@@ -1095,16 +1049,10 @@ async fn sequential_cutoff_renders_done_summaries_for_active_reasoning_item() ->
.await; .await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "reason through it".into(),
text: "reason through it".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let reasoning_item = wait_for_event_match(&codex, |ev| match ev { let reasoning_item = wait_for_event_match(&codex, |ev| match ev {
@@ -1165,16 +1113,10 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> {
mount_sse_once(&server, stream).await; mount_sse_once(&server, stream).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "show raw reasoning".into(),
text: "show raw reasoning".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let reasoning_item = wait_for_event_match(&codex, |ev| match ev { let reasoning_item = wait_for_event_match(&codex, |ev| match ev {
+19 -13
View File
@@ -1,9 +1,14 @@
#![cfg(not(target_os = "windows"))] #![cfg(not(target_os = "windows"))]
use codex_core::TurnInputRequest;
use codex_core::TurnStartOptions;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses; use core_test_support::responses;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
@@ -77,30 +82,31 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> {
// 1) Normal user input – should hit server once. // 1) Normal user input – should hit server once.
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello world".into(), text: "hello world".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: Some(serde_json::from_str(SCHEMA)?), .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd)), environments: Some(local_selections(cwd)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model, model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, })
}) .on_start(TurnStartOptions {
final_output_json_schema: Some(serde_json::from_str(SCHEMA)?),
..Default::default()
}),
)
.await?; .await?;
let message = wait_for_event(&codex, |ev| matches!(ev, EventMsg::AgentMessage(_))).await; let message = wait_for_event(&codex, |ev| matches!(ev, EventMsg::AgentMessage(_))).await;
@@ -1,6 +1,7 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_protocol::approvals::ElicitationRequest; use codex_protocol::approvals::ElicitationRequest;
@@ -135,16 +136,10 @@ default_tools_approval_mode = "auto"
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Use [$calendar](app://calendar) to create a calendar event.".to_string(),
text: "Use [$calendar](app://calendar) to create a calendar event.".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let EventMsg::ElicitationRequest(request) = wait_for_event(&test.codex, |event| { let EventMsg::ElicitationRequest(request) = wait_for_event(&test.codex, |event| {
@@ -1,3 +1,4 @@
use codex_core::TurnInputRequest;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
@@ -10,7 +11,7 @@ use codex_features::Feature;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUri;
use core_test_support::apps_test_server::AppsTestServer; use core_test_support::apps_test_server::AppsTestServer;
@@ -313,8 +314,8 @@ async fn skill_mcp_dependency_oauth_uses_configured_http_client() -> Result<()>
turn_permission_fields(PermissionProfile::Disabled, fixture.config.cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, fixture.config.cwd.as_path());
fixture fixture
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![ TurnInputRequest::user_input(vec![
UserInput::Text { UserInput::Text {
text: "please use $proxy-skill".to_string(), text: "please use $proxy-skill".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
@@ -323,18 +324,15 @@ async fn skill_mcp_dependency_oauth_uses_configured_http_client() -> Result<()>
name: "proxy-skill".to_string(), name: "proxy-skill".to_string(),
path: skill_path.to_path_buf(), path: skill_path.to_path_buf(),
}, },
], ])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(fixture.config.cwd.clone())), environments: Some(local_selections(fixture.config.cwd.clone())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
core_test_support::wait_for_event(fixture.codex.as_ref(), |event| { core_test_support::wait_for_event(fixture.codex.as_ref(), |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+22 -22
View File
@@ -7,6 +7,7 @@ use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig; use codex_config::types::McpServerTransportConfig;
use codex_core::NewThread; use codex_core::NewThread;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
use codex_exec_server::ExecutorFileSystem; use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::RemoveOptions; use codex_exec_server::RemoveOptions;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
@@ -17,6 +18,7 @@ use codex_protocol::protocol::McpStartupStatus;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUri;
use core_test_support::apps_test_server::AppsTestServer; use core_test_support::apps_test_server::AppsTestServer;
@@ -40,21 +42,16 @@ use super::rmcp_client::remote_aware_stdio_server_bin;
const SERVER_NAME: &str = "cached_rmcp"; const SERVER_NAME: &str = "cached_rmcp";
const NAMESPACE: &str = "mcp__cached_rmcp"; const NAMESPACE: &str = "mcp__cached_rmcp";
fn user_turn(prompt: &str) -> Op { fn user_turn(prompt: &str) -> TurnInputRequest {
Op::UserInput { TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: prompt.to_string(),
text: prompt.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }])
}], .with_thread_settings(ThreadSettingsOverrides {
final_output_json_schema: None, approval_policy: Some(AskForApproval::Never),
responsesapi_client_metadata: None, permission_profile: Some(PermissionProfile::Disabled),
additional_context: Default::default(), ..Default::default()
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { })
approval_policy: Some(AskForApproval::Never),
permission_profile: Some(PermissionProfile::Disabled),
..Default::default()
},
}
} }
fn process_label(pid: &str) -> String { fn process_label(pid: &str) -> String {
@@ -193,7 +190,7 @@ async fn mcp_calls_stay_bound_to_each_thread() -> anyhow::Result<()> {
) )
.await; .await;
thread thread
.submit(user_turn(&format!("Call the {SERVER_NAME} echo tool."))) .start_or_steer_turn(user_turn(&format!("Call the {SERVER_NAME} echo tool.")))
.await?; .await?;
let EventMsg::McpToolCallEnd(end) = wait_for_event( let EventMsg::McpToolCallEnd(end) = wait_for_event(
thread, thread,
@@ -362,7 +359,7 @@ async fn cached_http_mcp_starts_lazily_for_subagents(
) )
.await; .await;
subagent subagent
.submit(user_turn("Call the cached HTTP tool.")) .start_or_steer_turn(user_turn("Call the cached HTTP tool."))
.await?; .await?;
wait_for_event(&subagent, |event| { wait_for_event(&subagent, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -445,7 +442,10 @@ async fn cached_mcp_startup_is_eager_for_root_and_lazy_for_subagents() -> anyhow
]), ]),
) )
.await; .await;
fixture.codex.submit(user_turn("use the echo tool")).await?; fixture
.codex
.start_or_steer_turn(user_turn("use the echo tool"))
.await?;
let first_pid = wait_for_new_pid(fs.as_ref(), &pid_file, /*previous_pid*/ None).await?; let first_pid = wait_for_new_pid(fs.as_ref(), &pid_file, /*previous_pid*/ None).await?;
fs.write_file(&barrier_file, b"ready".to_vec(), /*sandbox*/ None) fs.write_file(&barrier_file, b"ready".to_vec(), /*sandbox*/ None)
.await?; .await?;
@@ -515,7 +515,7 @@ async fn cached_mcp_startup_is_eager_for_root_and_lazy_for_subagents() -> anyhow
) )
.await; .await;
second_thread second_thread
.submit(user_turn("Do not call any MCP tools.")) .start_or_steer_turn(user_turn("Do not call any MCP tools."))
.await?; .await?;
let mut reported_ready_before_startup = false; let mut reported_ready_before_startup = false;
wait_for_event(&second_thread, |event| { wait_for_event(&second_thread, |event| {
@@ -578,7 +578,7 @@ async fn cached_mcp_startup_is_eager_for_root_and_lazy_for_subagents() -> anyhow
let second_for_turn = Arc::clone(&second_thread); let second_for_turn = Arc::clone(&second_thread);
let cached_turn = tokio::spawn(async move { let cached_turn = tokio::spawn(async move {
second_for_turn second_for_turn
.submit(user_turn("call the echo and cwd tools")) .start_or_steer_turn(user_turn("call the echo and cwd tools"))
.await?; .await?;
let mut unrelated_finished_tx = Some(unrelated_finished_tx); let mut unrelated_finished_tx = Some(unrelated_finished_tx);
let mut saw_starting = false; let mut saw_starting = false;
@@ -734,7 +734,7 @@ async fn cached_mcp_startup_is_eager_for_root_and_lazy_for_subagents() -> anyhow
) )
.await; .await;
interrupted_thread interrupted_thread
.submit(user_turn("Start the cached MCP tool.")) .start_or_steer_turn(user_turn("Start the cached MCP tool."))
.await?; .await?;
let interrupted_pid = wait_for_new_pid(fs.as_ref(), &pid_file, Some(&filtered_pid)).await?; let interrupted_pid = wait_for_new_pid(fs.as_ref(), &pid_file, Some(&filtered_pid)).await?;
wait_for_event(&interrupted_thread, |event| { wait_for_event(&interrupted_thread, |event| {
@@ -790,7 +790,7 @@ async fn cached_mcp_startup_is_eager_for_root_and_lazy_for_subagents() -> anyhow
) )
.await; .await;
interrupted_thread interrupted_thread
.submit(user_turn("Retry the cached MCP tool.")) .start_or_steer_turn(user_turn("Retry the cached MCP tool."))
.await?; .await?;
wait_for_event(&interrupted_thread, |event| { wait_for_event(&interrupted_thread, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+9 -20
View File
@@ -1,5 +1,6 @@
use anyhow::Result; use anyhow::Result;
use codex_config::Constrained; use codex_config::Constrained;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_extension_api::ExtensionFuture; use codex_extension_api::ExtensionFuture;
use codex_extension_api::ExtensionRegistryBuilder; use codex_extension_api::ExtensionRegistryBuilder;
@@ -904,16 +905,10 @@ async fn apps_guidance_and_deferred_namespace_appear_after_recovery_within_a_tur
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "use an app after it recovers".into(),
text: "use an app after it recovers".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let EventMsg::RequestUserInput(request) = wait_for_event(&test.codex, |event| { let EventMsg::RequestUserInput(request) = wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::RequestUserInput(_)) matches!(event, EventMsg::RequestUserInput(_))
@@ -1093,16 +1088,10 @@ async fn later_follow_up_uses_background_recovered_apps_after_mid_thread_startup
startup_control.fail_next_initialize_attempts(/*attempts*/ 1); startup_control.fail_next_initialize_attempts(/*attempts*/ 1);
test.codex.submit(Op::RefreshMcpServers).await?; test.codex.submit(Op::RefreshMcpServers).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "use Calendar after transient Apps startup failures".into(),
text: "use Calendar after transient Apps startup failures".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
tokio::time::timeout(Duration::from_secs(5), async { tokio::time::timeout(Duration::from_secs(5), async {
let mut turn_complete = false; let mut turn_complete = false;
+11 -12
View File
@@ -4,6 +4,7 @@
use anyhow::Result; use anyhow::Result;
use codex_config::test_support::CloudConfigBundleFixture; use codex_config::test_support::CloudConfigBundleFixture;
use codex_config::types::AppToolApproval; use codex_config::types::AppToolApproval;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_features::Feature; use codex_features::Feature;
use codex_history::RolloutItem; use codex_history::RolloutItem;
@@ -20,6 +21,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ElicitationAction; use codex_protocol::protocol::ElicitationAction;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -111,23 +113,20 @@ async fn submit_user_turn(
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.cwd.path()); turn_permission_fields(permission_profile, test.cwd.path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: text.to_string(), text: text.to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: collaboration_mode.or({ collaboration_mode: collaboration_mode.or({
Some(codex_protocol::config_types::CollaborationMode { Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
@@ -135,8 +134,8 @@ async fn submit_user_turn(
}) })
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(()) Ok(())
} }
+1 -1
View File
@@ -148,13 +148,13 @@ mod tool_harness;
mod tool_parallelism; mod tool_parallelism;
mod tools; mod tools;
mod truncation; mod truncation;
mod turn_input_submission;
mod turn_state; mod turn_state;
mod unified_exec; mod unified_exec;
mod unified_exec_process_events; mod unified_exec_process_events;
#[cfg(unix)] #[cfg(unix)]
mod unified_exec_zsh_fork_approvals; mod unified_exec_zsh_fork_approvals;
mod unstable_features_warning; mod unstable_features_warning;
mod user_message_admission;
mod user_notification; mod user_notification;
mod user_shell_cmd; mod user_shell_cmd;
mod view_image; mod view_image;
@@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_features::Feature; use codex_features::Feature;
use codex_login::CodexAuth; use codex_login::CodexAuth;
@@ -13,7 +14,6 @@ use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ToolMode; use codex_protocol::openai_models::ToolMode;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses; use core_test_support::responses;
@@ -133,16 +133,10 @@ async fn response_for_remote_model(
) )
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "list tools".into(),
text: "list tools".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let mut warnings = Vec::new(); let mut warnings = Vec::new();
loop { loop {
@@ -329,16 +323,10 @@ async fn unsupported_code_mode_warning_is_emitted_each_turn() -> Result<()> {
let mut warning_counts = Vec::new(); let mut warning_counts = Vec::new();
for prompt in ["first turn", "second turn"] { for prompt in ["first turn", "second turn"] {
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: prompt.to_string(),
text: prompt.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let mut warning_count = 0; let mut warning_count = 0;
@@ -471,16 +459,10 @@ async fn remote_multi_agent_selector_uses_model_selected_before_first_turn() ->
assert_eq!(test.codex.multi_agent_version(), None); assert_eq!(test.codex.multi_agent_version(), None);
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: ROOT_PROMPT.into(),
text: ROOT_PROMPT.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+42 -47
View File
@@ -2,15 +2,19 @@ use anyhow::Result;
use codex_config::types::Personality; use codex_config::types::Personality;
use codex_core::CodexThread; use codex_core::CodexThread;
use codex_core::ForkSnapshot; use codex_core::ForkSnapshot;
use codex_core::TurnInputRequest;
use codex_features::Feature; use codex_features::Feature;
use codex_history::RolloutItem; use codex_history::RolloutItem;
use codex_history::RolloutLine; use codex_history::RolloutLine;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_models_manager::bundled_models_response; use codex_models_manager::bundled_models_response;
use codex_models_manager::manager::RefreshStrategy; use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE; use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::Settings;
use codex_protocol::models::BaseInstructionsProvenance; use codex_protocol::models::BaseInstructionsProvenance;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
@@ -47,30 +51,24 @@ use pretty_assertions::assert_eq;
use test_case::test_case; use test_case::test_case;
use wiremock::MockServer; use wiremock::MockServer;
fn read_only_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -> Op { fn read_only_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -> TurnInputRequest {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::read_only(), test.cwd_path()); turn_permission_fields(PermissionProfile::read_only(), test.cwd_path());
Op::UserInput { TurnInputRequest::user_input(items).with_thread_settings(ThreadSettingsOverrides {
items, environments: Some(local_selections(test.config.cwd.clone())),
final_output_json_schema: None, approval_policy: Some(AskForApproval::Never),
responsesapi_client_metadata: None, sandbox_policy: Some(sandbox_policy),
additional_context: Default::default(), permission_profile,
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { collaboration_mode: Some(CollaborationMode {
environments: Some(local_selections(test.config.cwd.clone())), mode: ModeKind::Default,
approval_policy: Some(AskForApproval::Never), settings: Settings {
sandbox_policy: Some(sandbox_policy), model,
permission_profile, reasoning_effort: test.config.model_reasoning_effort.clone(),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { developer_instructions: None,
mode: codex_protocol::config_types::ModeKind::Default, },
settings: codex_protocol::config_types::Settings { }),
model, ..Default::default()
reasoning_effort: test.config.model_reasoning_effort.clone(), })
developer_instructions: None,
},
}),
..Default::default()
},
}
} }
async fn submit_model_turn( async fn submit_model_turn(
@@ -80,16 +78,13 @@ async fn submit_model_turn(
) -> Result<()> { ) -> Result<()> {
thread_settings.model = Some(model.to_string()); thread_settings.model = Some(model.to_string());
thread thread
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "switch models".into(), text: "switch models".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(thread_settings),
responsesapi_client_metadata: None, )
additional_context: Default::default(),
thread_settings,
})
.await?; .await?;
wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await;
Ok(()) Ok(())
@@ -371,7 +366,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
let next_model = "gpt-5.4"; let next_model = "gpt-5.4";
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "hello".into(), text: "hello".into(),
@@ -384,7 +379,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some(next_model.to_string()), model: Some(next_model.to_string()),
..Default::default() ..Default::default()
}, },
@@ -392,7 +387,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
.await?; .await?;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "switch models".into(), text: "switch models".into(),
@@ -466,7 +461,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
let next_model = "exp-codex-personality"; let next_model = "exp-codex-personality";
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "hello".into(), text: "hello".into(),
@@ -479,7 +474,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some(next_model.to_string()), model: Some(next_model.to_string()),
personality: Some(Personality::Pragmatic), personality: Some(Personality::Pragmatic),
..Default::default() ..Default::default()
@@ -488,7 +483,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
.await?; .await?;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "switch model and personality".into(), text: "switch model and personality".into(),
@@ -793,7 +788,7 @@ async fn model_change_from_multimodal_to_text_strips_prior_media_content() -> Re
.to_string(); .to_string();
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![ vec![
UserInput::Image { UserInput::Image {
@@ -814,7 +809,7 @@ async fn model_change_from_multimodal_to_text_strips_prior_media_content() -> Re
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "second turn".to_string(), text: "second turn".to_string(),
@@ -911,7 +906,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
.await; .await;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "generate a lobster".to_string(), text: "generate a lobster".to_string(),
@@ -923,7 +918,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "describe the generated image".to_string(), text: "describe the generated image".to_string(),
@@ -1008,7 +1003,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
.await; .await;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "generate a lobster".to_string(), text: "generate a lobster".to_string(),
@@ -1020,7 +1015,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "describe the generated image".to_string(), text: "describe the generated image".to_string(),
@@ -1107,7 +1102,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
.await; .await;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "generate a lobster".to_string(), text: "generate a lobster".to_string(),
@@ -1127,7 +1122,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
.await; .await;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "after rollback".to_string(), text: "after rollback".to_string(),
@@ -1283,7 +1278,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
); );
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "use larger model".into(), text: "use larger model".into(),
@@ -1318,7 +1313,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some(smaller_model_slug.to_string()), model: Some(smaller_model_slug.to_string()),
..Default::default() ..Default::default()
}, },
@@ -1326,7 +1321,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
.await?; .await?;
test.codex test.codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&test, &test,
vec![UserInput::Text { vec![UserInput::Text {
text: "switch to smaller model".into(), text: "switch to smaller model".into(),
+69 -101
View File
@@ -5,6 +5,7 @@ use std::sync::Mutex;
use anyhow::Result; use anyhow::Result;
use codex_config::types::Personality; use codex_config::types::Personality;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_extension_api::ContextualUserFragment; use codex_extension_api::ContextualUserFragment;
@@ -17,10 +18,13 @@ use codex_extension_api::TurnInputContext;
use codex_extension_api::TurnInputContributor; use codex_extension_api::TurnInputContributor;
use codex_extension_api::TurnInputEnvironment; use codex_extension_api::TurnInputEnvironment;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -201,15 +205,12 @@ async fn model_visible_environment_context_preserves_foreign_workspace_roots() -
let foreign_root = PathUri::parse("file:///C:/workspace")?; let foreign_root = PathUri::parse("file:///C:/workspace")?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "inspect the workspace".into(), text: "inspect the workspace".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new( environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(), test.config.cwd.clone(),
vec![TurnEnvironmentSelection { vec![TurnEnvironmentSelection {
@@ -219,8 +220,8 @@ async fn model_visible_environment_context_preserves_foreign_workspace_roots() -
}], }],
)), )),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -282,30 +283,27 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
turn_permission_fields(PermissionProfile::read_only(), first_turn_cwd.as_path()); turn_permission_fields(PermissionProfile::read_only(), first_turn_cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "first turn".into(), text: "first turn".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(first_turn_cwd)), environments: Some(local_selections(first_turn_cwd)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(first_sandbox_policy), sandbox_policy: Some(first_sandbox_policy),
permission_profile: first_permission_profile, permission_profile: first_permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort.clone(), reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -317,31 +315,28 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
preturn_context_diff_cwd.as_path(), preturn_context_diff_cwd.as_path(),
); );
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "second turn with context updates".into(), text: "second turn with context updates".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(preturn_context_diff_cwd)), environments: Some(local_selections(preturn_context_diff_cwd)),
approval_policy: Some(AskForApproval::OnRequest), approval_policy: Some(AskForApproval::OnRequest),
sandbox_policy: Some(second_sandbox_policy), sandbox_policy: Some(second_sandbox_policy),
permission_profile: second_permission_profile, permission_profile: second_permission_profile,
personality: Some(Personality::Friendly), personality: Some(Personality::Friendly),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort.clone(), reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -408,30 +403,27 @@ async fn snapshot_model_visible_layout_cwd_change_refreshes_agents() -> Result<(
turn_permission_fields(PermissionProfile::read_only(), cwd_one.as_path()); turn_permission_fields(PermissionProfile::read_only(), cwd_one.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "first turn in agents_one".into(), text: "first turn in agents_one".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd_one.clone())), environments: Some(local_selections(cwd_one.clone())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(first_sandbox_policy), sandbox_policy: Some(first_sandbox_policy),
permission_profile: first_permission_profile, permission_profile: first_permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort.clone(), reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -441,30 +433,27 @@ async fn snapshot_model_visible_layout_cwd_change_refreshes_agents() -> Result<(
let (second_sandbox_policy, second_permission_profile) = let (second_sandbox_policy, second_permission_profile) =
turn_permission_fields(PermissionProfile::read_only(), cwd_two.as_path()); turn_permission_fields(PermissionProfile::read_only(), cwd_two.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "second turn in agents_two".into(), text: "second turn in agents_two".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd_two)), environments: Some(local_selections(cwd_two)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(second_sandbox_policy), sandbox_policy: Some(second_sandbox_policy),
permission_profile: second_permission_profile, permission_profile: second_permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort.clone(), reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -520,16 +509,10 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
) )
.await; .await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "seed resume history".into(),
text: "seed resume history".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let initial_request = initial_mock.single_request(); let initial_request = initial_mock.single_request();
@@ -564,31 +547,28 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
); );
resumed resumed
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "resume and change personality".into(), text: "resume and change personality".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(resume_override_cwd)), environments: Some(local_selections(resume_override_cwd)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
personality: Some(Personality::Friendly), personality: Some(Personality::Friendly),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: resumed.session_configured.model.clone(), model: resumed.session_configured.model.clone(),
reasoning_effort: resumed.config.model_reasoning_effort.clone(), reasoning_effort: resumed.config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&resumed.codex, |event| { wait_for_event(&resumed.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -633,16 +613,10 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
) )
.await; .await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "seed resume history".into(),
text: "seed resume history".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let initial_request = initial_mock.single_request(); let initial_request = initial_mock.single_request();
@@ -668,7 +642,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
let resume_override_cwd = resume_override_cwd.abs(); let resume_override_cwd = resume_override_cwd.abs();
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&resumed.codex, &resumed.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
environments: Some(local_selections(resume_override_cwd)), environments: Some(local_selections(resume_override_cwd)),
model: Some("gpt-5.2".to_string()), model: Some("gpt-5.2".to_string()),
..Default::default() ..Default::default()
@@ -677,16 +651,10 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
.await?; .await?;
resumed resumed
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "first resumed turn after model override".into(),
text: "first resumed turn after model override".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&resumed.codex, |event| { wait_for_event(&resumed.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+14 -13
View File
@@ -1,3 +1,4 @@
use codex_core::TurnInputRequest;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
@@ -9,7 +10,10 @@ use chrono::Utc;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_models_manager::client_version_to_whole; use codex_models_manager::client_version_to_whole;
use codex_models_manager::manager::RefreshStrategy; use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
@@ -21,7 +25,7 @@ use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::openai_models::TruncationPolicyConfig; use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::openai_models::default_input_modalities; use codex_protocol::openai_models::default_input_modalities;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses; use core_test_support::responses;
use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_assistant_message;
@@ -98,30 +102,27 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> {
turn_permission_fields(PermissionProfile::Disabled, test.cwd_path()); turn_permission_fields(PermissionProfile::Disabled, test.cwd_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hi".into(), text: "hi".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(codex_protocol::protocol::AskForApproval::Never), approval_policy: Some(codex_protocol::protocol::AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let _ = wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; let _ = wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -1,5 +1,6 @@
#![cfg(not(target_os = "windows"))] #![cfg(not(target_os = "windows"))]
use codex_core::TurnInputRequest;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -7,11 +8,14 @@ use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use codex_features::Feature; use codex_features::Feature;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::TempDirExt; use core_test_support::TempDirExt;
use core_test_support::responses; use core_test_support::responses;
@@ -106,30 +110,27 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch
.await; .await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "please run a tool".into(), text: "please run a tool".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd_path)), environments: Some(local_selections(cwd_path)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let _ = wait_for_event_with_timeout( let _ = wait_for_event_with_timeout(
+7 -10
View File
@@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
@@ -6,7 +7,6 @@ use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ReasoningEffortPreset; use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG; use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
@@ -74,19 +74,16 @@ async fn submit_turn(
effort: Option<ReasoningEffort>, effort: Option<ReasoningEffort>,
) -> Result<()> { ) -> Result<()> {
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.to_string(), text: prompt.to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
effort: effort.map(Some), effort: effort.map(Some),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
Ok(()) Ok(())
+12 -21
View File
@@ -1,12 +1,12 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::AgentRoleConfig; use codex_core::config::AgentRoleConfig;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::Submission;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::assert_parent_turn; use core_test_support::responses::assert_parent_turn;
use core_test_support::responses::assert_root_turn; use core_test_support::responses::assert_root_turn;
@@ -246,27 +246,18 @@ async fn cold_root_resume_restores_agent_identity_and_role_on_followup() -> Resu
}); });
let initial = initial_builder.build_with_auto_env(&server).await?; let initial = initial_builder.build_with_auto_env(&server).await?;
let root_thread_id = initial.session_configured.thread_id; let root_thread_id = initial.session_configured.thread_id;
let mut op = vec![UserInput::Text {
text: INITIAL_PROMPT.to_string(),
text_elements: Vec::new(),
}]
.into();
if let Op::UserInput {
thread_settings, ..
} = &mut op
{
thread_settings.permission_profile = Some(PermissionProfile::Disabled);
}
initial initial
.codex .codex
.submit_with_id(Submission { .start_or_steer_turn(
id: "spoofed-root-turn".to_string(), TurnInputRequest::user_input(vec![UserInput::Text {
op, text: INITIAL_PROMPT.to_string(),
client_user_message_id: None, text_elements: Vec::new(),
trace: None, }])
parent_turn_id: Some("spoofed-parent-turn".to_string()), .with_thread_settings(ThreadSettingsOverrides {
root_turn_id: Some("spoofed-foreign-root-turn".to_string()), permission_profile: Some(PermissionProfile::Disabled),
}) ..Default::default()
}),
)
.await?; .await?;
wait_for_event(&initial.codex, |event| { wait_for_event(&initial.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -1,3 +1,4 @@
use codex_core::TurnInputRequest;
use std::process::Stdio; use std::process::Stdio;
use std::time::Duration; use std::time::Duration;
@@ -9,7 +10,6 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::TurnEnvironmentSelections;
@@ -206,15 +206,12 @@ async fn two_exec_servers_isolate_workspace_write_roots() -> Result<()> {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.config.cwd.as_path()); turn_permission_fields(permission_profile, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "write one file in each environment".into(), text: "write one file in each environment".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new( environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(), test.config.cwd.clone(),
environments, environments,
@@ -223,8 +220,8 @@ async fn two_exec_servers_isolate_workspace_write_roots() -> Result<()> {
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event_with_timeout( wait_for_event_with_timeout(
&test.codex, &test.codex,
+14 -12
View File
@@ -1,6 +1,7 @@
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use codex_config::types::ApprovalsReviewer; use codex_config::types::ApprovalsReviewer;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_exec_server::LOCAL_ENVIRONMENT_ID;
@@ -11,6 +12,9 @@ use codex_protocol::approvals::NetworkApprovalContext;
use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::approvals::NetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction; use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
@@ -19,6 +23,7 @@ use codex_protocol::protocol::ExecApprovalRequestEvent;
use codex_protocol::protocol::GuardianAssessmentStatus; use codex_protocol::protocol::GuardianAssessmentStatus;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -1664,31 +1669,28 @@ async fn submit_managed_network_turn(
TurnEnvironmentSelections::new(test.config.cwd.clone(), environments); TurnEnvironmentSelections::new(test.config.cwd.clone(), environments);
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(turn_environment_selections), environments: Some(turn_environment_selections),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(approvals_reviewer), approvals_reviewer: Some(approvals_reviewer),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(()) Ok(())
+89 -220
View File
@@ -1,3 +1,4 @@
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_features::Feature; use codex_features::Feature;
use codex_otel::SessionTelemetry; use codex_otel::SessionTelemetry;
@@ -128,16 +129,10 @@ async fn responses_api_emits_api_request_event() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -193,16 +188,10 @@ async fn process_sse_emits_tracing_for_output_item() {
let TestCodex { codex, .. } = test_codex().build(&server).await.unwrap(); let TestCodex { codex, .. } = test_codex().build(&server).await.unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -239,16 +228,10 @@ async fn process_sse_emits_failed_event_on_parse_error() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -286,16 +269,10 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed()
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -353,16 +330,10 @@ async fn process_sse_failed_event_records_response_error_message() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -418,16 +389,10 @@ async fn process_sse_failed_event_logs_parse_error() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -470,16 +435,10 @@ async fn process_sse_failed_event_logs_missing_error() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -531,16 +490,10 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -592,16 +545,10 @@ async fn process_sse_emits_completed_telemetry() {
let TestCodex { codex, .. } = test_codex().build(&server).await.unwrap(); let TestCodex { codex, .. } = test_codex().build(&server).await.unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -678,16 +625,10 @@ async fn turn_and_completed_response_spans_record_token_usage() {
let TestCodex { codex, .. } = test; let TestCodex { codex, .. } = test;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -768,16 +709,10 @@ async fn handle_responses_span_records_response_kind_and_tool_name() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -862,16 +797,10 @@ async fn record_responses_sets_span_fields_for_response_events() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -951,16 +880,10 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1027,16 +950,10 @@ async fn handle_response_item_records_tool_result_for_function_call() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1104,16 +1021,10 @@ async fn handle_response_item_records_tool_result_for_shell_command_call() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1277,16 +1188,10 @@ async fn handle_shell_command_autoapprove_from_config_records_tool_decision() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1332,16 +1237,10 @@ async fn handle_shell_command_user_approved_records_tool_decision() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "approved".into(),
text: "approved".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1402,16 +1301,10 @@ async fn handle_shell_command_user_approved_for_session_records_tool_decision()
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "persist".into(),
text: "persist".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1472,16 +1365,10 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "retry".into(),
text: "retry".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1542,16 +1429,10 @@ async fn handle_shell_command_user_denies_records_tool_decision() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "deny".into(),
text: "deny".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1612,16 +1493,10 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision()
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "persist".into(),
text: "persist".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -1683,16 +1558,10 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() {
.unwrap(); .unwrap();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "deny".into(),
text: "deny".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
+43 -66
View File
@@ -2,7 +2,10 @@ use core_test_support::test_codex::local_selections;
use std::sync::Arc; use std::sync::Arc;
use codex_core::CodexThread; use codex_core::CodexThread;
use codex_core::StartIfIdleSubmission;
use codex_core::TurnInput; use codex_core::TurnInput;
use codex_core::TurnInputRequest;
use codex_core::TurnInputSubmission;
use codex_core::config::CurrentTimeReminderConfig; use codex_core::config::CurrentTimeReminderConfig;
use codex_extension_items::ExtensionItem; use codex_extension_items::ExtensionItem;
use codex_extension_items::sleep::SleepItem; use codex_extension_items::sleep::SleepItem;
@@ -70,14 +73,13 @@ async fn idle_response_items_include_pending_mailbox_in_first_request() -> anyho
let test = test_codex().build_with_auto_env(&server).await?; let test = test_codex().build_with_auto_env(&server).await?;
submit_queue_only_agent_mail(test.codex.as_ref(), "pending mailbox input").await; submit_queue_only_agent_mail(test.codex.as_ref(), "pending mailbox input").await;
test.codex let submission = test
.try_start_turn_if_idle(vec![TurnInput::ResponseItem( .codex
responses::user_message_item("automatic response item").into(), .start_turn_if_idle(TurnInputRequest::new(TurnInput::ResponseItem(
)]) responses::user_message_item("automatic response item"),
.await )))
.map_err(|error| { .await?;
anyhow::anyhow!("idle response items were rejected: {:?}", error.reason()) assert!(matches!(submission, StartIfIdleSubmission::Started { .. }));
})?;
wait_for_turn_complete(test.codex.as_ref()).await; wait_for_turn_complete(test.codex.as_ref()).await;
let request = response.single_request(); let request = response.single_request();
@@ -144,13 +146,14 @@ async fn assert_idle_user_input_reaches_the_first_model_request(
text: "queued user input reaches the first request".to_string(), text: "queued user input reaches the first request".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}]; }];
test.codex let submission = test
.try_start_turn_if_idle(vec![TurnInput::UserInput { .codex
.start_turn_if_idle(TurnInputRequest::new(TurnInput::UserInput {
content: expected_input.clone(), content: expected_input.clone(),
client_id: Some("queued-user-message".to_string()), client_id: Some("queued-user-message".to_string()),
}]) }))
.await .await?;
.map_err(|error| anyhow::anyhow!("idle user input was rejected: {:?}", error.reason()))?; assert!(matches!(submission, StartIfIdleSubmission::Started { .. }));
let user_message = core_test_support::wait_for_event_match(test.codex.as_ref(), |event| { let user_message = core_test_support::wait_for_event_match(test.codex.as_ref(), |event| {
let EventMsg::ItemCompleted(event) = event else { let EventMsg::ItemCompleted(event) = event else {
@@ -276,16 +279,10 @@ async fn build_codex(server: &StreamingSseServer) -> Arc<CodexThread> {
async fn submit_user_input(codex: &CodexThread, text: &str) { async fn submit_user_input(codex: &CodexThread, text: &str) {
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.to_string(),
text: text.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.expect("submit user input"); .expect("submit user input");
} }
@@ -294,48 +291,40 @@ async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: text.to_string(), text: text.to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await .await
.expect("submit user turn"); .expect("submit user turn");
} }
async fn steer_user_input(codex: &CodexThread, text: &str) { async fn steer_user_input(codex: &CodexThread, text: &str) {
codex let submission = codex
.steer_input( .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
vec![UserInput::Text { text: text.to_string(),
text: text.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
/*additional_context*/ Default::default(),
/*expected_turn_id*/ None,
/*client_user_message_id*/ None,
/*responsesapi_client_metadata*/ None,
)
.await .await
.expect("steer user input"); .expect("steer user input");
assert!(matches!(submission, TurnInputSubmission::Steered { .. }));
} }
async fn enqueue_queue_only_agent_mail(codex: &CodexThread, text: &str) { async fn enqueue_queue_only_agent_mail(codex: &CodexThread, text: &str) {
@@ -759,16 +748,10 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
.codex; .codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "first prompt".into(),
text: "first prompt".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -778,16 +761,10 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
.await; .await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "second prompt".into(),
text: "second prompt".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
+72 -167
View File
@@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use codex_config::ConfigLayerStack; use codex_config::ConfigLayerStack;
use codex_core::ForkSnapshot; use codex_core::ForkSnapshot;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_core::context::ApprovalPromptContext; use codex_core::context::ApprovalPromptContext;
use codex_core::context::ContextualUserFragment; use codex_core::context::ContextualUserFragment;
@@ -15,7 +16,7 @@ use codex_protocol::openai_models::PermissionMessages;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::responses::ResponsesRequest; use core_test_support::responses::ResponsesRequest;
@@ -84,16 +85,10 @@ async fn submit_text_turn(
text: &str, text: &str,
) -> Result<()> { ) -> Result<()> {
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.to_string(),
text: text.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
Ok(()) Ok(())
@@ -171,7 +166,7 @@ async fn model_change_appends_new_catalog_approval_message() -> Result<()> {
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some(second_slug.to_string()), model: Some(second_slug.to_string()),
..Default::default() ..Default::default()
}, },
@@ -304,7 +299,7 @@ async fn catalog_permission_message_is_sent_initially_and_after_model_change() -
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some(second_slug.to_string()), model: Some(second_slug.to_string()),
..Default::default() ..Default::default()
}, },
@@ -393,16 +388,10 @@ async fn permissions_message_sent_once_on_start() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -433,22 +422,16 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
..Default::default() ..Default::default()
}, },
@@ -456,16 +439,10 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -502,30 +479,18 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -562,22 +527,16 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
..Default::default() ..Default::default()
}, },
@@ -585,16 +544,10 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> {
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -638,22 +591,16 @@ async fn resume_replays_permissions_messages() -> Result<()> {
initial initial
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&initial.codex, &initial.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
..Default::default() ..Default::default()
}, },
@@ -662,32 +609,20 @@ async fn resume_replays_permissions_messages() -> Result<()> {
initial initial
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let resumed = builder.restart(&server, &initial).await?; let resumed = builder.restart(&server, &initial).await?;
resumed resumed
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "after resume".into(),
text: "after resume".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -737,22 +672,16 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
initial initial
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&initial.codex, &initial.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
..Default::default() ..Default::default()
}, },
@@ -761,16 +690,10 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
initial initial
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -783,16 +706,10 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
let resumed = builder.restart(&server, &initial).await?; let resumed = builder.restart(&server, &initial).await?;
resumed resumed
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "after resume".into(),
text: "after resume".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -818,16 +735,10 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
.await?; .await?;
forked forked
.thread .thread
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "after fork".into(),
text: "after fork".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -879,16 +790,10 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello".into(),
text: "hello".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+46 -47
View File
@@ -1,10 +1,14 @@
use codex_config::types::Personality; use codex_config::types::Personality;
use codex_core::TurnInputRequest;
use codex_features::Feature; use codex_features::Feature;
use codex_models_manager::bundled_models_response; use codex_models_manager::bundled_models_response;
use codex_models_manager::manager::RefreshStrategy; use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager; use codex_models_manager::manager::SharedModelsManager;
use codex_models_manager::model_info::BASE_INSTRUCTIONS; use codex_models_manager::model_info::BASE_INSTRUCTIONS;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
@@ -18,7 +22,7 @@ use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::openai_models::default_input_modalities; use codex_protocol::openai_models::default_input_modalities;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::mount_models_once; use core_test_support::responses::mount_models_once;
use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_once;
@@ -47,7 +51,7 @@ fn read_only_text_turn(
text: &str, text: &str,
model: String, model: String,
approval_policy: AskForApproval, approval_policy: AskForApproval,
) -> Op { ) -> TurnInputRequest {
let personality = None; let personality = None;
read_only_text_turn_with_personality(test, text, model, approval_policy, personality) read_only_text_turn_with_personality(test, text, model, approval_policy, personality)
} }
@@ -58,34 +62,29 @@ fn read_only_text_turn_with_personality(
model: String, model: String,
approval_policy: AskForApproval, approval_policy: AskForApproval,
personality: Option<Personality>, personality: Option<Personality>,
) -> Op { ) -> TurnInputRequest {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::read_only(), test.cwd_path()); turn_permission_fields(PermissionProfile::read_only(), test.cwd_path());
Op::UserInput { TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.into(),
text: text.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }])
}], .with_thread_settings(ThreadSettingsOverrides {
final_output_json_schema: None, environments: Some(local_selections(test.config.cwd.clone())),
responsesapi_client_metadata: None, approval_policy: Some(approval_policy),
additional_context: Default::default(), sandbox_policy: Some(sandbox_policy),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { permission_profile,
environments: Some(local_selections(test.config.cwd.clone())), personality,
approval_policy: Some(approval_policy), collaboration_mode: Some(CollaborationMode {
sandbox_policy: Some(sandbox_policy), mode: ModeKind::Default,
permission_profile, settings: Settings {
personality, model,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { reasoning_effort: test.config.model_reasoning_effort.clone(),
mode: codex_protocol::config_types::ModeKind::Default, developer_instructions: None,
settings: codex_protocol::config_types::Settings { },
model, }),
reasoning_effort: test.config.model_reasoning_effort.clone(), ..Default::default()
developer_instructions: None, })
},
}),
..Default::default()
},
}
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -103,7 +102,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -141,7 +140,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result<
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -186,7 +185,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -245,7 +244,7 @@ async fn config_personality_none_strips_baked_personality_section() -> anyhow::R
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -282,7 +281,7 @@ async fn config_personality_none_preserves_explicit_base_instructions() -> anyho
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -315,7 +314,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -356,7 +355,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -368,7 +367,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
personality: Some(Personality::Friendly), personality: Some(Personality::Friendly),
..Default::default() ..Default::default()
}, },
@@ -376,7 +375,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
.await?; .await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -432,7 +431,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -444,7 +443,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
personality: Some(Personality::Pragmatic), personality: Some(Personality::Pragmatic),
..Default::default() ..Default::default()
}, },
@@ -452,7 +451,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
.await?; .await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -552,7 +551,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -564,7 +563,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
personality: Some(Personality::Pragmatic), personality: Some(Personality::Pragmatic),
..Default::default() ..Default::default()
}, },
@@ -572,7 +571,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
.await?; .await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
test.session_configured.model.clone(), test.session_configured.model.clone(),
@@ -702,7 +701,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await; wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await;
test.codex test.codex
.submit(read_only_text_turn_with_personality( .start_or_steer_turn(read_only_text_turn_with_personality(
&test, &test,
"hello", "hello",
remote_slug.to_string(), remote_slug.to_string(),
@@ -830,7 +829,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await; wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
remote_slug.to_string(), remote_slug.to_string(),
@@ -842,7 +841,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&test.codex, &test.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
personality: Some(Personality::Friendly), personality: Some(Personality::Friendly),
..Default::default() ..Default::default()
}, },
@@ -850,7 +849,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
.await?; .await?;
test.codex test.codex
.submit(read_only_text_turn( .start_or_steer_turn(read_only_text_turn(
&test, &test,
"hello", "hello",
remote_slug.to_string(), remote_slug.to_string(),
+55 -112
View File
@@ -5,6 +5,7 @@ use std::time::Duration;
use std::time::Instant; use std::time::Instant;
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core_plugins::store::PluginStore; use codex_core_plugins::store::PluginStore;
use codex_extension_api::ExtensionRegistry; use codex_extension_api::ExtensionRegistry;
@@ -16,10 +17,13 @@ use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
use codex_model_provider_info::OPENAI_PROVIDER_ID; use codex_model_provider_info::OPENAI_PROVIDER_ID;
use codex_plugin::PluginId; use codex_plugin::PluginId;
use codex_protocol::auth::AuthMode; use codex_protocol::auth::AuthMode;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_skills_extension::SkillsExtensionConfig; use codex_skills_extension::SkillsExtensionConfig;
use codex_skills_extension::install; use codex_skills_extension::install;
@@ -415,30 +419,27 @@ async fn persisted_remote_plugin_command_attribution_flows_through_turn_context(
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::read_only(), cwd.as_path()); turn_permission_fields(PermissionProfile::read_only(), cwd.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![codex_protocol::user_input::UserInput::Text { TurnInputRequest::user_input(vec![codex_protocol::user_input::UserInput::Text {
text: "run the remote plugin script".into(), text: "run the remote plugin script".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd)), environments: Some(local_selections(cwd)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let begin = wait_for_event_match(&codex, |event| match event { let begin = wait_for_event_match(&codex, |event| match event {
@@ -503,16 +504,10 @@ async fn agent_plugin_skills_use_shared_catalog_and_direct_child_discovery() ->
test_codex test_codex
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Skill {
items: vec![UserInput::Skill { name: "acme.tools:review".into(),
name: "acme.tools:review".into(), path: skill_path,
path: skill_path, }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let warning = wait_for_event(&test_codex.codex, |ev| { let warning = wait_for_event(&test_codex.codex, |ev| {
matches!( matches!(
@@ -618,16 +613,10 @@ async fn plugin_skill_product_policy_and_migrated_command_precedence_reach_agent
); );
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Inspect the available plugin skills.".to_string(),
text: "Inspect the available plugin skills.".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -675,16 +664,10 @@ async fn legacy_plugin_skill_prompt_remains_complete() -> Result<()> {
test_codex test_codex
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Skill {
items: vec![UserInput::Skill { name: "sample:sample-search".into(),
name: "sample:sample-search".into(), path: skill_path,
path: skill_path, }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test_codex.codex, |ev| { wait_for_event(&test_codex.codex, |ev| {
matches!(ev, EventMsg::TurnComplete(_)) matches!(ev, EventMsg::TurnComplete(_))
@@ -776,16 +759,10 @@ async fn agent_plugin_root_mcp_stdio_tool_round_trip_expands_reserved_paths() ->
test_codex test_codex
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "call the Agent Plugin echo tool".into(),
text: "call the Agent Plugin echo tool".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let end = wait_for_event(&test_codex.codex, |event| { let end = wait_for_event(&test_codex.codex, |event| {
matches!(event, EventMsg::McpToolCallEnd(_)) matches!(event, EventMsg::McpToolCallEnd(_))
@@ -890,16 +867,12 @@ async fn agent_turns_route_curated_plugin_skills_after_auth_switch() -> Result<(
.await? .await?
.thread; .thread;
thread thread
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![
items: vec![codex_protocol::user_input::UserInput::Text { codex_protocol::user_input::UserInput::Text {
text: prompt.to_string(), text: prompt.to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], },
final_output_json_schema: None, ]))
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&thread, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -1106,16 +1079,12 @@ async fn explicit_plugin_mentions_use_apps_for_chatgpt_dual_surface_plugins(
wait_for_mcp_server(&codex, CODEX_APPS_MCP_SERVER_NAME).await?; wait_for_mcp_server(&codex, CODEX_APPS_MCP_SERVER_NAME).await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![
items: vec![codex_protocol::user_input::UserInput::Mention { codex_protocol::user_input::UserInput::Mention {
name: "sample".into(), name: "sample".into(),
path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"),
}], },
final_output_json_schema: None, ]))
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1190,16 +1159,12 @@ async fn explicit_plugin_mentions_keep_non_conflicting_mcp_for_chatgpt_auth() ->
wait_for_mcp_server(&codex, "sample").await?; wait_for_mcp_server(&codex, "sample").await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![
items: vec![codex_protocol::user_input::UserInput::Mention { codex_protocol::user_input::UserInput::Mention {
name: "sample".into(), name: "sample".into(),
path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"),
}], },
final_output_json_schema: None, ]))
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1291,13 +1256,7 @@ async fn explicitly_requested_mcp_waits_for_startup(request: ExplicitMcpRequest)
}, },
}; };
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![input]))
items: vec![input],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
tokio::time::sleep(Duration::from_millis(1200)).await; tokio::time::sleep(Duration::from_millis(1200)).await;
assert!( assert!(
@@ -1372,16 +1331,12 @@ async fn explicit_plugin_mentions_track_plugin_used_analytics() -> Result<()> {
let codex = Arc::clone(&test_codex.codex); let codex = Arc::clone(&test_codex.codex);
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![
items: vec![codex_protocol::user_input::UserInput::Mention { codex_protocol::user_input::UserInput::Mention {
name: "sample".into(), name: "sample".into(),
path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"),
}], },
final_output_json_schema: None, ]))
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1427,16 +1382,10 @@ async fn explicit_plugin_skill_invocation_tracks_remote_plugin_id() -> Result<()
let codex = Arc::clone(&test_codex.codex); let codex = Arc::clone(&test_codex.codex);
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Skill {
items: vec![UserInput::Skill { name: "sample:sample-search".into(),
name: "sample:sample-search".into(), path: skill_path,
path: skill_path, }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1512,16 +1461,10 @@ async fn implicit_plugin_skill_invocation_tracks_remote_plugin_id(
let codex = Arc::clone(&test_codex.codex); let codex = Arc::clone(&test_codex.codex);
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "inspect the sample skill".into(),
text: "inspect the sample skill".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+86 -160
View File
@@ -3,6 +3,7 @@
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use codex_core::TurnInputRequest;
use codex_core::shell::default_user_shell; use codex_core::shell::default_user_shell;
use codex_features::Feature; use codex_features::Feature;
use codex_prompts::APPLY_PATCH_TOOL_INSTRUCTIONS; use codex_prompts::APPLY_PATCH_TOOL_INSTRUCTIONS;
@@ -17,7 +18,7 @@ use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::TempDirExt; use core_test_support::TempDirExt;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
@@ -167,30 +168,18 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
let base_instructions = model_info.get_model_instructions(config.personality); let base_instructions = model_info.get_model_instructions(config.personality);
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -261,30 +250,18 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -340,30 +317,18 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -442,16 +407,10 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
// First turn // First turn
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -467,7 +426,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
.expect("workspace profile should have legacy projection"); .expect("workspace profile should have legacy projection");
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&codex, &codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile: Some(permission_profile), permission_profile: Some(permission_profile),
@@ -480,16 +439,10 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
// Second turn after overrides // Second turn after overrides
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 2".into(),
text: "hello 2".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -569,7 +522,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&codex, &codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
model: Some("gpt-5.4".to_string()), model: Some("gpt-5.4".to_string()),
effort: Some(Some(ReasoningEffort::Low)), effort: Some(Some(ReasoningEffort::Low)),
@@ -580,16 +533,10 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "first message".into(),
text: "first message".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -728,16 +675,10 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
// First turn // First turn
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "hello 1".into(),
text: "hello 1".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -753,15 +694,12 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, new_cwd.path()); turn_permission_fields(permission_profile, new_cwd.path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello 2".into(), text: "hello 2".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(new_cwd.abs())), environments: Some(local_selections(new_cwd.abs())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
@@ -770,8 +708,8 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
effort: Some(Some(ReasoningEffort::High)), effort: Some(Some(ReasoningEffort::High)),
summary: Some(ReasoningSummary::Detailed), summary: Some(ReasoningSummary::Detailed),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -875,58 +813,52 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
let default_summary = config.model_reasoning_summary; let default_summary = config.model_reasoning_summary;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello 1".into(), text: "hello 1".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(default_cwd.clone())), environments: Some(local_selections(default_cwd.clone())),
approval_policy: Some(default_approval_policy), approval_policy: Some(default_approval_policy),
sandbox_policy: Some(default_sandbox_policy.clone()), sandbox_policy: Some(default_sandbox_policy.clone()),
summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: default_model.clone(), model: default_model.clone(),
reasoning_effort: default_effort.clone(), reasoning_effort: default_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello 2".into(), text: "hello 2".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(default_cwd.clone())), environments: Some(local_selections(default_cwd.clone())),
approval_policy: Some(default_approval_policy), approval_policy: Some(default_approval_policy),
sandbox_policy: Some(default_sandbox_policy.clone()), sandbox_policy: Some(default_sandbox_policy.clone()),
summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: default_model.clone(), model: default_model.clone(),
reasoning_effort: default_effort, reasoning_effort: default_effort,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1014,61 +946,55 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
let default_summary = config.model_reasoning_summary; let default_summary = config.model_reasoning_summary;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello 1".into(), text: "hello 1".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(default_cwd.clone())), environments: Some(local_selections(default_cwd.clone())),
approval_policy: Some(default_approval_policy), approval_policy: Some(default_approval_policy),
sandbox_policy: Some(default_sandbox_policy.clone()), sandbox_policy: Some(default_sandbox_policy.clone()),
summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: default_model, model: default_model,
reasoning_effort: default_effort, reasoning_effort: default_effort,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, default_cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, default_cwd.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello 2".into(), text: "hello 2".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(default_cwd.clone())), environments: Some(local_selections(default_cwd.clone())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
summary: Some(ReasoningSummary::Detailed), summary: Some(ReasoningSummary::Detailed),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: "o3".to_string(), model: "o3".to_string(),
reasoning_effort: Some(ReasoningEffort::High), reasoning_effort: Some(ReasoningEffort::High),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+5 -11
View File
@@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_response_created; use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_once;
@@ -40,16 +40,10 @@ async fn quota_exceeded_emits_single_error_event() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "quota?".into(),
text: "quota?".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
@@ -2,6 +2,7 @@ use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use chrono::Utc; use chrono::Utc;
use codex_config::config_toml::RealtimeWsVersion; use codex_config::config_toml::RealtimeWsVersion;
use codex_core::TurnInputRequest;
use codex_core::test_support::auth_manager_from_auth; use codex_core::test_support::auth_manager_from_auth;
use codex_history::InitialHistory; use codex_history::InitialHistory;
use codex_history::RolloutItem; use codex_history::RolloutItem;
@@ -3054,16 +3055,10 @@ async fn conversation_user_text_turn_is_not_sent_to_realtime() -> Result<()> {
let user_text = "typed follow-up for realtime"; let user_text = "typed follow-up for realtime";
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: user_text.to_string(),
text: user_text.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let turn_complete = wait_for_event_match(&test.codex, |event| match event { let turn_complete = wait_for_event_match(&test.codex, |event| match event {
@@ -4695,16 +4690,10 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> {
.await; .await;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "first prompt".to_string(),
text: "first prompt".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
+62 -97
View File
@@ -6,6 +6,7 @@ use codex_api::AuthProvider;
use codex_config::types::ApprovalsReviewer; use codex_config::types::ApprovalsReviewer;
use codex_core::EnvironmentConfig; use codex_core::EnvironmentConfig;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
use codex_core::WaitForEnvironmentToolConfig; use codex_core::WaitForEnvironmentToolConfig;
use codex_core::compact::SUMMARIZATION_PROMPT; use codex_core::compact::SUMMARIZATION_PROMPT;
use codex_core::config::Config; use codex_core::config::Config;
@@ -39,6 +40,9 @@ use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy; use codex_http_client::OutboundProxyPolicy;
use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxPermissions; use codex_protocol::models::SandboxPermissions;
@@ -206,30 +210,27 @@ async fn submit_turn_with_approval_and_environments(
environments, environments,
); );
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(turn_environment_selections), environments: Some(turn_environment_selections),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::User), approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy: Some(SandboxPolicy::new_read_only_policy()), sandbox_policy: Some(SandboxPolicy::new_read_only_policy()),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(()) Ok(())
@@ -540,16 +541,10 @@ async fn settings_update_does_not_retarget_active_turn_environment() -> Result<(
let next_cwd = next_workspace.path().abs(); let next_cwd = next_workspace.path().abs();
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "pause before continuing".into(),
text: "pause before continuing".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let request = wait_for_event_match(&test.codex, |event| match event { let request = wait_for_event_match(&test.codex, |event| match event {
EventMsg::RequestUserInput(request) => Some(request.clone()), EventMsg::RequestUserInput(request) => Some(request.clone()),
@@ -676,22 +671,19 @@ async fn deferred_executor_promotes_primary_environment_when_startup_completes()
) )
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "wait for the primary environment".into(), text: "wait for the primary environment".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new( environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(), test.config.cwd.clone(),
vec![remote_selection, local_selection], vec![remote_selection, local_selection],
)), )),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let request = wait_for_event_match(&test.codex, |event| match event { let request = wait_for_event_match(&test.codex, |event| match event {
EventMsg::RequestUserInput(request) => Some(request.clone()), EventMsg::RequestUserInput(request) => Some(request.clone()),
@@ -1062,13 +1054,10 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
} }
thread thread
.submit( .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
vec![UserInput::Text { text: prompt.to_string(),
text: prompt.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}]
.into(),
)
.await?; .await?;
wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await;
} }
@@ -1359,15 +1348,12 @@ async fn deferred_executor_stays_pending_after_materialization() -> Result<()> {
)?; )?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "wait for the environment".into(), text: "wait for the environment".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new( environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(), test.config.cwd.clone(),
vec![TurnEnvironmentSelection { vec![TurnEnvironmentSelection {
@@ -1377,8 +1363,8 @@ async fn deferred_executor_stays_pending_after_materialization() -> Result<()> {
}], }],
)), )),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_response_request_count(&response_mock, /*expected_count*/ 1).await; wait_for_response_request_count(&response_mock, /*expected_count*/ 1).await;
assert_eq!(provider.calls.load(Ordering::Relaxed), 0); assert_eq!(provider.calls.load(Ordering::Relaxed), 0);
@@ -1503,22 +1489,19 @@ async fn deferred_executor_spawn_agent_inherits_ready_step_environments(
let mut created_threads = test.thread_manager.subscribe_thread_created(); let mut created_threads = test.thread_manager.subscribe_thread_created();
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "spawn after the environment becomes ready".into(), text: "spawn after the environment becomes ready".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new( environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(), test.config.cwd.clone(),
expected_environments.clone(), expected_environments.clone(),
)), )),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_response_request_count(&response_mock, /*expected_count*/ 1).await; wait_for_response_request_count(&response_mock, /*expected_count*/ 1).await;
attach_tx.send(()).expect("attach remote environment"); attach_tx.send(()).expect("attach remote environment");
@@ -1655,15 +1638,12 @@ async fn deferred_executor_guardian_uses_newly_ready_step_environment() -> Resul
turn_permission_fields(permission_profile, test.config.cwd.as_path()); turn_permission_fields(permission_profile, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "review a command after the remote environment becomes ready".into(), text: "review a command after the remote environment becomes ready".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new( environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(), test.config.cwd.clone(),
vec![remote_selection, local(local_cwd.clone())], vec![remote_selection, local(local_cwd.clone())],
@@ -1671,8 +1651,8 @@ async fn deferred_executor_guardian_uses_newly_ready_step_environment() -> Resul
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_response_request_count(&responses, /*expected_count*/ 1).await; wait_for_response_request_count(&responses, /*expected_count*/ 1).await;
attach_tx.send(()).expect("attach remote environment"); attach_tx.send(()).expect("attach remote environment");
@@ -1776,16 +1756,10 @@ async fn deferred_executor_loads_agents_md_when_environment_becomes_ready() -> R
.context("thread startup should not wait for the remote environment")??; .context("thread startup should not wait for the remote environment")??;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "load the environment instructions".into(),
text: "load the environment instructions".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_response_request_count(&response_mock, /*expected_count*/ 1).await; wait_for_response_request_count(&response_mock, /*expected_count*/ 1).await;
let agents_path = PathUri::from_abs_path(&test.config.cwd).join("AGENTS.md")?; let agents_path = PathUri::from_abs_path(&test.config.cwd).join("AGENTS.md")?;
@@ -1890,16 +1864,10 @@ async fn deferred_executor_compaction_preserves_then_updates_environment_once()
.context("thread startup should not wait for the remote environment")??; .context("thread startup should not wait for the remote environment")??;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "wait for the environment".into(),
text: "wait for the environment".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let request = wait_for_event_match(&test.codex, |event| match event { let request = wait_for_event_match(&test.codex, |event| match event {
EventMsg::RequestUserInput(request) => Some(request.clone()), EventMsg::RequestUserInput(request) => Some(request.clone()),
@@ -2269,15 +2237,12 @@ async fn remote_exec_materializes_target_roots_before_sandbox_selection() -> Res
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.config.cwd.as_path()); turn_permission_fields(permission_profile, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "try to read the denied remote workspace root".into(), text: "try to read the denied remote workspace root".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new( environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(), test.config.cwd.clone(),
vec![ vec![
@@ -2296,17 +2261,17 @@ async fn remote_exec_materializes_target_roots_before_sandbox_selection() -> Res
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+42 -80
View File
@@ -1,5 +1,6 @@
#![cfg(not(target_os = "windows"))] #![cfg(not(target_os = "windows"))]
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::built_in_model_providers; use codex_model_provider_info::built_in_model_providers;
@@ -22,7 +23,7 @@ use codex_protocol::openai_models::default_input_modalities;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandSource; use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::TempDirExt; use core_test_support::TempDirExt;
use core_test_support::load_default_config_for_test; use core_test_support::load_default_config_for_test;
@@ -206,16 +207,10 @@ async fn remote_models_config_context_window_override_clamps_to_max_context_wind
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "check context window".into(),
text: "check context window".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let turn_started_event = wait_for_event(&codex, |event| { let turn_started_event = wait_for_event(&codex, |event| {
@@ -273,16 +268,10 @@ async fn remote_models_config_override_above_max_uses_max_context_window() -> Re
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "check context window".into(),
text: "check context window".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let turn_started_event = wait_for_event(&codex, |event| { let turn_started_event = wait_for_event(&codex, |event| {
@@ -339,16 +328,10 @@ async fn remote_models_use_context_window_when_config_override_is_absent() -> Re
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "check context window".into(),
text: "check context window".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let turn_started_event = wait_for_event(&codex, |event| { let turn_started_event = wait_for_event(&codex, |event| {
@@ -418,16 +401,10 @@ async fn remote_models_long_model_slug_is_sent_with_custom_reasoning() -> Result
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "check model slug".into(),
text: "check model slug".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -468,16 +445,10 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning()
.await?; .await?;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "check namespaced model metadata".into(),
text: "check namespaced model metadata".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let mut fallback_warning_count = 0; let mut fallback_warning_count = 0;
@@ -601,7 +572,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&codex, &codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some(REMOTE_MODEL_SLUG.to_string()), model: Some(REMOTE_MODEL_SLUG.to_string()),
..Default::default() ..Default::default()
}, },
@@ -631,23 +602,20 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run call".into(), text: "run call".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd_path)), environments: Some(local_selections(cwd_path)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
summary: Some(ReasoningSummary::Auto), summary: Some(ReasoningSummary::Auto),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let begin_event = wait_for_event_match(&codex, |msg| match msg { let begin_event = wait_for_event_match(&codex, |msg| match msg {
@@ -876,30 +844,27 @@ async fn remote_models_apply_legacy_instructions() -> Result<()> {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello base".into(), text: "hello base".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd_path.clone())), environments: Some(local_selections(cwd_path.clone())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
summary: Some(ReasoningSummary::Auto), summary: Some(ReasoningSummary::Auto),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&codex, &codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some(model.to_string()), model: Some(model.to_string()),
..Default::default() ..Default::default()
}, },
@@ -909,23 +874,20 @@ async fn remote_models_apply_legacy_instructions() -> Result<()> {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "hello remote".into(), text: "hello remote".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd_path)), environments: Some(local_selections(cwd_path)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
summary: Some(ReasoningSummary::Auto), summary: Some(ReasoningSummary::Auto),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -1,9 +1,9 @@
#![cfg(not(target_os = "windows"))] #![cfg(not(target_os = "windows"))]
use codex_core::TurnInputRequest;
use codex_features::Feature; use codex_features::Feature;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created; use core_test_support::responses::ev_response_created;
@@ -39,16 +39,10 @@ async fn request_body_is_zstd_compressed_for_codex_backend_when_enabled() -> any
let codex = builder.build(&server).await?.codex; let codex = builder.build(&server).await?.codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "compress me".into(),
text: "compress me".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
// Wait until the task completes so the request definitely hit the server. // Wait until the task completes so the request definitely hit the server.
@@ -89,16 +83,10 @@ async fn request_body_is_not_compressed_for_api_key_auth_even_when_enabled() ->
let codex = builder.build(&server).await?.codex; let codex = builder.build(&server).await?.codex;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "do not compress".into(),
text: "do not compress".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1,10 +1,14 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_core::sandboxing::SandboxPermissions; use codex_core::sandboxing::SandboxPermissions;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile;
use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile as CorePermissionProfile; use codex_protocol::models::PermissionProfile as CorePermissionProfile;
@@ -15,6 +19,7 @@ use codex_protocol::protocol::ExecApprovalRequestEvent;
use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -175,31 +180,28 @@ async fn submit_turn(
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.cwd.path()); turn_permission_fields(permission_profile, test.cwd.path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::User), approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(()) Ok(())
} }
@@ -2,9 +2,13 @@
#![cfg(target_os = "macos")] #![cfg(target_os = "macos")]
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
@@ -12,6 +16,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -144,31 +149,28 @@ async fn submit_turn(
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.config.cwd.as_path()); turn_permission_fields(permission_profile, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(test.config.cwd.clone())), environments: Some(local_selections(test.config.cwd.clone())),
approval_policy: Some(approval_policy), approval_policy: Some(approval_policy),
approvals_reviewer, approvals_reviewer,
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(()) Ok(())
} }
@@ -5,6 +5,7 @@ use anyhow::Result;
use codex_config::types::ToolSuggestDisabledTool; use codex_config::types::ToolSuggestDisabledTool;
use codex_config::types::ToolSuggestDiscoverable; use codex_config::types::ToolSuggestDiscoverable;
use codex_config::types::ToolSuggestDiscoverableType; use codex_config::types::ToolSuggestDiscoverableType;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core_plugins::startup_sync::curated_plugins_repo_path; use codex_core_plugins::startup_sync::curated_plugins_repo_path;
use codex_features::Feature; use codex_features::Feature;
@@ -213,21 +214,18 @@ async fn start_gated_step_preparation(test: &TestCodex, server: &MockServer) ->
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "prepare MCP and plugin recommendations".to_string(), text: "prepare MCP and plugin recommendations".to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let fs = test.fs(); let fs = test.fs();
@@ -262,15 +260,12 @@ async fn start_install_turn(test: &TestCodex, prompt: &str) -> Result<Elicitatio
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.to_string(), text: prompt.to_string(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
@@ -283,8 +278,8 @@ async fn start_install_turn(test: &TestCodex, prompt: &str) -> Result<Elicitatio
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
Ok(wait_for_event_match(&test.codex, |event| match event { Ok(wait_for_event_match(&test.codex, |event| match event {
+20 -27
View File
@@ -1,5 +1,6 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use codex_core::TurnInputRequest;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
use std::collections::HashMap; use std::collections::HashMap;
@@ -11,6 +12,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputResponse; use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -130,15 +132,12 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
turn_permission_fields(PermissionProfile::Disabled, cwd.path()); turn_permission_fields(PermissionProfile::Disabled, cwd.path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "please confirm".into(), text: "please confirm".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd.abs())), environments: Some(local_selections(cwd.abs())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
@@ -152,8 +151,8 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let request = wait_for_event_match(&codex, |event| match event { let request = wait_for_event_match(&codex, |event| match event {
@@ -275,15 +274,12 @@ async fn request_user_input_interrupt_emits_deferred_token_count() -> anyhow::Re
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd.path()); turn_permission_fields(PermissionProfile::Disabled, cwd.path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "please confirm".into(), text: "please confirm".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd.abs())), environments: Some(local_selections(cwd.abs())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
@@ -297,8 +293,8 @@ async fn request_user_input_interrupt_emits_deferred_token_count() -> anyhow::Re
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let request = wait_for_event_match(&codex, |event| match event { let request = wait_for_event_match(&codex, |event| match event {
@@ -379,23 +375,20 @@ where
turn_permission_fields(PermissionProfile::Disabled, cwd.path()); turn_permission_fields(PermissionProfile::Disabled, cwd.path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "please confirm".into(), text: "please confirm".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd.abs())), environments: Some(local_selections(cwd.abs())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(collaboration_mode), collaboration_mode: Some(collaboration_mode),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -3,11 +3,15 @@
use anyhow::Result; use anyhow::Result;
use anyhow::anyhow; use anyhow::anyhow;
use codex_core::TurnInputRequest;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ResponseMock; use core_test_support::responses::ResponseMock;
use core_test_support::responses::ResponsesRequest; use core_test_support::responses::ResponsesRequest;
@@ -157,30 +161,27 @@ async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()>
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::workspace_write(), cwd.as_path()); turn_permission_fields(PermissionProfile::workspace_write(), cwd.as_path());
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd)), environments: Some(local_selections(cwd)),
approval_policy: Some(AskForApproval::OnRequest), approval_policy: Some(AskForApproval::OnRequest),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let turn_started = wait_for_event_result(test, "turn started", |event| { let turn_started = wait_for_event_result(test, "turn started", |event| {
+11 -16
View File
@@ -3,6 +3,7 @@ use std::sync::Arc;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_extension_api::ExtensionRegistry; use codex_extension_api::ExtensionRegistry;
use codex_extension_api::ExtensionRegistryBuilder; use codex_extension_api::ExtensionRegistryBuilder;
@@ -248,22 +249,16 @@ async fn responses_lite_prepares_images() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![
items: vec![ UserInput::Image {
UserInput::Image { image_url: image_url.to_string(),
image_url: image_url.to_string(), detail: Some(ImageDetail::Original),
detail: Some(ImageDetail::Original), },
}, UserInput::Image {
UserInput::Image { image_url: remote_image_url.to_string(),
image_url: remote_image_url.to_string(), detail: Some(ImageDetail::High),
detail: Some(ImageDetail::High), },
}, ]))
],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+31 -72
View File
@@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::ByteRange; use codex_protocol::user_input::ByteRange;
use codex_protocol::user_input::TextElement; use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
@@ -40,16 +41,10 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> {
)]; )];
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Record some messages".into(),
text: "Record some messages".into(), text_elements: text_elements.clone(),
text_elements: text_elements.clone(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -101,16 +96,10 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()>
mount_sse_once(&server, initial_sse).await; mount_sse_once(&server, initial_sse).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Record reasoning messages".into(),
text: "Record reasoning messages".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
@@ -164,16 +153,10 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
let initial_mock = mount_sse_once(&server, initial_sse).await; let initial_mock = mount_sse_once(&server, initial_sse).await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Record initial instructions".into(),
text: "Record initial instructions".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let initial_body = initial_mock.single_request().body_json(); let initial_body = initial_mock.single_request().body_json();
@@ -206,16 +189,10 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
let resumed = resume_builder.restart(&server, &initial).await?; let resumed = resume_builder.restart(&server, &initial).await?;
resumed resumed
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Resume with different model".into(),
text: "Resume with different model".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&resumed.codex, |event| { wait_for_event(&resumed.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -224,16 +201,10 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
resumed resumed
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Second turn after resume".into(),
text: "Second turn after resume".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&resumed.codex, |event| { wait_for_event(&resumed.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -291,16 +262,10 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
) )
.await; .await;
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Record initial instructions".into(),
text: "Record initial instructions".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let _ = initial_mock.single_request(); let _ = initial_mock.single_request();
@@ -321,7 +286,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
let resumed = resume_builder.restart(&server, &initial).await?; let resumed = resume_builder.restart(&server, &initial).await?;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&resumed.codex, &resumed.codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
model: Some("gpt-5.4".to_string()), model: Some("gpt-5.4".to_string()),
..Default::default() ..Default::default()
}, },
@@ -329,16 +294,10 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
.await?; .await?;
resumed resumed
.codex .codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "first turn after override".into(),
text: "first turn after override".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&resumed.codex, |event| { wait_for_event(&resumed.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
+6 -11
View File
@@ -1,5 +1,6 @@
use codex_core::CodexThread; use codex_core::CodexThread;
use codex_core::REVIEW_PROMPT; use codex_core::REVIEW_PROMPT;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_core::config::Constrained; use codex_core::config::Constrained;
use codex_core::find_thread_path_by_id_str; use codex_core::find_thread_path_by_id_str;
@@ -1125,16 +1126,10 @@ async fn review_history_surfaces_in_parent_session() {
// 2) Continue in the parent session; request input must not include any review items. // 2) Continue in the parent session; request input must not include any review items.
let followup = "back to parent".to_string(); let followup = "back to parent".to_string();
codex codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: followup.clone(),
text: followup.clone(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await .await
.unwrap(); .unwrap();
let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1240,7 +1235,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() {
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&codex, &codex,
codex_protocol::protocol::ThreadSettingsOverrides { ThreadSettingsOverrides {
environments: Some(local_selections(repo_path.to_path_buf().abs())), environments: Some(local_selections(repo_path.to_path_buf().abs())),
..Default::default() ..Default::default()
}, },
+47 -47
View File
@@ -22,6 +22,7 @@ use codex_config::types::McpServerConfig;
use codex_config::types::McpServerEnvVar; use codex_config::types::McpServerEnvVar;
use codex_config::types::McpServerTransportConfig; use codex_config::types::McpServerTransportConfig;
use codex_config::types::OAuthCredentialsStoreMode; use codex_config::types::OAuthCredentialsStoreMode;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::Environment; use codex_exec_server::Environment;
@@ -37,7 +38,10 @@ use codex_models_manager::manager::RefreshStrategy;
use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::LegacyAppPathString;
use codex_history::RolloutItem; use codex_history::RolloutItem;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
@@ -55,6 +59,7 @@ use codex_protocol::protocol::McpStartupFailureReason;
use codex_protocol::protocol::McpStartupStatus; use codex_protocol::protocol::McpStartupStatus;
use codex_protocol::protocol::McpToolCallBeginEvent; use codex_protocol::protocol::McpToolCallBeginEvent;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_utils_cargo_bin::cargo_bin; use codex_utils_cargo_bin::cargo_bin;
use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUri;
@@ -115,7 +120,7 @@ fn assert_wall_time_header(output: &str) {
assert_eq!(marker, "Output:"); assert_eq!(marker, "Output:");
} }
fn read_only_user_turn(fixture: &TestCodex, text: impl Into<String>) -> Op { fn read_only_user_turn(fixture: &TestCodex, text: impl Into<String>) -> TurnInputRequest {
read_only_user_turn_with_model(fixture, text, fixture.session_configured.model.clone()) read_only_user_turn_with_model(fixture, text, fixture.session_configured.model.clone())
} }
@@ -123,11 +128,11 @@ fn read_only_user_turn_with_model(
fixture: &TestCodex, fixture: &TestCodex,
text: impl Into<String>, text: impl Into<String>,
model: String, model: String,
) -> Op { ) -> TurnInputRequest {
user_turn_with_permission_profile(fixture, text, model, PermissionProfile::read_only()) user_turn_with_permission_profile(fixture, text, model, PermissionProfile::read_only())
} }
fn auto_approved_user_turn(fixture: &TestCodex, text: impl Into<String>) -> Op { fn auto_approved_user_turn(fixture: &TestCodex, text: impl Into<String>) -> TurnInputRequest {
user_turn_with_permission_profile( user_turn_with_permission_profile(
fixture, fixture,
text, text,
@@ -141,33 +146,28 @@ fn user_turn_with_permission_profile(
text: impl Into<String>, text: impl Into<String>,
model: String, model: String,
permission_profile: PermissionProfile, permission_profile: PermissionProfile,
) -> Op { ) -> TurnInputRequest {
let cwd = fixture.config.cwd.clone(); let cwd = fixture.config.cwd.clone();
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, cwd.as_path()); turn_permission_fields(permission_profile, cwd.as_path());
Op::UserInput { TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.into(),
text: text.into(), text_elements: Vec::new(),
text_elements: Vec::new(), }])
}], .with_thread_settings(ThreadSettingsOverrides {
final_output_json_schema: None, approval_policy: Some(AskForApproval::Never),
responsesapi_client_metadata: None, sandbox_policy: Some(sandbox_policy),
additional_context: Default::default(), permission_profile,
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { collaboration_mode: Some(CollaborationMode {
approval_policy: Some(AskForApproval::Never), mode: ModeKind::Default,
sandbox_policy: Some(sandbox_policy), settings: Settings {
permission_profile, model,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { reasoning_effort: None,
mode: codex_protocol::config_types::ModeKind::Default, developer_instructions: None,
settings: codex_protocol::config_types::Settings { },
model, }),
reasoning_effort: None, ..Default::default()
developer_instructions: None, })
},
}),
..Default::default()
},
}
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@@ -390,7 +390,7 @@ async fn call_structured_tool(
fixture fixture
.codex .codex
.submit(read_only_user_turn(fixture, "call the requested rmcp tool")) .start_or_steer_turn(read_only_user_turn(fixture, "call the requested rmcp tool"))
.await?; .await?;
wait_for_event(&fixture.codex, |ev| { wait_for_event(&fixture.codex, |ev| {
@@ -540,7 +540,7 @@ async fn mcp_namespace_instructions_are_preserved_without_hiding_tools() -> anyh
fixture fixture
.codex .codex
.submit(read_only_user_turn(&fixture, "show the bounded MCP tools")) .start_or_steer_turn(read_only_user_turn(&fixture, "show the bounded MCP tools"))
.await?; .await?;
wait_for_event(&fixture.codex, |event| { wait_for_event(&fixture.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)) matches!(event, EventMsg::TurnComplete(_))
@@ -650,7 +650,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
fixture fixture
.codex .codex
.submit(read_only_user_turn(&fixture, "call the rmcp echo tool")) .start_or_steer_turn(read_only_user_turn(&fixture, "call the rmcp echo tool"))
.await?; .await?;
let begin_event = wait_for_event(&fixture.codex, |ev| { let begin_event = wait_for_event(&fixture.codex, |ev| {
@@ -951,7 +951,7 @@ async fn modern_mcp_pagination_preserves_valid_tools_and_rejects_oversized_curso
fixture fixture
.codex .codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&fixture, &fixture,
"show the paginated MCP tools", "show the paginated MCP tools",
)) ))
@@ -1159,7 +1159,7 @@ async fn interrupt_during_mcp_startup_preserves_user_input_in_history(
let prompt = "keep this interrupted prompt in conversation history"; let prompt = "keep this interrupted prompt in conversation history";
fixture fixture
.codex .codex
.submit(read_only_user_turn(&fixture, prompt)) .start_or_steer_turn(read_only_user_turn(&fixture, prompt))
.await?; .await?;
wait_for_event(&fixture.codex, |event| { wait_for_event(&fixture.codex, |event| {
matches!(event, EventMsg::TurnStarted(_)) matches!(event, EventMsg::TurnStarted(_))
@@ -1546,7 +1546,7 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow::
// Keep this baseline on the mutable sync tool so read-only hints do not // Keep this baseline on the mutable sync tool so read-only hints do not
// make the call parallel-safe. Bypass read-only turn permissions so // make the call parallel-safe. Bypass read-only turn permissions so
// approval behavior does not block the scheduling assertion. // approval behavior does not block the scheduling assertion.
.submit(auto_approved_user_turn( .start_or_steer_turn(auto_approved_user_turn(
&fixture, &fixture,
"call the rmcp sync tool twice", "call the rmcp sync tool twice",
)) ))
@@ -1684,7 +1684,7 @@ async fn stdio_mcp_read_only_tool_calls_run_concurrently_without_server_opt_in()
fixture fixture
.codex .codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&fixture, &fixture,
"call the rmcp sync_readonly tool twice", "call the rmcp sync_readonly tool twice",
)) ))
@@ -1777,7 +1777,7 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res
// Exercise the server opt-in with the mutable sync tool rather than the // Exercise the server opt-in with the mutable sync tool rather than the
// read-only sync_readonly tool. Bypass read-only turn permissions so // read-only sync_readonly tool. Bypass read-only turn permissions so
// approval behavior does not block the scheduling assertion. // approval behavior does not block the scheduling assertion.
.submit(auto_approved_user_turn( .start_or_steer_turn(auto_approved_user_turn(
&fixture, &fixture,
"call the rmcp sync tool twice", "call the rmcp sync tool twice",
)) ))
@@ -1857,7 +1857,7 @@ async fn stdio_encrypted_content_responses_round_trip() -> anyhow::Result<()> {
fixture fixture
.codex .codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&fixture, &fixture,
"call the rmcp encrypted output tool", "call the rmcp encrypted output tool",
)) ))
@@ -1956,7 +1956,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
fixture fixture
.codex .codex
.submit(read_only_user_turn(&fixture, "call the rmcp image tool")) .start_or_steer_turn(read_only_user_turn(&fixture, "call the rmcp image tool"))
.await?; .await?;
// Wait for tool begin/end and final completion. // Wait for tool begin/end and final completion.
@@ -2113,7 +2113,7 @@ async fn stdio_image_responses_resize_large_image() -> anyhow::Result<()> {
fixture fixture
.codex .codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&fixture, &fixture,
"call the rmcp image_scenario tool", "call the rmcp image_scenario tool",
)) ))
@@ -2201,7 +2201,7 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re
fixture fixture
.codex .codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&fixture, &fixture,
"call the rmcp image_scenario tool", "call the rmcp image_scenario tool",
)) ))
@@ -2362,7 +2362,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
fixture fixture
.codex .codex
.submit(read_only_user_turn_with_model( .start_or_steer_turn(read_only_user_turn_with_model(
&fixture, &fixture,
"call the rmcp image tool", "call the rmcp image tool",
text_only_model_slug.to_string(), text_only_model_slug.to_string(),
@@ -2463,7 +2463,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
fixture fixture
.codex .codex
.submit(read_only_user_turn(&fixture, "call the rmcp echo tool")) .start_or_steer_turn(read_only_user_turn(&fixture, "call the rmcp echo tool"))
.await?; .await?;
let begin_event = wait_for_event(&fixture.codex, |ev| { let begin_event = wait_for_event(&fixture.codex, |ev| {
@@ -2588,7 +2588,7 @@ async fn stdio_server_propagates_explicit_local_env_var_source() -> anyhow::Resu
fixture fixture
.codex .codex
.submit(read_only_user_turn(&fixture, "call the rmcp echo tool")) .start_or_steer_turn(read_only_user_turn(&fixture, "call the rmcp echo tool"))
.await?; .await?;
wait_for_event(&fixture.codex, |ev| { wait_for_event(&fixture.codex, |ev| {
@@ -2684,7 +2684,7 @@ async fn remote_stdio_env_var_source_does_not_copy_local_env() -> anyhow::Result
fixture fixture
.codex .codex
.submit(read_only_user_turn(&fixture, "call the rmcp echo tool")) .start_or_steer_turn(read_only_user_turn(&fixture, "call the rmcp echo tool"))
.await?; .await?;
wait_for_event(&fixture.codex, |ev| { wait_for_event(&fixture.codex, |ev| {
@@ -2886,7 +2886,7 @@ async fn streamable_http_tool_call_round_trip(with_headers_helper: bool) -> anyh
// Phase 4: submit the user turn that should trigger the MCP tool call. // Phase 4: submit the user turn that should trigger the MCP tool call.
fixture fixture
.codex .codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&fixture, &fixture,
"call the rmcp streamable http echo tool", "call the rmcp streamable http echo tool",
)) ))
@@ -3317,7 +3317,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
store_lock.try_lock()?; store_lock.try_lock()?;
fixture fixture
.codex .codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&fixture, &fixture,
"continue while OAuth credentials are locked", "continue while OAuth credentials are locked",
)) ))
@@ -3397,7 +3397,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
.await; .await;
fixture fixture
.codex .codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&fixture, &fixture,
"continue while a newly discovered OAuth server is starting", "continue while a newly discovered OAuth server is starting",
)) ))
@@ -3466,7 +3466,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
// Phase 6: submit the user turn that should invoke the OAuth-backed tool. // Phase 6: submit the user turn that should invoke the OAuth-backed tool.
fixture fixture
.codex .codex
.submit(read_only_user_turn( .start_or_steer_turn(read_only_user_turn(
&fixture, &fixture,
"call the rmcp streamable http oauth echo tool", "call the rmcp streamable http oauth echo tool",
)) ))
+9 -20
View File
@@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_core::config::RolloutBudgetConfig; use codex_core::config::RolloutBudgetConfig;
use codex_features::Feature; use codex_features::Feature;
use codex_model_provider_info::built_in_model_providers; use codex_model_provider_info::built_in_model_providers;
@@ -141,16 +142,10 @@ async fn invalid_provider_rollout_budget_units_fail_without_retry() -> Result<()
.await?; .await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "reject invalid provider budget units".to_string(),
text: "reject invalid provider budget units".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let EventMsg::Error(error) = let EventMsg::Error(error) =
@@ -312,16 +307,10 @@ async fn exhausted_budget_fails_current_and_later_turns() -> Result<()> {
for prompt in ["exhaust the budget", "try another turn"] { for prompt in ["exhaust the budget", "try another turn"] {
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: prompt.to_string(),
text: prompt.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
+9 -21
View File
@@ -1,6 +1,6 @@
use anyhow::Ok; use anyhow::Ok;
use codex_core::TurnInputRequest;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SafetyBufferingEvent; use codex_protocol::protocol::SafetyBufferingEvent;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
@@ -48,16 +48,10 @@ async fn emits_safety_buffering_from_response_metadata_with_the_header_fallback_
let test = test_codex().build(&server).await?; let test = test_codex().build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Check this request".into(),
text: "Check this request".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let event = wait_for_event_match(&test.codex, |event| match event { let event = wait_for_event_match(&test.codex, |event| match event {
@@ -103,16 +97,10 @@ async fn emits_safety_buffering_with_the_responses_api_model_without_header_gati
let test = test_codex().build(&server).await?; let test = test_codex().build(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Check this request".into(),
text: "Check this request".into(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let event = wait_for_event_match(&test.codex, |event| match event { let event = wait_for_event_match(&test.codex, |event| match event {
@@ -1,4 +1,8 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
@@ -7,7 +11,7 @@ use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ModelRerouteReason; use codex_protocol::protocol::ModelRerouteReason;
use codex_protocol::protocol::ModelVerification; use codex_protocol::protocol::ModelVerification;
use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_function_call; use core_test_support::responses::ev_function_call;
@@ -35,33 +39,28 @@ const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber";
const CYBER_POLICY_MESSAGE: &str = const CYBER_POLICY_MESSAGE: &str =
"This request has been flagged for potentially high-risk cyber activity."; "This request has been flagged for potentially high-risk cyber activity.";
fn disabled_text_turn(test: &TestCodex, text: &str) -> Op { fn disabled_text_turn(test: &TestCodex, text: &str) -> TurnInputRequest {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.cwd_path()); turn_permission_fields(PermissionProfile::Disabled, test.cwd_path());
Op::UserInput { TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: text.to_string(),
text: text.to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }])
}], .with_thread_settings(ThreadSettingsOverrides {
final_output_json_schema: None, environments: Some(local_selections(test.config.cwd.clone())),
responsesapi_client_metadata: None, approval_policy: Some(AskForApproval::Never),
additional_context: Default::default(), sandbox_policy: Some(sandbox_policy),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { permission_profile,
environments: Some(local_selections(test.config.cwd.clone())), collaboration_mode: Some(CollaborationMode {
approval_policy: Some(AskForApproval::Never), mode: ModeKind::Default,
sandbox_policy: Some(sandbox_policy), settings: Settings {
permission_profile, model: test.session_configured.model.clone(),
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { reasoning_effort: test.config.model_reasoning_effort.clone(),
mode: codex_protocol::config_types::ModeKind::Default, developer_instructions: None,
settings: codex_protocol::config_types::Settings { },
model: test.session_configured.model.clone(), }),
reasoning_effort: test.config.model_reasoning_effort.clone(), ..Default::default()
developer_instructions: None, })
},
}),
..Default::default()
},
}
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -77,7 +76,7 @@ async fn openai_model_header_mismatch_emits_warning_event() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(disabled_text_turn(&test, "trigger safety check")) .start_or_steer_turn(disabled_text_turn(&test, "trigger safety check"))
.await?; .await?;
let reroute = wait_for_event(&test.codex, |event| { let reroute = wait_for_event(&test.codex, |event| {
@@ -125,7 +124,7 @@ async fn cyber_policy_response_emits_typed_error_without_retry() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(disabled_text_turn(&test, "trigger cyber policy error")) .start_or_steer_turn(disabled_text_turn(&test, "trigger cyber policy error"))
.await?; .await?;
let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await; let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await;
@@ -164,7 +163,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(disabled_text_turn(&test, "trigger response model check")) .start_or_steer_turn(disabled_text_turn(&test, "trigger response model check"))
.await?; .await?;
let reroute = wait_for_event(&test.codex, |event| { let reroute = wait_for_event(&test.codex, |event| {
@@ -234,7 +233,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(disabled_text_turn(&test, "trigger follow-up turn")) .start_or_steer_turn(disabled_text_turn(&test, "trigger follow-up turn"))
.await?; .await?;
let mut warning_count = 0; let mut warning_count = 0;
@@ -272,7 +271,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()>
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(disabled_text_turn(&test, "trigger casing check")) .start_or_steer_turn(disabled_text_turn(&test, "trigger casing check"))
.await?; .await?;
let mut reroute_count = 0; let mut reroute_count = 0;
@@ -315,7 +314,7 @@ async fn model_verification_emits_structured_event_without_reroute_or_warning()
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(disabled_text_turn(&test, "trigger model verification")) .start_or_steer_turn(disabled_text_turn(&test, "trigger model verification"))
.await?; .await?;
let mut verification_count = 0; let mut verification_count = 0;
@@ -391,7 +390,7 @@ async fn model_verification_only_emits_once_per_turn() -> Result<()> {
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
test.codex test.codex
.submit(disabled_text_turn( .start_or_steer_turn(disabled_text_turn(
&test, &test,
"trigger follow-up model verification", "trigger follow-up model verification",
)) ))
+17 -40
View File
@@ -4,6 +4,7 @@ use anyhow::Result;
use codex_config::types::McpServerConfig; use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig; use codex_config::types::McpServerTransportConfig;
use codex_core::StartThreadOptions; use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
use codex_core::config::Config; use codex_core::config::Config;
use codex_extension_api::ExtensionData; use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionRegistryBuilder; use codex_extension_api::ExtensionRegistryBuilder;
@@ -598,16 +599,10 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
let mut builder = configured_builder(apps_server.chatgpt_base_url.clone()); let mut builder = configured_builder(apps_server.chatgpt_base_url.clone());
let test = builder.build_with_auto_env(&server).await?; let test = builder.build_with_auto_env(&server).await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Find the calendar create tool".to_string(),
text: "Find the calendar create tool".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let EventMsg::McpToolCallBegin(begin) = wait_for_event(&test.codex, |event| { let EventMsg::McpToolCallBegin(begin) = wait_for_event(&test.codex, |event| {
@@ -1157,16 +1152,10 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
test.session_configured = new_thread.session_configured; test.session_configured = new_thread.session_configured;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Use the automation tool".to_string(),
text: "Use the automation tool".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let EventMsg::DynamicToolCallRequest(request) = wait_for_event(&test.codex, |event| { let EventMsg::DynamicToolCallRequest(request) = wait_for_event(&test.codex, |event| {
@@ -1489,16 +1478,10 @@ async fn tool_search_surfaced_mcp_tool_errors_are_returned_to_model() -> Result<
wait_for_mcp_server(&test.codex, "rmcp").await?; wait_for_mcp_server(&test.codex, "rmcp").await?;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Find the rmcp echo tool and call it.".to_string(),
text: "Find the rmcp echo tool and call it.".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
let EventMsg::McpToolCallEnd(end) = wait_for_event(&test.codex, |event| { let EventMsg::McpToolCallEnd(end) = wait_for_event(&test.codex, |event| {
@@ -1822,16 +1805,10 @@ async fn tool_search_matches_dynamic_tools_by_name_description_namespace_and_sch
test.session_configured = new_thread.session_configured; test.session_configured = new_thread.session_configured;
test.codex test.codex
.submit(Op::UserInput { .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
items: vec![UserInput::Text { text: "Search for the dynamic tool".to_string(),
text: "Search for the dynamic tool".to_string(), text_elements: Vec::new(),
text_elements: Vec::new(), }]))
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?; .await?;
wait_for_event(&test.codex, |event| { wait_for_event(&test.codex, |event| {
+41 -48
View File
@@ -1,13 +1,18 @@
use anyhow::Result; use anyhow::Result;
use codex_core::TurnInputRequest;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use codex_core::shell::get_shell_by_model_provided_path; use codex_core::shell::get_shell_by_model_provided_path;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandBeginEvent; use codex_protocol::protocol::ExecCommandBeginEvent;
use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::ExecCommandEndEvent;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed;
@@ -162,30 +167,27 @@ async fn run_snapshot_command_with_options(
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run unified exec with shell snapshot".into(), text: "run unified exec with shell snapshot".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd)), environments: Some(local_selections(cwd)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let begin = wait_for_event_match(&codex, |ev| match ev { let begin = wait_for_event_match(&codex, |ev| match ev {
@@ -260,30 +262,27 @@ async fn run_shell_command_snapshot_with_options(
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "run shell_command with shell snapshot".into(), text: "run shell_command with shell snapshot".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd)), environments: Some(local_selections(cwd)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let begin = wait_for_event_match(&codex, |ev| match ev { let begin = wait_for_event_match(&codex, |ev| match ev {
@@ -339,30 +338,27 @@ async fn run_tool_turn_on_harness(
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.into(), text: prompt.into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd)), environments: Some(local_selections(cwd)),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model: session_model, model: session_model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
wait_for_event_match(&codex, |ev| match ev { wait_for_event_match(&codex, |ev| match ev {
@@ -582,30 +578,27 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
codex codex
.submit(Op::UserInput { .start_or_steer_turn(
items: vec![UserInput::Text { TurnInputRequest::user_input(vec![UserInput::Text {
text: "apply patch via shell_command with snapshot".into(), text: "apply patch via shell_command with snapshot".into(),
text_elements: Vec::new(), text_elements: Vec::new(),
}], }])
final_output_json_schema: None, .with_thread_settings(ThreadSettingsOverrides {
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd.clone())), environments: Some(local_selections(cwd.clone())),
approval_policy: Some(AskForApproval::Never), approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy), sandbox_policy: Some(sandbox_policy),
permission_profile, permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { collaboration_mode: Some(CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default, mode: ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: Settings {
model, model,
reasoning_effort: None, reasoning_effort: None,
developer_instructions: None, developer_instructions: None,
}, },
}), }),
..Default::default() ..Default::default()
}, }),
}) )
.await?; .await?;
let mut saw_patch_begin = false; let mut saw_patch_begin = false;
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff. Show more