Use model catalog collaboration mode messages (#36351)

## What changed

- Add optional `default` and `plan` collaboration-mode messages to model catalog metadata.
- Prefer the active mode's catalog message, including an explicit empty value, while falling back to the existing developer instructions when the catalog variant is absent.
- Track the model in collaboration-mode world state so mode and model changes append the appropriate instructions, and clear prior instructions when no current message exists.
- Preserve catalog collaboration messages when instruction overrides remove other model instruction fields.

## Testing

- Cover catalog deserialization, variant selection, fallback and empty-message behavior, legacy snapshot migration, mode changes, and model changes.

GitOrigin-RevId: d4c6c0028423bc50b0e8175e55586f3e7bc1c3a8
This commit is contained in:
rhan-oai
2026-07-31 15:40:52 +00:00
committed by copyberry
parent 7b38c48da9
commit 2c005abb07
13 changed files with 556 additions and 36 deletions
@@ -3,36 +3,66 @@ use super::WorldStateSection;
use crate::context::ContextualUserFragment;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::openai_models::CollaborationModeMessages;
use codex_protocol::protocol::COLLABORATION_MODE_CLOSE_TAG;
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
use serde::Deserialize;
use serde::Serialize;
/// Collaboration-mode instructions currently visible to the model.
#[derive(Clone, Debug)]
pub(crate) struct CollaborationModeState {
mode: ModeKind,
instructions: String,
model: String,
instructions: Option<String>,
}
impl CollaborationModeState {
pub(crate) fn from_collaboration_mode(collaboration_mode: &CollaborationMode) -> Option<Self> {
collaboration_mode
.settings
.developer_instructions
.clone()
.filter(|instructions| !instructions.is_empty())
.map(|instructions| Self {
mode: collaboration_mode.mode,
instructions,
})
pub(crate) fn from_collaboration_mode(
collaboration_mode: &CollaborationMode,
catalog_messages: Option<&CollaborationModeMessages>,
) -> Self {
let catalog_instructions =
catalog_messages.and_then(|messages| match collaboration_mode.mode {
ModeKind::Default => messages.default.as_ref(),
ModeKind::Plan => messages.plan.as_ref(),
ModeKind::PairProgramming | ModeKind::Execute => None,
});
Self {
mode: collaboration_mode.mode,
model: collaboration_mode.settings.model.clone(),
instructions: catalog_instructions.cloned().or_else(|| {
collaboration_mode
.settings
.developer_instructions
.clone()
.filter(|instructions| !instructions.is_empty())
}),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum CollaborationModeSnapshot {
Current { mode: ModeKind, model: String },
Legacy(ModeKind),
}
impl WorldStateSection for CollaborationModeState {
const ID: &'static str = "collaboration_mode";
type Snapshot = ModeKind;
type Snapshot = CollaborationModeSnapshot;
fn snapshot(&self) -> Self::Snapshot {
self.mode
CollaborationModeSnapshot::Current {
mode: self.mode,
model: self.model.clone(),
}
}
fn should_persist(&self) -> bool {
self.instructions.is_some()
}
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
@@ -51,14 +81,18 @@ impl WorldStateSection for CollaborationModeState {
&self,
previous: PreviousSectionState<'_, Self::Snapshot>,
) -> Option<Box<dyn ContextualUserFragment>> {
if matches!(previous, PreviousSectionState::Known(previous) if previous == &self.mode)
|| matches!(previous, PreviousSectionState::Unknown)
if matches!(
previous,
PreviousSectionState::Known(CollaborationModeSnapshot::Current { mode, model })
if *mode == self.mode && model == &self.model
) || matches!(previous, PreviousSectionState::Unknown)
|| (self.instructions.is_none() && matches!(previous, PreviousSectionState::Absent))
{
return None;
}
Some(Box::new(CollaborationModeInstructions {
instructions: self.instructions.clone(),
instructions: self.instructions.clone().unwrap_or_default(),
}))
}
}
@@ -5,6 +5,8 @@ use crate::context::world_state::WorldState;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::CollaborationModeMessages;
use pretty_assertions::assert_eq;
#[test]
fn snapshots() {
@@ -31,7 +33,7 @@ fn snapshots() {
fn persisted_instructions_are_restored_only_when_missing_from_history() {
let state = collaboration_mode_state(ModeKind::Default, "pair with the user");
let retained: ResponseItem = ContextualUserFragment::into(CollaborationModeInstructions {
instructions: state.instructions.clone(),
instructions: state.instructions.clone().expect("test instructions"),
});
let mut world_state = WorldState::default();
world_state.add_section(state);
@@ -53,14 +55,107 @@ fn persisted_instructions_are_restored_only_when_missing_from_history() {
);
}
fn collaboration_mode_state(mode: ModeKind, instructions: &str) -> CollaborationModeState {
CollaborationModeState::from_collaboration_mode(&CollaborationMode {
#[test]
fn catalog_collaboration_messages_select_mode_variant() {
let messages = CollaborationModeMessages {
default: Some("catalog default instructions".to_string()),
plan: Some("catalog plan instructions".to_string()),
};
for (mode, expected) in [
(ModeKind::Default, "catalog default instructions"),
(ModeKind::Plan, "catalog plan instructions"),
] {
let state = CollaborationModeState::from_collaboration_mode(
&collaboration_mode(mode, Some("legacy instructions")),
Some(&messages),
);
assert_eq!(state.instructions.as_deref(), Some(expected));
}
}
#[test]
fn empty_catalog_collaboration_message_suppresses_legacy_instructions() {
let messages = CollaborationModeMessages {
default: None,
plan: Some(String::new()),
};
let state = CollaborationModeState::from_collaboration_mode(
&collaboration_mode(ModeKind::Plan, Some("legacy plan instructions")),
Some(&messages),
);
assert_eq!(
state
.render_diff(PreviousSectionState::Absent)
.expect("explicit empty collaboration message")
.render(),
format!("{COLLABORATION_MODE_OPEN_TAG}{COLLABORATION_MODE_CLOSE_TAG}")
);
}
#[test]
fn missing_catalog_collaboration_message_uses_legacy_instructions() {
let messages = CollaborationModeMessages {
default: Some("catalog default instructions".to_string()),
plan: None,
};
let state = CollaborationModeState::from_collaboration_mode(
&collaboration_mode(ModeKind::Plan, Some("legacy plan instructions")),
Some(&messages),
);
assert_eq!(
state.instructions.as_deref(),
Some("legacy plan instructions")
);
}
#[test]
fn legacy_collaboration_mode_snapshots_refresh_catalog_messages_once() {
let previous = serde_json::from_str::<CollaborationModeSnapshot>("\"default\"")
.expect("legacy collaboration mode snapshot");
for instructions in ["catalog instructions", ""] {
let messages = CollaborationModeMessages {
default: Some(instructions.to_string()),
plan: None,
};
let state = CollaborationModeState::from_collaboration_mode(
&collaboration_mode(ModeKind::Default, Some("stale legacy instructions")),
Some(&messages),
);
assert_eq!(
state
.render_diff(PreviousSectionState::Known(&previous))
.expect("legacy snapshot should refresh collaboration instructions")
.render(),
format!("{COLLABORATION_MODE_OPEN_TAG}{instructions}{COLLABORATION_MODE_CLOSE_TAG}")
);
assert!(
state
.render_diff(PreviousSectionState::Known(&state.snapshot()))
.is_none()
);
}
}
fn collaboration_mode(mode: ModeKind, instructions: Option<&str>) -> CollaborationMode {
CollaborationMode {
mode,
settings: Settings {
model: "test-model".to_string(),
reasoning_effort: None,
developer_instructions: Some(instructions.to_string()),
developer_instructions: instructions.map(str::to_string),
},
})
.expect("test collaboration mode should have instructions")
}
}
fn collaboration_mode_state(mode: ModeKind, instructions: &str) -> CollaborationModeState {
CollaborationModeState::from_collaboration_mode(
&collaboration_mode(mode, Some(instructions)),
/*catalog_messages*/ None,
)
}
@@ -5,17 +5,17 @@ expression: "render_section_cases(&[(Absent, Absent), (Absent, Known(&default)),
Absent -> Absent
None
Absent -> "default" (role - developer)
Absent -> {"mode":"default","model":"test-model"} (role - developer)
<collaboration_mode>pair with the user</collaboration_mode>
"default" -> "default"
{"mode":"default","model":"test-model"} -> {"mode":"default","model":"test-model"}
None
"default" -> "default"
{"mode":"default","model":"test-model"} -> {"mode":"default","model":"test-model"}
None
"default" -> "plan" (role - developer)
{"mode":"default","model":"test-model"} -> {"mode":"plan","model":"test-model"} (role - developer)
<collaboration_mode>make a plan</collaboration_mode>
Unknown -> "default"
Unknown -> {"mode":"default","model":"test-model"}
None
@@ -1453,6 +1453,7 @@ mod tests {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: Some(AutoReviewMessages {
policy: Some("Use the catalog Guardian policy.".to_string()),
policy_template: Some(catalog_template.to_string()),
@@ -1486,6 +1487,7 @@ mod tests {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: Some(AutoReviewMessages {
policy: Some(String::new()),
policy_template: None,
@@ -1527,6 +1529,7 @@ mod tests {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: Some(AutoReviewMessages {
policy: Some(catalog_policy.to_string()),
policy_template: Some(String::new()),
+9 -5
View File
@@ -129,11 +129,15 @@ impl Session {
.enabled(Feature::RequestPermissionsTool),
));
}
if turn_context.config.include_collaboration_mode_instructions
&& let Some(collaboration_mode) =
CollaborationModeState::from_collaboration_mode(&turn_context.collaboration_mode())
{
world_state.add_section(collaboration_mode);
if turn_context.config.include_collaboration_mode_instructions {
world_state.add_section(CollaborationModeState::from_collaboration_mode(
&turn_context.collaboration_mode(),
turn_context
.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.collaboration_modes.as_ref()),
));
}
if turn_context.config.include_environment_context {
let current_date = self
+22 -1
View File
@@ -47,6 +47,7 @@ use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::user_input::UserInput;
@@ -66,6 +67,7 @@ use crate::responses::output_value_to_text;
use crate::responses::start_mock_server;
use crate::streaming_sse::StreamingSseServer;
use crate::test_environment;
use crate::wait_for_event;
use crate::wait_for_event_match;
use crate::wait_for_event_with_timeout;
use wiremock::Match;
@@ -831,6 +833,25 @@ impl TestCodex {
.await
}
/// Submits a text turn without changing the current thread settings.
pub async fn submit_text_turn(&self, prompt: &str) -> Result<()> {
self.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: prompt.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides::default(),
})
.await?;
wait_for_event(&self.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
Ok(())
}
pub async fn submit_turn_with_permission_profile(
&self,
prompt: &str,
@@ -960,7 +981,7 @@ impl TestCodex {
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
thread_settings: ThreadSettingsOverrides {
environments: turn_environment_selections,
approval_policy: Some(approval_policy),
sandbox_policy: Some(sandbox_policy),
@@ -34,6 +34,7 @@ async fn catalog_permission_message_loaded_from_remote_models_is_sent() -> Resul
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: Some(PermissionMessages {
danger_full_access: None,
@@ -1,7 +1,11 @@
use anyhow::Result;
use codex_models_manager::model_info::model_info_from_slug;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::openai_models::CollaborationModeMessages;
use codex_protocol::openai_models::ModelMessages;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::COLLABORATION_MODE_CLOSE_TAG;
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
use codex_protocol::protocol::EventMsg;
@@ -37,6 +41,43 @@ fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMod
collab_mode_with_mode_and_instructions(ModeKind::Default, instructions)
}
fn collab_mode_for_model(
mode: ModeKind,
model: &str,
instructions: Option<&str>,
) -> CollaborationMode {
CollaborationMode {
mode,
settings: Settings {
model: model.to_string(),
reasoning_effort: None,
developer_instructions: instructions.map(str::to_string),
},
}
}
fn model_with_collaboration_messages(
slug: &str,
default: Option<&str>,
plan: Option<&str>,
) -> codex_protocol::openai_models::ModelInfo {
let mut model = model_info_from_slug(slug);
let model_messages = model.model_messages.get_or_insert(ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
});
model_messages.collaboration_modes = Some(CollaborationModeMessages {
default: default.map(str::to_string),
plan: plan.map(str::to_string),
});
model
}
fn developer_texts(input: &[Value]) -> Vec<String> {
input
.iter()
@@ -101,6 +142,227 @@ async fn no_collaboration_instructions_by_default() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn catalog_collaboration_messages_track_mode_changes() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let req1 = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let req2 = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
)
.await;
let model_slug = "catalog-collaboration-model";
let default_text = "catalog default instructions";
let plan_text = "catalog plan instructions";
let model = model_with_collaboration_messages(model_slug, Some(default_text), Some(plan_text));
let mut builder = test_codex()
.with_model(model_slug)
.with_config(move |config| {
config.model_catalog = Some(ModelsResponse {
models: vec![model],
});
});
let test = builder.build(&server).await?;
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model(
ModeKind::Default,
model_slug,
Some("legacy default instructions"),
)),
..Default::default()
},
)
.await?;
test.submit_text_turn("default turn").await?;
let first_dev_texts = developer_texts(&req1.single_request().input());
assert_eq!(
count_messages_containing(&first_dev_texts, &collab_xml(default_text)),
1
);
assert_eq!(
count_messages_containing(&first_dev_texts, "legacy default instructions"),
0
);
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model(
ModeKind::Plan,
model_slug,
Some("legacy plan instructions"),
)),
..Default::default()
},
)
.await?;
test.submit_text_turn("plan turn").await?;
let second_dev_texts = developer_texts(&req2.single_request().input());
assert_eq!(
count_messages_containing(&second_dev_texts, &collab_xml(default_text)),
1
);
assert_eq!(
count_messages_containing(&second_dev_texts, &collab_xml(plan_text)),
1
);
assert_eq!(
count_messages_containing(&second_dev_texts, "legacy plan instructions"),
0
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn missing_catalog_and_legacy_collaboration_message_clears_prior_instructions() -> Result<()>
{
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let _req1 = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let req2 = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
)
.await;
let model_slug = "catalog-collaboration-clear-model";
let default_text = "catalog default instructions";
let model =
model_with_collaboration_messages(model_slug, Some(default_text), /*plan*/ None);
let mut builder = test_codex()
.with_model(model_slug)
.with_config(move |config| {
config.model_catalog = Some(ModelsResponse {
models: vec![model],
});
});
let test = builder.build(&server).await?;
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model(
ModeKind::Default,
model_slug,
/*instructions*/ None,
)),
..Default::default()
},
)
.await?;
test.submit_text_turn("default turn").await?;
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model(
ModeKind::Plan,
model_slug,
/*instructions*/ None,
)),
..Default::default()
},
)
.await?;
test.submit_text_turn("plan turn").await?;
let dev_texts = developer_texts(&req2.single_request().input());
assert_eq!(
count_messages_containing(&dev_texts, &collab_xml(default_text)),
1
);
assert_eq!(count_messages_containing(&dev_texts, &collab_xml("")), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_change_appends_new_catalog_collaboration_message() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let _req1 = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let req2 = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
)
.await;
let first_slug = "catalog-collaboration-model-a";
let second_slug = "catalog-collaboration-model-b";
let first_text = "model A collaboration instructions";
let second_text = "model B collaboration instructions";
let first = model_with_collaboration_messages(first_slug, Some(first_text), /*plan*/ None);
let second =
model_with_collaboration_messages(second_slug, Some(second_text), /*plan*/ None);
let mut builder = test_codex()
.with_model(first_slug)
.with_config(move |config| {
config.model_catalog = Some(ModelsResponse {
models: vec![first, second],
});
});
let test = builder.build(&server).await?;
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_for_model(
ModeKind::Default,
first_slug,
Some("legacy instructions"),
)),
..Default::default()
},
)
.await?;
test.submit_text_turn("first").await?;
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some(second_slug.to_string()),
..Default::default()
},
)
.await?;
test.submit_text_turn("second").await?;
let dev_texts = developer_texts(&req2.single_request().input());
assert_eq!(
count_messages_containing(&dev_texts, &collab_xml(first_text)),
1
);
assert_eq!(
count_messages_containing(&dev_texts, &collab_xml(second_text)),
1
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn user_input_includes_collaboration_instructions_after_override() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -54,6 +54,7 @@ fn model_with_approval_messages(
never: None,
unless_trusted: None,
}),
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -70,6 +71,7 @@ fn model_with_permission_messages(
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: Some(permissions),
token_budget: None,
@@ -223,6 +225,7 @@ async fn catalog_non_on_request_approval_messages_are_sent_in_initial_permission
instructions_template: None,
instructions_variables: None,
approvals: Some(approvals),
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
+2
View File
@@ -640,6 +640,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
personality_pragmatic: Some("Pragmatic variant".to_string()),
}),
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -760,6 +761,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
personality_pragmatic: Some(remote_pragmatic_message.to_string()),
}),
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -113,6 +113,7 @@ fn clear_instruction_messages(model: &mut ModelInfo) {
model_messages.instructions_template = None;
model_messages.instructions_variables = None;
if model_messages.approvals.is_none()
&& model_messages.collaboration_modes.is_none()
&& model_messages.auto_review.is_none()
&& model_messages.permissions.is_none()
&& model_messages.token_budget.is_none()
@@ -180,6 +181,7 @@ fn local_personality_messages_for_slug(slug: &str) -> Option<ModelMessages> {
personality_pragmatic: Some(LOCAL_PRAGMATIC_TEMPLATE.to_string()),
}),
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -3,6 +3,7 @@ use crate::ModelsManagerConfig;
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::PermissionMessages;
use pretty_assertions::assert_eq;
@@ -31,6 +32,7 @@ fn base_instruction_override_preserves_catalog_approval_messages() {
personality_pragmatic: Some("pragmatic".to_string()),
}),
approvals: Some(approvals.clone()),
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -48,6 +50,7 @@ fn base_instruction_override_preserves_catalog_approval_messages() {
instructions_template: None,
instructions_variables: None,
approvals: Some(approvals),
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -55,6 +58,38 @@ fn base_instruction_override_preserves_catalog_approval_messages() {
);
}
#[test]
fn base_instruction_override_preserves_catalog_collaboration_mode_messages() {
let mut model = model_info_from_slug("gpt-5.2-codex");
let collaboration_modes = CollaborationModeMessages {
default: Some("default instructions".to_string()),
plan: Some("plan instructions".to_string()),
};
if let Some(model_messages) = model.model_messages.as_mut() {
model_messages.collaboration_modes = Some(collaboration_modes.clone());
}
let config = ModelsManagerConfig {
base_instructions: Some("override".to_string()),
..Default::default()
};
let updated = with_config_overrides(model, &config);
assert_eq!(
updated.model_messages,
Some(ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: Some(collaboration_modes),
auto_review: None,
permissions: None,
token_budget: None,
})
);
assert_eq!(updated.base_instructions, "override");
}
#[test]
fn disabled_personality_preserves_catalog_approval_messages() {
let mut model = model_info_from_slug("unknown-model");
@@ -68,6 +103,7 @@ fn disabled_personality_preserves_catalog_approval_messages() {
instructions_template: Some("template".to_string()),
instructions_variables: None,
approvals: Some(approvals.clone()),
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -85,6 +121,7 @@ fn disabled_personality_preserves_catalog_approval_messages() {
instructions_template: None,
instructions_variables: None,
approvals: Some(approvals),
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -103,6 +140,7 @@ fn base_instruction_override_preserves_catalog_auto_review_messages() {
instructions_template: Some("template".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: Some(auto_review.clone()),
permissions: None,
token_budget: None,
@@ -120,6 +158,7 @@ fn base_instruction_override_preserves_catalog_auto_review_messages() {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: Some(auto_review),
permissions: None,
token_budget: None,
@@ -139,6 +178,7 @@ fn base_instruction_override_preserves_catalog_permission_messages() {
instructions_template: Some("template".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: Some(permissions.clone()),
token_budget: None,
@@ -156,6 +196,7 @@ fn base_instruction_override_preserves_catalog_permission_messages() {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: Some(permissions),
token_budget: None,
@@ -193,6 +234,7 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
instructions_template: Some(instructions.to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
+54 -3
View File
@@ -507,6 +507,7 @@ pub struct ModelMessages {
pub instructions_template: Option<String>,
pub instructions_variables: Option<ModelInstructionsVariables>,
pub approvals: Option<ApprovalMessages>,
pub collaboration_modes: Option<CollaborationModeMessages>,
pub auto_review: Option<AutoReviewMessages>,
pub permissions: Option<PermissionMessages>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -531,6 +532,12 @@ pub struct ApprovalMessages {
pub unless_trusted: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct CollaborationModeMessages {
pub default: Option<String>,
pub plan: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct AutoReviewMessages {
pub policy: Option<String>,
@@ -763,13 +770,23 @@ mod tests {
}
#[test]
fn model_messages_deserialize_without_approvals() {
fn model_messages_deserialize_without_optional_sections() {
let messages: ModelMessages =
from_str(r#"{"instructions_template":null,"instructions_variables":null}"#)
.expect("model messages should deserialize");
assert_eq!(messages.approvals, None);
assert_eq!(messages.permissions, None);
assert_eq!(
messages,
ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
}
);
}
#[test]
@@ -860,6 +877,36 @@ mod tests {
);
}
#[test]
fn collaboration_mode_messages_preserve_missing_and_empty_values() {
let messages: ModelMessages = from_str(
r#"{
"instructions_template": null,
"instructions_variables": null,
"collaboration_modes": {
"default": ""
}
}"#,
)
.expect("collaboration mode messages should deserialize");
assert_eq!(
messages,
ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: Some(CollaborationModeMessages {
default: Some(String::new()),
plan: None,
}),
auto_review: None,
permissions: None,
token_budget: None,
}
);
}
#[test]
fn reasoning_effort_accepts_known_and_custom_values() {
let custom = ReasoningEffort::Custom("future".to_string());
@@ -932,6 +979,7 @@ mod tests {
instructions_template: Some("Hello {{ personality }}".to_string()),
instructions_variables: Some(personality_variables()),
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -952,6 +1000,7 @@ mod tests {
personality_pragmatic: None,
}),
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -981,6 +1030,7 @@ mod tests {
personality_pragmatic: None,
}),
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
@@ -1013,6 +1063,7 @@ mod tests {
personality_pragmatic: None,
}),
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,