Source Guardian v2 defaults from the model catalog (#38990)

## What changed

- Add optional `guardian_v2` model-message defaults for classifier instructions,
  review thresholds, reasoning effort, transcript selection and limits, and
  action and compaction token limits.
- Apply those defaults when Guardian v2 samples a tool call, while preserving
  explicit `[features.guardianv2]` settings over catalog values.
- Preserve the new catalog configuration when applying model overrides.

## Testing

- Cover model-catalog serialization and model override behavior.
- Verify that sampling uses catalog defaults while retaining local overrides.

GitOrigin-RevId: 4ae9ec235c647acbca05a20b7c4b87a4e1331161
This commit is contained in:
felixxia-oai
2026-08-17 11:44:56 +00:00
committed by copyberry
parent 37cf6c84c0
commit 8bf50439f2
15 changed files with 347 additions and 6 deletions
@@ -42,6 +42,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
include_plugin_usage_instructions: false,
@@ -1836,6 +1836,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
};
let guardian_config = build_guardian_review_session_config(
@@ -1871,6 +1872,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
};
let guardian_config = build_guardian_review_session_config(
@@ -1914,6 +1916,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
};
let guardian_config = build_guardian_review_session_config(
@@ -43,6 +43,7 @@ async fn catalog_permission_message_loaded_from_remote_models_is_sent() -> Resul
}),
multi_agent: None,
token_budget: None,
guardian_v2: None,
});
let models_mock = mount_models_once(
&server,
@@ -72,6 +72,7 @@ fn model_with_collaboration_messages(
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
});
model_messages.collaboration_modes = Some(CollaborationModeMessages {
default: default.map(str::to_string),
@@ -512,6 +512,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
include_plugin_usage_instructions: false,
@@ -60,6 +60,7 @@ fn model_with_approval_messages(
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
});
model
}
@@ -78,6 +79,7 @@ fn model_with_permission_messages(
permissions: Some(permissions),
multi_agent: None,
token_budget: None,
guardian_v2: None,
});
model
}
@@ -231,6 +233,7 @@ async fn catalog_non_on_request_approval_messages_are_sent_in_initial_permission
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
});
let mut builder = test_codex()
.with_model(model_slug)
+2
View File
@@ -645,6 +645,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
include_plugin_usage_instructions: false,
@@ -770,6 +771,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
include_plugin_usage_instructions: false,
@@ -112,6 +112,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}),
..specific
};
@@ -126,6 +127,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}),
..generic
};
@@ -774,6 +776,7 @@ async fn remote_models_apply_legacy_instructions() -> Result<()> {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
include_plugin_usage_instructions: false,
+79
View File
@@ -1,6 +1,8 @@
use codex_core::config::Config;
use codex_features::FeatureToml;
use codex_features::GuardianV2ConfigToml;
use codex_features::GuardianV2TranscriptConfigToml;
use codex_protocol::openai_models::GuardianV2ModelConfig;
use codex_protocol::openai_models::ReasoningEffort;
use crate::transcript::MAX_MESSAGE_ENTRY_TOKENS;
@@ -24,6 +26,7 @@ Return an action_risk score from 0.0 (safe and authorized) to 1.0 (dangerous or
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct GuardianV2Config {
local_overrides: GuardianV2ConfigToml,
pub(crate) classifier_instructions: String,
pub(crate) review_threshold: f64,
pub(crate) reasoning_effort: ReasoningEffort,
@@ -53,6 +56,81 @@ impl GuardianV2Config {
None => GuardianV2ConfigToml::default(),
};
Self::from_overrides(configured)
}
pub(crate) fn with_model_defaults(
&self,
model_defaults: Option<&GuardianV2ModelConfig>,
) -> Result<Self, String> {
let mut configured = self.local_overrides.clone();
if let Some(model_defaults) = model_defaults {
configured.classifier_instructions = configured
.classifier_instructions
.or_else(|| model_defaults.classifier_instructions.clone());
configured.review_threshold = configured.review_threshold.or_else(|| {
model_defaults
.review_threshold_basis_points
.map(|basis_points| f64::from(basis_points) / 10_000.0)
});
configured.reasoning_effort = configured
.reasoning_effort
.or_else(|| model_defaults.reasoning_effort.clone());
configured.max_action_tokens = configured
.max_action_tokens
.or(model_defaults.max_action_tokens);
configured.max_classifier_instruction_tokens = configured
.max_classifier_instruction_tokens
.or(model_defaults.max_classifier_instruction_tokens);
configured.max_parent_compaction_tokens = configured
.max_parent_compaction_tokens
.or(model_defaults.max_parent_compaction_tokens);
if let Some(model_transcript) = model_defaults.transcript.as_ref() {
let transcript = configured
.transcript
.get_or_insert_with(GuardianV2TranscriptConfigToml::default);
if transcript.sources.is_none()
&& let Some(sources) = model_transcript.sources.as_ref()
{
transcript.sources = Some(
sources
.iter()
.map(|source| match source.as_str() {
"tool_calls" => Ok(TranscriptSource::ToolCalls),
"tool_outputs" => Ok(TranscriptSource::ToolOutputs),
"reasoning" => Ok(TranscriptSource::Reasoning),
_ => {
Err(format!("invalid Guardian v2 transcript source: {source}"))
}
})
.collect::<Result<Vec<_>, _>>()?,
);
}
transcript.max_message_entry_tokens = transcript
.max_message_entry_tokens
.or(model_transcript.max_message_entry_tokens);
transcript.max_tool_entry_tokens = transcript
.max_tool_entry_tokens
.or(model_transcript.max_tool_entry_tokens);
transcript.max_message_transcript_tokens = transcript
.max_message_transcript_tokens
.or(model_transcript.max_message_transcript_tokens);
transcript.max_tool_transcript_tokens = transcript
.max_tool_transcript_tokens
.or(model_transcript.max_tool_transcript_tokens);
transcript.max_recent_non_user_entries = transcript
.max_recent_non_user_entries
.or(model_transcript.max_recent_non_user_entries);
}
}
let mut resolved = Self::from_overrides(configured)?;
resolved.local_overrides = self.local_overrides.clone();
Ok(resolved)
}
fn from_overrides(configured: GuardianV2ConfigToml) -> Result<Self, String> {
let review_threshold = configured
.review_threshold
.unwrap_or(DEFAULT_REVIEW_THRESHOLD);
@@ -126,6 +204,7 @@ impl GuardianV2Config {
}
Ok(Self {
local_overrides: configured.clone(),
classifier_instructions: truncate_entry(
configured
.classifier_instructions
+17 -1
View File
@@ -288,6 +288,23 @@ impl ToolLifecycleContributor for GuardianV2Extension {
let Some(guardian_config) = input.thread_store.get::<GuardianV2Config>() else {
return Box::pin(std::future::ready(()));
};
let parent_model = input.thread_store.get::<ModelInfo>();
let model_defaults = parent_model
.as_ref()
.and_then(|model| model.model_messages.as_ref())
.and_then(|messages| messages.guardian_v2.as_ref());
let guardian_config = match guardian_config.with_model_defaults(model_defaults) {
Ok(config) => config,
Err(error) => {
self.event_sink.emit_warning(ExtensionWarning {
thread_id: input.thread_store.level_id().to_owned(),
turn_id: Some(input.turn_id.to_owned()),
message: error,
});
return Box::pin(std::future::ready(()));
}
};
input.thread_store.insert(guardian_config.clone());
let sampled_at = SystemTime::now();
let latest_parent_compaction = input
.conversation_history
@@ -337,7 +354,6 @@ impl ToolLifecycleContributor for GuardianV2Extension {
tool_name: input.tool_name.clone(),
payload: input.payload.clone(),
};
let parent_model = input.thread_store.get::<ModelInfo>();
let parent_compaction_hash = parent_model
.as_ref()
.and_then(|model| model.comp_hash.clone());
+151 -5
View File
@@ -26,6 +26,9 @@ use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ImageDetail;
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::openai_models::GuardianV2ModelConfig;
use codex_protocol::openai_models::GuardianV2TranscriptModelConfig;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TruncationPolicy;
@@ -222,8 +225,14 @@ async fn sample_conversation_history(
arguments: &str,
guardian_policy: Option<&str>,
) -> Result<(serde_json::Value, TestCodex, ExtensionRegistry<Config>)> {
sample_configured_conversation_history(conversation_history, arguments, guardian_policy, "")
.await
sample_configured_conversation_history(
conversation_history,
arguments,
guardian_policy,
"",
/*model_defaults*/ None,
)
.await
}
async fn sample_configured_conversation_history(
@@ -231,11 +240,13 @@ async fn sample_configured_conversation_history(
arguments: &str,
guardian_policy: Option<&str>,
guardian_config: &str,
model_defaults: Option<GuardianV2ModelConfig>,
) -> Result<(serde_json::Value, TestCodex, ExtensionRegistry<Config>)> {
let thread_server = responses::start_mock_server().await;
let guardian_policy = guardian_policy.map(str::to_owned);
let guardian_config = guardian_config.to_owned();
let test = test_codex()
let has_model_defaults = model_defaults.is_some();
let builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_model_info_override("codex-auto-review", |model_info| {
model_info
@@ -254,9 +265,19 @@ async fn sample_configured_conversation_history(
std::fs::write(home.join("config.toml"), guardian_config)
.expect("Guardian v2 configuration should be written");
}
});
let mut builder = if let Some(model_defaults) = model_defaults {
builder.with_model_info_override("gpt-5.5", move |model| {
model
.model_messages
.as_mut()
.expect("test model should expose model messages")
.guardian_v2 = Some(model_defaults);
})
.build_with_auto_env(&thread_server)
.await?;
} else {
builder
};
let test = builder.build_with_auto_env(&thread_server).await?;
let events = vec![
ev_assistant_message("sample", r#"{"scores":{"action_risk":0.8}}"#),
ev_completed("response-1"),
@@ -279,6 +300,14 @@ async fn sample_configured_conversation_history(
let registry = builder.build();
let session_store = ExtensionData::new("session-1");
let thread_store = test.codex.thread_extension_data();
if has_model_defaults {
let parent_model = test
.thread_manager
.get_models_manager()
.get_model_info("gpt-5.5", &config.to_models_manager_config())
.await;
thread_store.insert(parent_model);
}
assert_eq!(
registry
.approval_review(&session_store, thread_store, "review action")
@@ -389,6 +418,7 @@ max_recent_non_user_entries = 8
&arguments,
Some(TEST_GUARDIAN_POLICY),
configuration,
/*model_defaults*/ None,
)
.await?;
@@ -508,6 +538,7 @@ include_images = true
r#"{"path":"README.md"}"#,
Some(TEST_GUARDIAN_POLICY),
configuration,
/*model_defaults*/ None,
)
.await?;
let content = request["input"][2]["content"]
@@ -531,6 +562,121 @@ include_images = true
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn contributor_uses_model_defaults_and_preserves_local_overrides() -> Result<()> {
skip_if_no_network!(Ok(()));
let model_defaults = GuardianV2ModelConfig {
classifier_instructions: Some("Use the experimental model-owned prompt.".to_owned()),
review_threshold_basis_points: Some(6_000),
reasoning_effort: Some(ReasoningEffort::Minimal),
transcript: Some(GuardianV2TranscriptModelConfig {
sources: Some(vec!["reasoning".to_owned()]),
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),
max_parent_compaction_tokens: Some(384),
};
let conversation_history = vec![
ResponseItem::Message {
id: None,
role: "user".to_owned(),
content: vec![ContentItem::InputText {
text: "Review the pending action.".to_owned(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Reasoning {
id: None,
summary: vec![ReasoningItemReasoningSummary::SummaryText {
text: "Use the experimental transcript.".to_owned(),
}],
content: None,
encrypted_content: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::FunctionCall {
id: None,
name: "list_dir".to_owned(),
namespace: None,
arguments: r#"{"path":"."}"#.to_owned(),
encrypted_function_args: None,
call_id: "previous-call".to_owned(),
internal_chat_message_metadata_passthrough: None,
},
];
let arguments = json!({"body": "x".repeat(4_000)}).to_string();
let local_config = "[features.guardianv2]\nenabled = true\nreview_threshold = 0.70\n";
let (request, test, registry) = sample_configured_conversation_history(
conversation_history,
&arguments,
Some(TEST_GUARDIAN_POLICY),
local_config,
Some(model_defaults),
)
.await?;
assert_eq!(
request["input"][1],
json!({
"type": "message",
"role": "developer",
"content": [{
"type": "input_text",
"text": format!(
"Use the experimental model-owned prompt.\n\n# Security Policy\n{TEST_GUARDIAN_POLICY}"
)
}]
})
);
assert_eq!(request["reasoning"]["effort"], "minimal");
let content = request["input"][2]["content"]
.as_array()
.expect("Luna user content should be an array");
let transcript = content
.iter()
.filter_map(|item| item["text"].as_str())
.collect::<Vec<_>>();
assert!(transcript.contains(&"[2] reasoning: Use the experimental transcript.\n"));
assert!(!transcript.iter().any(|entry| entry.contains("list_dir")));
let action = content[content.len() - 2]["text"]
.as_str()
.expect("planned action should be a text item");
assert!(action.len() <= TruncationPolicy::Tokens(/*limit*/ 128).byte_budget());
let session_store = ExtensionData::new("session-1");
let thread_store = test.codex.thread_extension_data();
assert_eq!(
thread_store
.get::<crate::config::GuardianV2Config>()
.expect("Guardian v2 configuration should be installed")
.max_parent_compaction_tokens,
384
);
tokio::time::timeout(Duration::from_secs(5), async {
while thread_store.get::<SecurityRiskScore>().is_none() {
tokio::task::yield_now().await;
}
})
.await?;
thread_store.insert(SecurityRiskScore {
scores: BTreeMap::from([("action_risk".to_owned(), 0.65)]),
sampled_at: None,
});
assert_eq!(
registry
.approval_review(&session_store, thread_store, "review action")
.await,
Some(ReviewDecision::Approved)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -59,6 +59,7 @@ pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig)
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
});
model_messages.instructions_template = Some(base_instructions.clone());
model_messages.instructions_variables = None;
@@ -201,6 +202,7 @@ fn local_model_messages_for_slug(slug: &str) -> ModelMessages {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
},
_ => ModelMessages {
instructions_template: Some(BASE_INSTRUCTIONS.to_string()),
@@ -211,6 +213,7 @@ fn local_model_messages_for_slug(slug: &str) -> ModelMessages {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
},
}
}
@@ -4,6 +4,7 @@ use codex_protocol::config_types::Personality;
use codex_protocol::openai_models::ApprovalMessages;
use codex_protocol::openai_models::AutoReviewMessages;
use codex_protocol::openai_models::CollaborationModeMessages;
use codex_protocol::openai_models::GuardianV2ModelConfig;
use codex_protocol::openai_models::ModelTokenBudgetConfig;
use codex_protocol::openai_models::MultiAgentMessages;
use codex_protocol::openai_models::MultiAgentModeMessages;
@@ -59,6 +60,10 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
auto_compact_fallback_prompt: "compact prompt".to_string(),
auto_compact_fallback_buffer_tokens: 64,
};
let guardian_v2 = GuardianV2ModelConfig {
classifier_instructions: Some("Guardian experiment".to_string()),
..Default::default()
};
model.model_messages = Some(ModelMessages {
instructions_template: Some("template".to_string()),
instructions_variables: Some(ModelInstructionsVariables {
@@ -72,6 +77,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
permissions: Some(permissions.clone()),
multi_agent: Some(multi_agent.clone()),
token_budget: Some(token_budget.clone()),
guardian_v2: Some(guardian_v2.clone()),
});
let config = ModelsManagerConfig {
base_instructions: Some(override_instructions.to_string()),
@@ -91,6 +97,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
permissions: Some(permissions),
multi_agent: Some(multi_agent),
token_budget: Some(token_budget),
guardian_v2: Some(guardian_v2),
})
);
assert_eq!(
@@ -121,6 +128,7 @@ fn disabled_personality_bakes_default_and_preserves_catalog_approval_messages()
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
});
let config = ModelsManagerConfig {
personality_enabled: false,
@@ -140,6 +148,7 @@ fn disabled_personality_bakes_default_and_preserves_catalog_approval_messages()
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
})
);
}
@@ -166,6 +175,7 @@ fn disabled_personality_uses_plain_base_instructions_for_local_personality_model
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}),
"unexpected model messages for {slug}"
);
@@ -206,6 +216,7 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
});
let updated = with_config_overrides(model, &config);
+29
View File
@@ -33,6 +33,12 @@ use crate::config_types::ServiceTier;
use crate::config_types::Verbosity;
use crate::protocol::MultiAgentVersion;
#[path = "openai_models/guardian_v2.rs"]
mod guardian_v2;
pub use guardian_v2::GuardianV2ModelConfig;
pub use guardian_v2::GuardianV2TranscriptModelConfig;
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
/// Backend model-catalog specialty identifying cybersecurity-focused models.
pub const MODEL_SPECIALTY_CYBER: &str = "cyber";
@@ -534,6 +540,8 @@ pub struct ModelMessages {
pub multi_agent: Option<MultiAgentMessages>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_budget: Option<ModelTokenBudgetConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guardian_v2: Option<GuardianV2ModelConfig>,
}
/// Model-owned defaults for the context-window token-budget feature.
@@ -758,6 +766,7 @@ where
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
});
messages.instructions_template = Some(base_instructions);
}
@@ -947,6 +956,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}
);
}
@@ -1088,6 +1098,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}
);
}
@@ -1169,6 +1180,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}));
let instructions = model.get_model_instructions(Some(Personality::Friendly));
@@ -1191,6 +1203,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}));
assert_eq!(
model.get_model_instructions(Some(Personality::Pragmatic)),
@@ -1214,6 +1227,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}));
assert_eq!(
model_no_personality.get_model_instructions(Some(Personality::Friendly)),
@@ -1248,6 +1262,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}));
let instructions = model.get_model_instructions(Some(Personality::Friendly));
@@ -1288,6 +1303,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
})
);
assert_eq!(
@@ -1339,6 +1355,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
}))],
};
@@ -1376,6 +1393,17 @@ mod tests {
}),
multi_agent: None,
token_budget: None,
guardian_v2: Some(GuardianV2ModelConfig {
classifier_instructions: Some("Guardian classification".to_string()),
review_threshold_basis_points: Some(7_500),
reasoning_effort: Some(ReasoningEffort::Minimal),
transcript: Some(GuardianV2TranscriptModelConfig {
sources: Some(vec!["reasoning".to_string()]),
max_tool_entry_tokens: Some(500),
..Default::default()
}),
..Default::default()
}),
};
let mut value = serde_json::to_value(ModelsResponse {
models: vec![test_model(Some(messages.clone()))],
@@ -1398,6 +1426,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
guardian_v2: None,
};
let mut value = serde_json::to_value(ModelsResponse {
models: vec![test_model(Some(canonical_messages.clone()))],
@@ -0,0 +1,42 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
use super::ReasoningEffort;
/// Optional model-owned defaults for Guardian v2 classification experiments.
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct GuardianV2ModelConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub classifier_instructions: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub review_threshold_basis_points: Option<u16>,
#[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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_action_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_classifier_instruction_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_parent_compaction_tokens: Option<usize>,
}
/// Optional model-owned defaults for selecting and bounding Guardian v2 history.
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
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 max_message_entry_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tool_entry_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_message_transcript_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tool_transcript_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_recent_non_user_entries: Option<usize>,
}