diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/config.rs b/codex-rs/ext/guardian-v2/src/async_scorer/config.rs index 9fc6d949e5..76f8eeb5cd 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/config.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/config.rs @@ -75,6 +75,9 @@ impl GuardianV2Config { .review_threshold_basis_points .map(|basis_points| f64::from(basis_points) / 10_000.0) }); + configured.max_tool_call_lag = configured + .max_tool_call_lag + .or(model_defaults.max_tool_call_lag); configured.reasoning_effort = configured .reasoning_effort .or_else(|| model_defaults.reasoning_effort.clone()); @@ -84,6 +87,9 @@ impl GuardianV2Config { configured.max_classifier_instruction_tokens = configured .max_classifier_instruction_tokens .or(model_defaults.max_classifier_instruction_tokens); + configured.reuse_parent_compaction = configured + .reuse_parent_compaction + .or(model_defaults.reuse_parent_compaction); configured.max_parent_compaction_tokens = configured .max_parent_compaction_tokens .or(model_defaults.max_parent_compaction_tokens); @@ -109,6 +115,9 @@ impl GuardianV2Config { .collect::, _>>()?, ); } + transcript.include_images = transcript + .include_images + .or(model_transcript.include_images); transcript.max_message_entry_tokens = transcript .max_message_entry_tokens .or(model_transcript.max_message_entry_tokens); diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs index 065eec33ac..9836693e81 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs @@ -1,6 +1,8 @@ use codex_features::GuardianV2ConfigToml; use codex_features::GuardianV2ReviewScopeConfigToml; +use codex_features::GuardianV2TranscriptConfigToml; use codex_protocol::openai_models::GuardianV2ModelConfig; +use codex_protocol::openai_models::GuardianV2TranscriptModelConfig; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::TruncationPolicy; use pretty_assertions::assert_eq; @@ -145,3 +147,49 @@ fn model_prompt_and_explicit_threshold_precedence_are_preserved() { 0.5, ); } + +#[test] +fn model_runtime_settings_preserve_local_overrides() { + let defaults = GuardianV2ModelConfig { + max_tool_call_lag: Some(1), + reuse_parent_compaction: Some(false), + transcript: Some(GuardianV2TranscriptModelConfig { + include_images: Some(true), + ..Default::default() + }), + ..Default::default() + }; + let inherited = GuardianV2Config::from_overrides(GuardianV2ConfigToml::default()) + .unwrap() + .with_model_defaults(Some(&defaults)) + .unwrap(); + assert_eq!( + ( + inherited.max_tool_call_lag, + inherited.reuse_parent_compaction, + inherited.transcript.include_images, + ), + (1, false, true) + ); + + let overridden = GuardianV2Config::from_overrides(GuardianV2ConfigToml { + max_tool_call_lag: Some(4), + reuse_parent_compaction: Some(true), + transcript: Some(GuardianV2TranscriptConfigToml { + include_images: Some(false), + ..Default::default() + }), + ..Default::default() + }) + .unwrap() + .with_model_defaults(Some(&defaults)) + .unwrap(); + assert_eq!( + ( + overridden.max_tool_call_lag, + overridden.reuse_parent_compaction, + overridden.transcript.include_images, + ), + (4, true, false) + ); +} diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs index 5f57d76d34..8bf8c64bec 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs @@ -461,6 +461,12 @@ impl GuardianV2Extension { return; } }; + if guardian_config.transcript.include_images { + input + .thread_store + .get_or_init(NodeReplReviewEvidence::default) + .enable_image_capture(); + } input.thread_store.insert(guardian_config.clone()); let tool_call_index = score_progress .latest_tool_call diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs index eb46697ebf..4af0c635a5 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs @@ -7,6 +7,7 @@ use std::time::SystemTime; use anyhow::Result; use codex_core::config::Config; +use codex_core::context::NodeReplReviewEvidence; use codex_extension_api::ConversationHistorySnapshot; use codex_extension_api::ExtensionData; use codex_extension_api::ExtensionMetrics; @@ -1231,15 +1232,18 @@ async fn contributor_uses_model_defaults_and_preserves_local_overrides() -> Resu let model_defaults = GuardianV2ModelConfig { classifier_instructions: Some("Use the experimental model-owned prompt.".to_owned()), review_threshold_basis_points: Some(6_000), + max_tool_call_lag: Some(2), reasoning_effort: Some(ReasoningEffort::Minimal), transcript: Some(GuardianV2TranscriptModelConfig { sources: Some(vec!["reasoning".to_owned()]), + include_images: Some(true), max_message_entry_tokens: Some(128), max_message_transcript_tokens: Some(256), ..Default::default() }), max_action_tokens: Some(128), max_classifier_instruction_tokens: Some(256), + reuse_parent_compaction: Some(false), max_parent_compaction_tokens: Some(384), }; let conversation_history = vec![ @@ -1312,13 +1316,19 @@ async fn contributor_uses_model_defaults_and_preserves_local_overrides() -> Resu let session_store = ExtensionData::new("session-1"); let thread_store = test.codex.thread_extension_data(); + let guardian_config = thread_store + .get::() + .expect("Guardian v2 configuration should be installed"); assert_eq!( - thread_store - .get::() - .expect("Guardian v2 configuration should be installed") - .max_parent_compaction_tokens, - 384 + ( + guardian_config.max_tool_call_lag, + guardian_config.reuse_parent_compaction, + guardian_config.max_parent_compaction_tokens, + guardian_config.transcript.include_images, + ), + (2, false, 384, true) ); + assert!(thread_store.get::().is_some()); tokio::time::timeout(Duration::from_secs(5), async { while thread_store.get::().is_none() { tokio::task::yield_now().await; diff --git a/codex-rs/protocol/src/openai_models/guardian_v2.rs b/codex-rs/protocol/src/openai_models/guardian_v2.rs index 814bf3ad23..953a89a4e7 100644 --- a/codex-rs/protocol/src/openai_models/guardian_v2.rs +++ b/codex-rs/protocol/src/openai_models/guardian_v2.rs @@ -13,6 +13,8 @@ pub struct GuardianV2ModelConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub review_threshold_basis_points: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_tool_call_lag: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub transcript: Option, @@ -21,6 +23,8 @@ pub struct GuardianV2ModelConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub max_classifier_instruction_tokens: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub reuse_parent_compaction: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub max_parent_compaction_tokens: Option, } @@ -30,6 +34,8 @@ pub struct GuardianV2TranscriptModelConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub sources: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] + pub include_images: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub max_message_entry_tokens: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub max_tool_entry_tokens: Option,