diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index 952da565a6..a7ba311803 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -27,7 +27,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null | null, developerInstructions?: string | null | null, ephemeral?: boolean, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** * If true, persist additional rollout EventMsg variants required to * reconstruct a richer thread history on subsequent resume/fork/read. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index fcde0ae502..770344de8e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -36,7 +36,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null | null, developerInstructions?: string | null | null, personality?: Personality | null, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** * If true, persist additional rollout EventMsg variants required to * reconstruct a richer thread history on subsequent resume/fork/read. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index c7b27823f6..61f501ad60 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -12,7 +12,7 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string | * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null | null, developerInstructions?: string | null | null, personality?: Personality | null, ephemeral?: boolean | null, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /** * If true, opt into emitting raw Responses API items on the event stream. * This is for internal use only (e.g. Codex Cloud). */ diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index e0d22bfa24..0c95fe3001 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2603,22 +2603,10 @@ pub struct ThreadStartParams { pub config: Option>, #[ts(optional = nullable)] pub service_name: Option, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] #[ts(optional = nullable)] - pub base_instructions: Option>, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] + pub base_instructions: Option, #[ts(optional = nullable)] - pub developer_instructions: Option>, + pub developer_instructions: Option, #[ts(optional = nullable)] pub personality: Option, #[ts(optional = nullable)] @@ -2733,22 +2721,10 @@ pub struct ThreadResumeParams { pub sandbox: Option, #[ts(optional = nullable)] pub config: Option>, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] #[ts(optional = nullable)] - pub base_instructions: Option>, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] + pub base_instructions: Option, #[ts(optional = nullable)] - pub developer_instructions: Option>, + pub developer_instructions: Option, #[ts(optional = nullable)] pub personality: Option, /// If true, persist additional rollout EventMsg variants required to @@ -2822,22 +2798,10 @@ pub struct ThreadForkParams { pub sandbox: Option, #[ts(optional = nullable)] pub config: Option>, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] #[ts(optional = nullable)] - pub base_instructions: Option>, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] + pub base_instructions: Option, #[ts(optional = nullable)] - pub developer_instructions: Option>, + pub developer_instructions: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub ephemeral: bool, /// If true, persist additional rollout EventMsg variants required to @@ -8364,35 +8328,6 @@ mod tests { assert_eq!(serialized_without_override.get("serviceTier"), None); } - #[test] - fn thread_start_params_preserve_explicit_null_instructions() { - let params: ThreadStartParams = serde_json::from_value(json!({ - "baseInstructions": null, - "developerInstructions": null, - })) - .expect("params should deserialize"); - assert_eq!(params.base_instructions, Some(None)); - assert_eq!(params.developer_instructions, Some(None)); - - let serialized = serde_json::to_value(¶ms).expect("params should serialize"); - assert_eq!( - serialized.get("baseInstructions"), - Some(&serde_json::Value::Null) - ); - assert_eq!( - serialized.get("developerInstructions"), - Some(&serde_json::Value::Null) - ); - - let serialized_without_override = - serde_json::to_value(ThreadStartParams::default()).expect("params should serialize"); - assert_eq!(serialized_without_override.get("baseInstructions"), None); - assert_eq!( - serialized_without_override.get("developerInstructions"), - None - ); - } - #[test] fn turn_start_params_preserve_explicit_null_service_tier() { let params: TurnStartParams = serde_json::from_value(json!({ diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 9282720828..3c7ef321fa 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2473,8 +2473,8 @@ impl CodexMessageProcessor { approval_policy: Option, approvals_reviewer: Option, sandbox: Option, - base_instructions: Option>, - developer_instructions: Option>, + base_instructions: Option, + developer_instructions: Option, personality: Option, ) -> ConfigOverrides { ConfigOverrides { @@ -4363,13 +4363,6 @@ impl CodexMessageProcessor { developer_instructions, /*personality*/ None, ); - if typesafe_overrides.base_instructions.is_none() - && let Ok(history) = RolloutRecorder::get_rollout_history(&rollout_path).await - && let Some(base_instructions) = history.get_base_instructions() - { - typesafe_overrides.base_instructions = - Some(base_instructions.map(|base_instructions| base_instructions.text)); - } typesafe_overrides.ephemeral = ephemeral.then_some(true); // Derive a Config using the same logic as new conversation, honoring overrides if provided. let cloud_requirements = self.current_cloud_requirements(); diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index 1137f09848..9907fc4b1d 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -26,8 +26,6 @@ use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput; use codex_config::types::AuthCredentialsStoreMode; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; -use core_test_support::responses; -use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; @@ -185,170 +183,6 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> { Ok(()) } -#[tokio::test] -async fn thread_fork_honors_explicit_null_thread_instructions() -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = responses::start_mock_server().await; - let body = responses::sse(vec![ - responses::ev_response_created("resp-1"), - responses::ev_assistant_message("msg-1", "Done"), - responses::ev_completed("resp-1"), - ]); - let response_mock = - responses::mount_sse_sequence(&server, vec![body.clone(), body.clone(), body]).await; - - let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri())?; - let config_path = codex_home.path().join("config.toml"); - let mut config_toml = std::fs::read_to_string(&config_path)?; - let first_table_index = config_toml - .find("\n[") - .expect("test config must include a table header"); - config_toml.insert_str( - first_table_index, - "\ndeveloper_instructions = \"Config developer instructions sentinel\"\n", - ); - std::fs::write(config_path, config_toml)?; - - let conversation_id = create_fake_rollout( - codex_home.path(), - "2025-01-05T12-00-00", - "2025-01-05T12:00:00Z", - "Saved user message", - Some("mock_provider"), - /*git_info*/ None, - )?; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - - let disabled_instruction_config = json!({ - "include_permissions_instructions": false, - "include_apps_instructions": false, - "include_environment_context": false, - "features.apps": false, - "features.plugins": false, - "features.codex_hooks": false, - "skills.bundled.enabled": false, - }); - - let fork_params = [ - ( - json!({ - "threadId": conversation_id.clone(), - "config": disabled_instruction_config.clone(), - }), - /*expect_instructions*/ true, - ), - ( - json!({ - "threadId": conversation_id.clone(), - "config": disabled_instruction_config.clone(), - "baseInstructions": null, - "developerInstructions": null, - }), - /*expect_instructions*/ false, - ), - ]; - - let mut forked_thread_ids = Vec::new(); - for (params, _expect_instructions) in fork_params { - let fork_id = mcp.send_raw_request("thread/fork", Some(params)).await?; - let fork_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(fork_id)), - ) - .await??; - let ThreadForkResponse { thread, .. } = to_response::(fork_resp)?; - forked_thread_ids.push(thread.id.clone()); - - let turn_id = mcp - .send_turn_start_request(TurnStartParams { - thread_id: thread.id, - input: vec![UserInput::Text { - text: "continue".to_string(), - text_elements: Vec::new(), - }], - ..Default::default() - }) - .await?; - let turn_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(turn_id)), - ) - .await??; - let _: TurnStartResponse = to_response::(turn_resp)?; - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - } - - let refork_id = mcp - .send_raw_request( - "thread/fork", - Some(json!({ - "threadId": forked_thread_ids[1].clone(), - "config": disabled_instruction_config.clone(), - })), - ) - .await?; - let refork_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(refork_id)), - ) - .await??; - let ThreadForkResponse { thread, .. } = to_response::(refork_resp)?; - let turn_id = mcp - .send_turn_start_request(TurnStartParams { - thread_id: thread.id, - input: vec![UserInput::Text { - text: "continue again".to_string(), - text_elements: Vec::new(), - }], - ..Default::default() - }) - .await?; - let turn_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(turn_id)), - ) - .await??; - let _: TurnStartResponse = to_response::(turn_resp)?; - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - - let requests = response_mock.requests(); - assert_eq!(requests.len(), 3); - for (request, expect_instructions) in requests.into_iter().zip([true, false, false]) { - let payload = request.body_json(); - assert_eq!( - payload.get("instructions").is_some(), - expect_instructions, - "unexpected instructions field in payload: {payload:?}" - ); - let developer_texts = request.message_input_texts("developer"); - assert_eq!( - developer_texts - .iter() - .any(|text| { text.contains("Config developer instructions sentinel") }), - expect_instructions, - "unexpected config developer instruction presence: {developer_texts:?}" - ); - assert!( - developer_texts.iter().all(|text| !text.is_empty()), - "did not expect empty developer instruction messages: {developer_texts:?}" - ); - } - - Ok(()) -} - #[tokio::test] async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 70c388a19d..59591dd0d1 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -152,120 +152,6 @@ async fn turn_start_sends_originator_header() -> Result<()> { Ok(()) } -#[tokio::test] -async fn turn_start_honors_explicit_null_thread_instructions() -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = responses::start_mock_server().await; - let body = responses::sse(vec![ - responses::ev_response_created("resp-1"), - responses::ev_assistant_message("msg-1", "Done"), - responses::ev_completed("resp-1"), - ]); - let response_mock = responses::mount_sse_sequence(&server, vec![body.clone(), body]).await; - - let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri(), "never", &BTreeMap::new())?; - let config_path = codex_home.path().join("config.toml"); - let mut config_toml = std::fs::read_to_string(&config_path)?; - let first_table_index = config_toml - .find("\n[") - .expect("test config must include a table header"); - config_toml.insert_str( - first_table_index, - "\ndeveloper_instructions = \"Config developer instructions sentinel\"\n", - ); - std::fs::write(config_path, config_toml)?; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - - let disabled_instruction_config = json!({ - "include_permissions_instructions": false, - "include_apps_instructions": false, - "include_environment_context": false, - "features.apps": false, - "features.plugins": false, - "features.codex_hooks": false, - "skills.bundled.enabled": false, - }); - - let thread_start_params = [ - ( - json!({ - "model": "mock-model", - "config": disabled_instruction_config.clone(), - }), - /*expect_instructions*/ true, - ), - ( - json!({ - "model": "mock-model", - "config": disabled_instruction_config.clone(), - "baseInstructions": null, - "developerInstructions": null, - }), - /*expect_instructions*/ false, - ), - ]; - - for (params, _expect_instructions) in thread_start_params { - let thread_req = mcp.send_raw_request("thread/start", Some(params)).await?; - let thread_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), - ) - .await??; - let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; - - let turn_req = mcp - .send_turn_start_request(TurnStartParams { - thread_id: thread.id, - input: vec![V2UserInput::Text { - text: "Hello".to_string(), - text_elements: Vec::new(), - }], - ..Default::default() - }) - .await?; - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), - ) - .await??; - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - } - - let requests = response_mock.requests(); - assert_eq!(requests.len(), 2); - for (request, expect_instructions) in requests.into_iter().zip([true, false]) { - let payload = request.body_json(); - assert_eq!( - payload.get("instructions").is_some(), - expect_instructions, - "unexpected instructions field in payload: {payload:?}" - ); - let developer_texts = request.message_input_texts("developer"); - assert_eq!( - developer_texts - .iter() - .any(|text| { text.contains("Config developer instructions sentinel") }), - expect_instructions, - "unexpected config developer instruction presence: {developer_texts:?}" - ); - assert!( - developer_texts.iter().all(|text| !text.is_empty()), - "did not expect empty developer instruction messages: {developer_texts:?}" - ); - } - - Ok(()) -} - #[tokio::test] async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> { let responses = vec![create_final_assistant_message_sse_response("Done")?]; diff --git a/codex-rs/codex-api/src/common.rs b/codex-rs/codex-api/src/common.rs index 6a88cd1f47..882b2a0e2d 100644 --- a/codex-rs/codex-api/src/common.rs +++ b/codex-rs/codex-api/src/common.rs @@ -153,8 +153,7 @@ impl From for OpenAiVerbosity { #[derive(Debug, Serialize, Clone, PartialEq)] pub struct ResponsesApiRequest { pub model: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub instructions: Option, + pub instructions: String, pub input: Vec, pub tools: Vec, pub tool_choice: String, @@ -199,8 +198,7 @@ impl From<&ResponsesApiRequest> for ResponseCreateWsRequest { #[derive(Debug, Serialize)] pub struct ResponseCreateWsRequest { pub model: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub instructions: Option, + pub instructions: String, #[serde(skip_serializing_if = "Option::is_none")] pub previous_response_id: Option, pub input: Vec, diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index 2e8411dd81..dd88bbd129 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -266,7 +266,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> { let request = ResponsesApiRequest { model: "gpt-test".into(), - instructions: Some("Say hi".into()), + instructions: "Say hi".into(), input: Vec::new(), tools: Vec::new(), tool_choice: "auto".into(), @@ -303,7 +303,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { let request = ResponsesApiRequest { model: "gpt-test".into(), - instructions: Some("Say hi".into()), + instructions: "Say hi".into(), input: vec![ResponseItem::Message { id: Some("msg_1".into()), role: "user".into(), diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 80ad24be5b..d09672acfb 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -403,11 +403,7 @@ impl ModelClient { ApiCompactClient::new(transport, client_setup.api_provider, client_setup.api_auth) .with_telemetry(Some(request_telemetry)); - let instructions = prompt - .base_instructions - .as_ref() - .map(|base_instructions| base_instructions.text.clone()) - .unwrap_or_default(); + let instructions = prompt.base_instructions.text.clone(); let input = prompt.get_formatted_input(); let tools = create_tools_json_for_responses_api(&prompt.tools)?; let reasoning = Self::build_reasoning(model_info, effort, summary); @@ -771,10 +767,7 @@ impl ModelClientSession { summary: ReasoningSummaryConfig, service_tier: Option, ) -> Result { - let instructions = prompt - .base_instructions - .as_ref() - .map(|base_instructions| base_instructions.text.clone()); + let instructions = &prompt.base_instructions.text; let input = prompt.get_formatted_input(); let tools = create_tools_json_for_responses_api(&prompt.tools)?; let default_reasoning_effort = model_info.default_reasoning_level; @@ -813,7 +806,7 @@ impl ModelClientSession { let prompt_cache_key = Some(self.client.state.conversation_id.to_string()); let request = ResponsesApiRequest { model: model_info.slug.clone(), - instructions, + instructions: instructions.clone(), input, tools, tool_choice: "auto".to_string(), diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index a90d75bf01..c1dfa29edc 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -23,7 +23,7 @@ pub const REVIEW_EXIT_INTERRUPTED_TMPL: &str = include_str!("../templates/review/exit_interrupted.xml"); /// API request payload for a single model turn -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct Prompt { /// Conversation context input items. pub input: Vec, @@ -35,7 +35,7 @@ pub struct Prompt { /// Whether parallel tool calls are permitted for this prompt. pub(crate) parallel_tool_calls: bool, - pub base_instructions: Option, + pub base_instructions: BaseInstructions, /// Optionally specify the personality of the model. pub personality: Option, @@ -44,19 +44,6 @@ pub struct Prompt { pub output_schema: Option, } -impl Default for Prompt { - fn default() -> Self { - Self { - input: Vec::new(), - tools: Vec::new(), - parallel_tool_calls: false, - base_instructions: Some(BaseInstructions::default()), - personality: None, - output_schema: None, - } - } -} - impl Prompt { pub(crate) fn get_formatted_input(&self) -> Vec { let mut input = self.input.clone(); diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index 4d4b5af4ff..1d46f47535 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -14,7 +14,7 @@ fn serializes_text_verbosity_when_set() { let tools: Vec = vec![]; let req = ResponsesApiRequest { model: "gpt-5.1".to_string(), - instructions: Some("i".to_string()), + instructions: "i".to_string(), input, tools, tool_choice: "auto".to_string(), @@ -58,7 +58,7 @@ fn serializes_text_schema_with_strict_format() { let req = ResponsesApiRequest { model: "gpt-5.1".to_string(), - instructions: Some("i".to_string()), + instructions: "i".to_string(), input, tools, tool_choice: "auto".to_string(), @@ -96,7 +96,7 @@ fn omits_text_when_not_set() { let tools: Vec = vec![]; let req = ResponsesApiRequest { model: "gpt-5.1".to_string(), - instructions: Some("i".to_string()), + instructions: "i".to_string(), input, tools, tool_choice: "auto".to_string(), @@ -119,7 +119,7 @@ fn omits_text_when_not_set() { fn serializes_flex_service_tier_when_set() { let req = ResponsesApiRequest { model: "gpt-5.1".to_string(), - instructions: Some("i".to_string()), + instructions: "i".to_string(), input: vec![], tools: vec![], tool_choice: "auto".to_string(), diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index dce76bf3d2..82c08556d5 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -579,15 +579,11 @@ impl Codex { let model_info = models_manager .get_model_info(model.as_str(), &config.to_models_manager_config()) .await; - let base_instructions = match config.base_instructions.clone() { - Some(base_instructions) => base_instructions, - None => conversation_history - .get_base_instructions() - .map(|base_instructions| { - base_instructions.map(|base_instructions| base_instructions.text) - }) - .unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))), - }; + let base_instructions = config + .base_instructions + .clone() + .or_else(|| conversation_history.get_base_instructions().map(|s| s.text)) + .unwrap_or_else(|| model_info.get_model_instructions(config.personality)); // Respect thread-start tools. When missing (resumed/forked threads), read from the db // first, then fall back to rollout-file tools. @@ -1116,7 +1112,7 @@ pub(crate) struct SessionConfiguration { personality: Option, /// Base instructions for the session. - base_instructions: Option, + base_instructions: String, /// Compact prompt override. compact_prompt: Option, @@ -1555,13 +1551,9 @@ impl Session { conversation_id, forked_from_id, session_source, - session_configuration - .base_instructions - .clone() - .map(|text| BaseInstructions { text }), - session_configuration - .developer_instructions_override - .clone(), + BaseInstructions { + text: session_configuration.base_instructions.clone(), + }, session_configuration.dynamic_tools.clone(), if session_configuration.persist_extended_history { EventPersistenceMode::Extended @@ -2125,9 +2117,8 @@ impl Session { )); } } - if let Some(base_instructions) = session_configuration.base_instructions.clone() { - sess.schedule_startup_prewarm(base_instructions).await; - } + sess.schedule_startup_prewarm(session_configuration.base_instructions.clone()) + .await; let session_start_source = match &initial_history { InitialHistory::Resumed(_) => codex_hooks::SessionStartSource::Resume, InitialHistory::New | InitialHistory::Forked(_) => { @@ -2229,13 +2220,11 @@ impl Session { state.history.estimate_token_count(turn_context) } - pub(crate) async fn get_base_instructions(&self) -> Option { + pub(crate) async fn get_base_instructions(&self) -> BaseInstructions { let state = self.state.lock().await; - state - .session_configuration - .base_instructions - .clone() - .map(|text| BaseInstructions { text }) + BaseInstructions { + text: state.session_configuration.base_instructions.clone(), + } } // Merges connector IDs into the session-level explicit connector selection. @@ -3639,11 +3628,7 @@ impl Session { state.reference_context_item(), state.previous_turn_settings(), state.session_configuration.collaboration_mode.clone(), - state - .session_configuration - .base_instructions - .clone() - .unwrap_or_default(), + state.session_configuration.base_instructions.clone(), state.session_configuration.session_source.clone(), ) }; @@ -3884,13 +3869,7 @@ impl Session { pub(crate) async fn recompute_token_usage(&self, turn_context: &TurnContext) { let history = self.clone_history().await; - let empty_base_instructions = BaseInstructions { - text: String::new(), - }; - let base_instructions = self - .get_base_instructions() - .await - .unwrap_or(empty_base_instructions); + let base_instructions = self.get_base_instructions().await; let Some(estimated_total_tokens) = history.estimate_token_count_with_base_instructions(&base_instructions) else { @@ -6584,7 +6563,7 @@ pub(crate) fn build_prompt( input: Vec, router: &ToolRouter, turn_context: &TurnContext, - base_instructions: Option, + base_instructions: BaseInstructions, ) -> Prompt { let deferred_dynamic_tools = turn_context .dynamic_tools diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 29fe9df89c..2ccf4f1de0 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -592,15 +592,11 @@ async fn get_base_instructions_no_user_content() { { let mut state = session.state.lock().await; - state.session_configuration.base_instructions = - Some(model_info.base_instructions.clone()); + state.session_configuration.base_instructions = model_info.base_instructions.clone(); } let base_instructions = session.get_base_instructions().await; - assert_eq!( - base_instructions.expect("base instructions").text, - model_info.base_instructions - ); + assert_eq!(base_instructions.text, model_info.base_instructions); } } @@ -1096,7 +1092,7 @@ async fn recompute_token_usage_uses_session_base_instructions() { let override_instructions = "SESSION_OVERRIDE_INSTRUCTIONS_ONLY".repeat(120); { let mut state = session.state.lock().await; - state.session_configuration.base_instructions = Some(override_instructions.clone()); + state.session_configuration.base_instructions = override_instructions.clone(); } let item = user_message("hello"); @@ -1861,7 +1857,7 @@ async fn set_rate_limits_retains_previous_credits() { base_instructions: config .base_instructions .clone() - .unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))), + .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, @@ -1964,7 +1960,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { base_instructions: config .base_instructions .clone() - .unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))), + .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, @@ -2229,8 +2225,7 @@ async fn attach_rollout_recorder(session: &Arc) -> PathBuf { ThreadId::default(), /*forked_from_id*/ None, SessionSource::Exec, - Some(BaseInstructions::default()), - /*developer_instructions*/ None, + BaseInstructions::default(), Vec::new(), EventPersistenceMode::Limited, ), @@ -2315,7 +2310,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati base_instructions: config .base_instructions .clone() - .unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))), + .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, @@ -2579,7 +2574,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { base_instructions: config .base_instructions .clone() - .unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))), + .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, @@ -2683,7 +2678,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { base_instructions: config .base_instructions .clone() - .unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))), + .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, @@ -3525,7 +3520,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( base_instructions: config .base_instructions .clone() - .unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))), + .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, @@ -4270,8 +4265,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline ThreadId::default(), /*forked_from_id*/ None, SessionSource::Exec, - Some(BaseInstructions::default()), - /*developer_instructions*/ None, + BaseInstructions::default(), Vec::new(), EventPersistenceMode::Limited, ), @@ -4368,8 +4362,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei ThreadId::default(), /*forked_from_id*/ None, SessionSource::Exec, - Some(BaseInstructions::default()), - /*developer_instructions*/ None, + BaseInstructions::default(), Vec::new(), EventPersistenceMode::Limited, ), diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 640ca3c1f8..5bc7944d28 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -76,16 +76,10 @@ async fn run_remote_compact_task_inner_impl( .await; let mut history = sess.clone_history().await; let base_instructions = sess.get_base_instructions().await; - let token_count_base_instructions = - base_instructions - .clone() - .unwrap_or_else(|| BaseInstructions { - text: String::new(), - }); let deleted_items = trim_function_call_history_to_fit_context_window( &mut history, turn_context.as_ref(), - &token_count_base_instructions, + &base_instructions, ); if deleted_items > 0 { info!( @@ -133,13 +127,8 @@ async fn run_remote_compact_task_inner_impl( ) .or_else(|err| async { let total_usage_breakdown = sess.get_total_token_usage_breakdown().await; - let base_instruction_text = prompt - .base_instructions - .as_ref() - .map(|base_instructions| base_instructions.text.as_str()) - .unwrap_or(""); let compact_request_log_data = - build_compact_request_log_data(&prompt.input, base_instruction_text); + build_compact_request_log_data(&prompt.input, &prompt.base_instructions.text); log_remote_compact_failure( turn_context, &compact_request_log_data, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 3e9626866b..1787296cf9 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -243,7 +243,7 @@ pub struct Config { pub user_instructions: Option, /// Base instructions override. - pub base_instructions: Option>, + pub base_instructions: Option, /// Developer instructions override injected as a separate message. pub developer_instructions: Option, @@ -691,7 +691,7 @@ impl Config { model_context_window: self.model_context_window, model_auto_compact_token_limit: self.model_auto_compact_token_limit, tool_output_token_limit: self.tool_output_token_limit, - base_instructions: self.base_instructions.clone().flatten(), + base_instructions: self.base_instructions.clone(), personality_enabled: self.features.enabled(Feature::Personality), model_supports_reasoning_summaries: self.model_supports_reasoning_summaries, model_catalog: self.model_catalog.clone(), @@ -1204,8 +1204,8 @@ pub struct ConfigOverrides { pub js_repl_node_path: Option, pub js_repl_node_module_dirs: Option>, pub zsh_path: Option, - pub base_instructions: Option>, - pub developer_instructions: Option>, + pub base_instructions: Option, + pub developer_instructions: Option, pub personality: Option, pub compact_prompt: Option, pub include_apply_patch_tool: Option, @@ -1764,10 +1764,8 @@ impl Config { .or(cfg.model_instructions_file.as_ref()); let file_base_instructions = Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?; - let base_instructions = base_instructions.or_else(|| file_base_instructions.map(Some)); - let developer_instructions_override = developer_instructions.clone(); - let developer_instructions = - developer_instructions.unwrap_or_else(|| cfg.developer_instructions.clone()); + let base_instructions = base_instructions.or(file_base_instructions); + let developer_instructions = developer_instructions.or(cfg.developer_instructions); let include_permissions_instructions = config_profile .include_permissions_instructions .or(cfg.include_permissions_instructions) diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index d476b8e6fd..2375f45f2f 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -904,7 +904,7 @@ model_instructions_file = "child.txt" .await?; assert_eq!( - config.base_instructions.as_ref().and_then(Option::as_deref), + config.base_instructions.as_deref(), Some("child instructions") ); @@ -940,7 +940,7 @@ async fn cli_override_model_instructions_file_sets_base_instructions() -> std::i .await?; assert_eq!( - config.base_instructions.as_ref().and_then(Option::as_deref), + config.base_instructions.as_deref(), Some("cli override instructions") ); diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 368b812e56..bc24974c82 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -137,7 +137,7 @@ impl GuardianReviewSessionReuseKey { model_reasoning_summary: spawn_config.model_reasoning_summary, permissions: spawn_config.permissions.clone(), developer_instructions: spawn_config.developer_instructions.clone(), - base_instructions: spawn_config.base_instructions.clone().flatten(), + base_instructions: spawn_config.base_instructions.clone(), user_instructions: spawn_config.user_instructions.clone(), compact_prompt: spawn_config.compact_prompt.clone(), cwd: spawn_config.cwd.to_path_buf(), diff --git a/codex-rs/core/src/memories/phase1.rs b/codex-rs/core/src/memories/phase1.rs index 0b8cdcdad5..37cb0e9f48 100644 --- a/codex-rs/core/src/memories/phase1.rs +++ b/codex-rs/core/src/memories/phase1.rs @@ -336,9 +336,9 @@ mod job { }], tools: Vec::new(), parallel_tool_calls: false, - base_instructions: Some(BaseInstructions { + base_instructions: BaseInstructions { text: phase_one::PROMPT.to_string(), - }), + }, personality: None, output_schema: Some(output_schema()), }; diff --git a/codex-rs/core/src/session_startup_prewarm.rs b/codex-rs/core/src/session_startup_prewarm.rs index d3ab0e6198..acd9232c09 100644 --- a/codex-rs/core/src/session_startup_prewarm.rs +++ b/codex-rs/core/src/session_startup_prewarm.rs @@ -217,9 +217,9 @@ async fn schedule_startup_prewarm_inner( Vec::new(), startup_router.as_ref(), startup_turn_context.as_ref(), - Some(BaseInstructions { + BaseInstructions { text: base_instructions, - }), + }, ); let startup_turn_metadata_header = startup_turn_context .turn_metadata_state diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index 834cf20b4b..a1cc071108 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -112,7 +112,7 @@ async fn start_review_conversation( let _ = sub_agent_config.features.disable(Feature::Collab); // Set explicit review rubric for the sub-agent - sub_agent_config.base_instructions = Some(Some(crate::REVIEW_PROMPT.to_string())); + sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string()); sub_agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); let model = config diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index 837b654edc..fe30f3d750 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs.rs @@ -535,7 +535,7 @@ async fn build_runner_options( let max_concurrency = normalize_concurrency(requested_concurrency, turn.config.agent_max_threads); let base_instructions = session.get_base_instructions().await; - let spawn_config = build_agent_spawn_config(base_instructions.as_ref(), turn.as_ref())?; + let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref())?; Ok(JobRunnerOptions { max_concurrency, spawn_config, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs index a2482ea6a9..8e4bfb5b59 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -59,10 +59,8 @@ impl ToolHandler for Handler { .into(), ) .await; - let mut config = build_agent_spawn_config( - session.get_base_instructions().await.as_ref(), - turn.as_ref(), - )?; + let mut config = + build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?; apply_requested_spawn_agent_model_overrides( &session, turn.as_ref(), diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 4205c4bb43..2078c229b9 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -201,12 +201,11 @@ pub(crate) fn parse_collab_input( /// skipping this helper and cloning stale config state directly can send the child agent out with /// the wrong provider or runtime policy. pub(crate) fn build_agent_spawn_config( - base_instructions: Option<&BaseInstructions>, + base_instructions: &BaseInstructions, turn: &TurnContext, ) -> Result { let mut config = build_agent_shared_config(turn)?; - config.base_instructions = - Some(base_instructions.map(|base_instructions| base_instructions.text.clone())); + config.base_instructions = Some(base_instructions.text.clone()); Ok(config) } diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 5f09c248a4..bcb47f3b6b 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -3255,9 +3255,9 @@ async fn build_agent_spawn_config_uses_turn_context_values() { .set(AskForApproval::OnRequest) .expect("approval policy set"); - let config = build_agent_spawn_config(Some(&base_instructions), &turn).expect("spawn config"); + let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config"); let mut expected = (*turn.config).clone(); - expected.base_instructions = Some(Some(base_instructions.text)); + expected.base_instructions = Some(base_instructions.text); expected.model = Some(turn.model_info.slug.clone()); expected.model_provider = turn.provider.clone(); expected.model_reasoning_effort = turn.reasoning_effort; @@ -3293,7 +3293,7 @@ async fn build_agent_spawn_config_preserves_base_user_instructions() { text: "base".to_string(), }; - let config = build_agent_spawn_config(Some(&base_instructions), &turn).expect("spawn config"); + let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config"); assert_eq!(config.user_instructions, base_config.user_instructions); } @@ -3302,7 +3302,7 @@ async fn build_agent_spawn_config_preserves_base_user_instructions() { async fn build_agent_resume_config_clears_base_instructions() { let (_session, mut turn) = make_session_and_context().await; let mut base_config = (*turn.config).clone(); - base_config.base_instructions = Some(Some("caller-base".to_string())); + base_config.base_instructions = Some("caller-base".to_string()); turn.config = Arc::new(base_config); turn.approval_policy .set(AskForApproval::OnRequest) diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 0d460b1a04..b7b821b409 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -69,10 +69,8 @@ impl ToolHandler for Handler { .into(), ) .await; - let mut config = build_agent_spawn_config( - session.get_base_instructions().await.as_ref(), - turn.as_ref(), - )?; + let mut config = + build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?; apply_requested_spawn_agent_model_overrides( &session, turn.as_ref(), diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 15303a58ff..5200d252c4 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -932,7 +932,7 @@ async fn includes_base_instructions_override_in_request() { let mut builder = test_codex() .with_auth(CodexAuth::from_api_key("Test API Key")) .with_config(|config| { - config.base_instructions = Some(Some("test instructions".to_string())); + config.base_instructions = Some("test instructions".to_string()); }); let codex = builder .build(&server) @@ -964,47 +964,6 @@ async fn includes_base_instructions_override_in_request() { ); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn omits_explicit_null_base_instructions_from_request() { - skip_if_no_network!(); - // Mock server - let server = MockServer::start().await; - let resp_mock = mount_sse_once( - &server, - sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), - ) - .await; - - let mut builder = test_codex() - .with_auth(CodexAuth::from_api_key("Test API Key")) - .with_config(|config| { - config.base_instructions = Some(None); - }); - let codex = builder - .build(&server) - .await - .expect("create new conversation") - .codex; - - codex - .submit(Op::UserInput { - items: vec![UserInput::Text { - text: "hello".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - }) - .await - .unwrap(); - - wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - - let request = resp_mock.single_request(); - let request_body = request.body_json(); - - assert_eq!(request_body.get("instructions"), None); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn chatgpt_auth_sends_correct_request() { skip_if_no_network!(); diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 7ff4a9f88d..9a76638f67 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -1659,9 +1659,9 @@ fn prompt_with_input(input: Vec) -> Prompt { fn prompt_with_input_and_instructions(input: Vec, instructions: &str) -> Prompt { let mut prompt = prompt_with_input(input); - prompt.base_instructions = Some(BaseInstructions { + prompt.base_instructions = BaseInstructions { text: instructions.to_string(), - }); + }; prompt } diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index dd99e0f391..0069c5fb86 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -869,7 +869,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result let override_base_instructions = override_base_instructions.clone(); move |config| { config.model_context_window = Some(override_context_window); - config.base_instructions = Some(Some(override_base_instructions)); + config.base_instructions = Some(override_base_instructions); } }), ) diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 8a0cbdcb63..172738ffd1 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -66,7 +66,7 @@ async fn base_instructions_override_disables_personality_template() { .enable(Feature::Personality) .expect("test config should allow feature update"); config.personality = Some(Personality::Friendly); - config.base_instructions = Some(Some("override instructions".to_string())); + config.base_instructions = Some("override instructions".to_string()); let model_info = codex_core::test_support::construct_model_info_offline("gpt-5.2-codex", &config); diff --git a/codex-rs/core/tests/suite/rollout_list_find.rs b/codex-rs/core/tests/suite/rollout_list_find.rs index 1954654266..7445a4ba05 100644 --- a/codex-rs/core/tests/suite/rollout_list_find.rs +++ b/codex-rs/core/tests/suite/rollout_list_find.rs @@ -173,8 +173,7 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()> thread_id, /*forked_from_id*/ None, SessionSource::Exec, - Some(BaseInstructions::default()), - /*developer_instructions*/ None, + BaseInstructions::default(), Vec::new(), EventPersistenceMode::Limited, ), diff --git a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs index c5aa57ce74..19d9d27cf7 100644 --- a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs +++ b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs @@ -84,7 +84,7 @@ async fn continue_after_stream_error() { let TestCodex { codex, .. } = test_codex() .with_config(move |config| { - config.base_instructions = Some(Some("You are a helpful assistant".to_string())); + config.base_instructions = Some("You are a helpful assistant".to_string()); config.model_provider = provider; }) .build(&server) diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 341f0ac4b2..f83fd4fd5a 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -179,8 +179,8 @@ impl CodexToolCallParam { codex_self_exe: arg0_paths.codex_self_exe.clone(), codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), - base_instructions: base_instructions.map(Some), - developer_instructions: developer_instructions.map(Some), + base_instructions, + developer_instructions, compact_prompt, ..Default::default() }; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 1f199b149b..afb6a1b81c 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2320,7 +2320,7 @@ impl InitialHistory { } } - pub fn get_base_instructions(&self) -> Option> { + pub fn get_base_instructions(&self) -> Option { // TODO: SessionMeta should (in theory) always be first in the history, so we can probably only check the first item? match self { InitialHistory::New => None, @@ -2548,13 +2548,7 @@ pub struct SessionMeta { /// base_instructions for the session. This *should* always be present when creating a new session, /// but may be missing for older sessions. If not present, fall back to rendering the base_instructions /// from ModelsManager. - #[serde( - default, - deserialize_with = "deserialize_double_option", - serialize_with = "serialize_double_option", - skip_serializing_if = "Option::is_none" - )] - pub base_instructions: Option>, + pub base_instructions: Option, #[serde( default, deserialize_with = "deserialize_double_option", diff --git a/codex-rs/rollout/src/recorder.rs b/codex-rs/rollout/src/recorder.rs index b589ffac97..bb0f155653 100644 --- a/codex-rs/rollout/src/recorder.rs +++ b/codex-rs/rollout/src/recorder.rs @@ -81,8 +81,7 @@ pub enum RolloutRecorderParams { conversation_id: ThreadId, forked_from_id: Option, source: SessionSource, - base_instructions: Option, - developer_instructions: Option>, + base_instructions: BaseInstructions, dynamic_tools: Vec, event_persistence_mode: EventPersistenceMode, }, @@ -111,8 +110,7 @@ impl RolloutRecorderParams { conversation_id: ThreadId, forked_from_id: Option, source: SessionSource, - base_instructions: Option, - developer_instructions: Option>, + base_instructions: BaseInstructions, dynamic_tools: Vec, event_persistence_mode: EventPersistenceMode, ) -> Self { @@ -121,7 +119,6 @@ impl RolloutRecorderParams { forked_from_id, source, base_instructions, - developer_instructions, dynamic_tools, event_persistence_mode, } diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index 44e8ecb8c8..163c8a1ee8 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -73,8 +73,7 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result< thread_id, /*forked_from_id*/ None, SessionSource::Exec, - Some(BaseInstructions::default()), - /*developer_instructions*/ None, + BaseInstructions::default(), Vec::new(), EventPersistenceMode::Limited, ), @@ -167,8 +166,7 @@ async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Resu thread_id, /*forked_from_id*/ None, SessionSource::Cli, - Some(BaseInstructions::default()), - /*developer_instructions*/ None, + BaseInstructions::default(), Vec::new(), EventPersistenceMode::Limited, ), diff --git a/codex-rs/rollout/src/tests.rs b/codex-rs/rollout/src/tests.rs index 4fbd58c14b..622fc4bdb7 100644 --- a/codex-rs/rollout/src/tests.rs +++ b/codex-rs/rollout/src/tests.rs @@ -977,7 +977,7 @@ async fn test_get_thread_contents() { } #[tokio::test] -async fn test_base_instructions_missing_in_meta_stays_missing() { +async fn test_base_instructions_missing_in_meta_defaults_to_null() { let temp = TempDir::new().unwrap(); let home = temp.path(); @@ -1011,7 +1011,10 @@ async fn test_base_instructions_missing_in_meta_stays_missing() { .await .expect("session meta head"); let first = head.first().expect("first head entry"); - assert_eq!(first.get("base_instructions"), None); + assert_eq!( + first.get("base_instructions"), + Some(&serde_json::Value::Null) + ); } #[tokio::test]