Honor Guardian runtime settings from model defaults (#39738)

## What changed

- Add `max_tool_call_lag`, `reuse_parent_compaction`, and transcript
  `include_images` to the Guardian model configuration.
- Inherit these settings from model defaults while preserving explicit local
  overrides.
- Enable image capture for Node REPL review evidence when transcript images are
  included.

## Testing

- Cover inheritance and local override precedence for the new settings.
- Verify that enabling transcript images initializes review-evidence capture.

GitOrigin-RevId: 7c1965c2ec00a78a0436fa916b630be0004dbb51
This commit is contained in:
felixxia-oai
2026-08-20 16:45:59 +00:00
committed by copyberry
parent 8c828b18d6
commit 88da5520d4
5 changed files with 84 additions and 5 deletions
@@ -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::<Result<Vec<_>, _>>()?,
);
}
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);
@@ -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)
);
}
@@ -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
@@ -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::<crate::async_scorer::config::GuardianV2Config>()
.expect("Guardian v2 configuration should be installed");
assert_eq!(
thread_store
.get::<crate::async_scorer::config::GuardianV2Config>()
.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::<NodeReplReviewEvidence>().is_some());
tokio::time::timeout(Duration::from_secs(5), async {
while thread_store.get::<SecurityRiskScore>().is_none() {
tokio::task::yield_now().await;
@@ -13,6 +13,8 @@ pub struct GuardianV2ModelConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub review_threshold_basis_points: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tool_call_lag: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transcript: Option<GuardianV2TranscriptModelConfig>,
@@ -21,6 +23,8 @@ pub struct GuardianV2ModelConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_classifier_instruction_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reuse_parent_compaction: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_parent_compaction_tokens: Option<usize>,
}
@@ -30,6 +34,8 @@ pub struct GuardianV2TranscriptModelConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sources: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_images: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_message_entry_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tool_entry_tokens: Option<usize>,