mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
Preserve base instruction provenance across sessions (#37446)
## Why Persisted base instructions need to retain whether they were explicitly customized or generated from a model template. Without that distinction, forks and config-lock replays can treat model-generated instructions as custom and keep the wrong template after a model or personality change. ## What changed - Record custom or model provenance with base instructions in rollouts and config locks. - Preserve custom instructions across model changes, while allowing model-generated instructions to follow the selected model and personality. - Keep legacy rollouts without provenance compatible and propagate provenance through forks and spawned agents. ## Testing - Cover provenance serialization and legacy rollout decoding. - Cover role personality changes, forked model changes, config-lock replay, and multi-agent configuration. GitOrigin-RevId: 801c26559835f558027ce5112700c0cd31fcb21b
This commit is contained in:
1 parent
e58d9ef447
commit
964a227d8c
29 files changed
+324
-35
No files matched your search
BIN
Binary file not shown.
@@ -26,6 +26,7 @@ use codex_external_agent_migration::sessions::record_completed_session_imports;
|
|||||||
use codex_models_manager::manager::RefreshStrategy;
|
use codex_models_manager::manager::RefreshStrategy;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::models::BaseInstructions;
|
use codex_protocol::models::BaseInstructions;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
use codex_protocol::protocol::MultiAgentVersion;
|
use codex_protocol::protocol::MultiAgentVersion;
|
||||||
use codex_protocol::protocol::RolloutItem;
|
use codex_protocol::protocol::RolloutItem;
|
||||||
@@ -448,6 +449,17 @@ impl ExternalAgentSessionImporter {
|
|||||||
.base_instructions
|
.base_instructions
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||||
|
provenance: Some(config.base_instructions_provenance.clone().unwrap_or_else(
|
||||||
|
|| {
|
||||||
|
if config.base_instructions.is_some() {
|
||||||
|
BaseInstructionsProvenance::Custom
|
||||||
|
} else {
|
||||||
|
BaseInstructionsProvenance::Model {
|
||||||
|
model: model_info.slug.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
selected_capability_roots: Vec::new(),
|
selected_capability_roots: Vec::new(),
|
||||||
|
|||||||
@@ -624,6 +624,7 @@ mod tests {
|
|||||||
cwd: tempdir.path().to_path_buf(),
|
cwd: tempdir.path().to_path_buf(),
|
||||||
base_instructions: Some(codex_protocol::models::BaseInstructions {
|
base_instructions: Some(codex_protocol::models::BaseInstructions {
|
||||||
text: "actual developer prompt".to_string(),
|
text: "actual developer prompt".to_string(),
|
||||||
|
provenance: None,
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ use codex_protocol::config_types::Verbosity;
|
|||||||
use codex_protocol::config_types::WebSearchMode;
|
use codex_protocol::config_types::WebSearchMode;
|
||||||
use codex_protocol::config_types::WebSearchToolConfig;
|
use codex_protocol::config_types::WebSearchToolConfig;
|
||||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::openai_models::ReasoningEffort;
|
use codex_protocol::openai_models::ReasoningEffort;
|
||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
@@ -516,6 +517,10 @@ pub struct ConfigLockfileToml {
|
|||||||
pub version: u32,
|
pub version: u32,
|
||||||
pub codex_version: String,
|
pub codex_version: String,
|
||||||
|
|
||||||
|
/// Origin of the effective base instructions captured in the lockfile.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub base_instructions_provenance: Option<BaseInstructionsProvenance>,
|
||||||
|
|
||||||
/// Replayable effective config captured in the lockfile.
|
/// Replayable effective config captured in the lockfile.
|
||||||
pub config: ConfigToml,
|
pub config: ConfigToml,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ use codex_config::ConfigLayerStack;
|
|||||||
use codex_config::config_toml::ConfigToml;
|
use codex_config::config_toml::ConfigToml;
|
||||||
use codex_config::loader::resolve_relative_paths_in_config_toml;
|
use codex_config::loader::resolve_relative_paths_in_config_toml;
|
||||||
use codex_exec_server::LOCAL_FS;
|
use codex_exec_server::LOCAL_FS;
|
||||||
|
use codex_features::Feature;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -175,6 +177,8 @@ mod reload {
|
|||||||
let preserve_current_model = role_layer_toml.get("model").is_none();
|
let preserve_current_model = role_layer_toml.get("model").is_none();
|
||||||
let preserve_current_reasoning_effort =
|
let preserve_current_reasoning_effort =
|
||||||
role_layer_toml.get("model_reasoning_effort").is_none();
|
role_layer_toml.get("model_reasoning_effort").is_none();
|
||||||
|
let preserve_current_base_instructions = role_layer_toml.get("instructions").is_none()
|
||||||
|
&& role_layer_toml.get("model_instructions_file").is_none();
|
||||||
let mut overrides = reload_overrides(
|
let mut overrides = reload_overrides(
|
||||||
config,
|
config,
|
||||||
preserve_current_model,
|
preserve_current_model,
|
||||||
@@ -206,6 +210,24 @@ mod reload {
|
|||||||
.model_reasoning_effort
|
.model_reasoning_effort
|
||||||
.clone_from(&config.model_reasoning_effort);
|
.clone_from(&config.model_reasoning_effort);
|
||||||
}
|
}
|
||||||
|
if preserve_current_base_instructions {
|
||||||
|
let personality_changed = config.personality != next_config.personality
|
||||||
|
|| config.features.enabled(Feature::Personality)
|
||||||
|
!= next_config.features.enabled(Feature::Personality);
|
||||||
|
if personality_changed
|
||||||
|
&& matches!(
|
||||||
|
config.base_instructions_provenance,
|
||||||
|
Some(BaseInstructionsProvenance::Model { .. })
|
||||||
|
)
|
||||||
|
{
|
||||||
|
next_config.base_instructions = None;
|
||||||
|
next_config.base_instructions_provenance = None;
|
||||||
|
} else {
|
||||||
|
next_config.base_instructions = config.base_instructions.clone();
|
||||||
|
next_config.base_instructions_provenance =
|
||||||
|
config.base_instructions_provenance.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(next_config)
|
Ok(next_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::config::ConfigBuilder;
|
|||||||
use crate::plugins::plugins_manager_for_config;
|
use crate::plugins::plugins_manager_for_config;
|
||||||
use crate::skills_load_input_from_config;
|
use crate::skills_load_input_from_config;
|
||||||
use codex_protocol::config_types::ServiceTier;
|
use codex_protocol::config_types::ServiceTier;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::openai_models::ReasoningEffort;
|
use codex_protocol::openai_models::ReasoningEffort;
|
||||||
use codex_utils_absolute_path::test_support::PathExt;
|
use codex_utils_absolute_path::test_support::PathExt;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
@@ -179,6 +180,12 @@ async fn apply_role_preserves_unspecified_keys() {
|
|||||||
|
|
||||||
config.model = Some("spawn-model".to_string());
|
config.model = Some("spawn-model".to_string());
|
||||||
config.model_reasoning_effort = Some(ReasoningEffort::Low);
|
config.model_reasoning_effort = Some(ReasoningEffort::Low);
|
||||||
|
config.base_instructions = Some("inherited model instructions".to_string());
|
||||||
|
config.base_instructions_provenance = Some(BaseInstructionsProvenance::Model {
|
||||||
|
model: "parent-model".to_string(),
|
||||||
|
});
|
||||||
|
let base_instructions = config.base_instructions.clone();
|
||||||
|
let provenance = config.base_instructions_provenance.clone();
|
||||||
|
|
||||||
apply_role_to_config(&mut config, Some("custom"))
|
apply_role_to_config(&mut config, Some("custom"))
|
||||||
.await
|
.await
|
||||||
@@ -197,6 +204,66 @@ async fn apply_role_preserves_unspecified_keys() {
|
|||||||
Some(PathBuf::from("/tmp/codex-execve-wrapper"))
|
Some(PathBuf::from("/tmp/codex-execve-wrapper"))
|
||||||
);
|
);
|
||||||
assert!(config.psp);
|
assert!(config.psp);
|
||||||
|
assert_eq!(config.base_instructions, base_instructions);
|
||||||
|
assert_eq!(config.base_instructions_provenance, provenance);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn apply_role_regenerates_model_instructions_when_personality_changes() {
|
||||||
|
for (role_contents, provenance) in [
|
||||||
|
(
|
||||||
|
"personality = \"none\"",
|
||||||
|
BaseInstructionsProvenance::Model {
|
||||||
|
model: "parent-model".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"[features]\npersonality = false",
|
||||||
|
BaseInstructionsProvenance::Model {
|
||||||
|
model: "parent-model".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("personality = \"none\"", BaseInstructionsProvenance::Custom),
|
||||||
|
] {
|
||||||
|
let (home, mut config) = test_config_with_cli_overrides(vec![
|
||||||
|
(
|
||||||
|
"personality".to_string(),
|
||||||
|
TomlValue::String("friendly".to_string()),
|
||||||
|
),
|
||||||
|
("features.personality".to_string(), TomlValue::Boolean(true)),
|
||||||
|
])
|
||||||
|
.await;
|
||||||
|
let role_path = write_role_config(&home, "personality-role.toml", role_contents).await;
|
||||||
|
config.agent_roles.insert(
|
||||||
|
"custom".to_string(),
|
||||||
|
AgentRoleConfig {
|
||||||
|
description: None,
|
||||||
|
config_file: Some(role_path),
|
||||||
|
nickname_candidates: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
config.base_instructions = Some("inherited instructions".to_string());
|
||||||
|
config.base_instructions_provenance = Some(provenance.clone());
|
||||||
|
|
||||||
|
apply_role_to_config(&mut config, Some("custom"))
|
||||||
|
.await
|
||||||
|
.expect("custom role should apply");
|
||||||
|
|
||||||
|
let expected = match provenance {
|
||||||
|
BaseInstructionsProvenance::Model { .. } => (None, None),
|
||||||
|
BaseInstructionsProvenance::Custom => (
|
||||||
|
Some("inherited instructions".to_string()),
|
||||||
|
Some(BaseInstructionsProvenance::Custom),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
config.base_instructions,
|
||||||
|
config.base_instructions_provenance
|
||||||
|
),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ async fn compact_uses_bearer_after_agent_identity_session_fallback() -> anyhow::
|
|||||||
}],
|
}],
|
||||||
base_instructions: BaseInstructions {
|
base_instructions: BaseInstructions {
|
||||||
text: "base instructions".to_string(),
|
text: "base instructions".to_string(),
|
||||||
|
provenance: None,
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ use codex_protocol::config_types::WebSearchConfig;
|
|||||||
use codex_protocol::config_types::WebSearchMode;
|
use codex_protocol::config_types::WebSearchMode;
|
||||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||||
use codex_protocol::models::ActivePermissionProfile;
|
use codex_protocol::models::ActivePermissionProfile;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::models::SandboxEnforcement;
|
use codex_protocol::models::SandboxEnforcement;
|
||||||
use codex_protocol::openai_models::ModelsResponse;
|
use codex_protocol::openai_models::ModelsResponse;
|
||||||
@@ -689,6 +690,9 @@ pub struct Config {
|
|||||||
/// Base instructions override.
|
/// Base instructions override.
|
||||||
pub base_instructions: Option<String>,
|
pub base_instructions: Option<String>,
|
||||||
|
|
||||||
|
/// Origin of the configured base instructions when supplied by another session or lockfile.
|
||||||
|
pub base_instructions_provenance: Option<BaseInstructionsProvenance>,
|
||||||
|
|
||||||
/// Developer instructions override injected as a separate message.
|
/// Developer instructions override injected as a separate message.
|
||||||
pub developer_instructions: Option<String>,
|
pub developer_instructions: Option<String>,
|
||||||
|
|
||||||
@@ -1539,6 +1543,9 @@ impl ConfigBuilder {
|
|||||||
lock_config_layer_stack,
|
lock_config_layer_stack,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
if let Some(provenance) = lockfile_toml.base_instructions_provenance {
|
||||||
|
config.base_instructions_provenance = Some(provenance);
|
||||||
|
}
|
||||||
config.config_lock_toml = Some(Arc::new(expected_lock_config));
|
config.config_lock_toml = Some(Arc::new(expected_lock_config));
|
||||||
config.config_lock_allow_codex_version_mismatch = allow_codex_version_mismatch;
|
config.config_lock_allow_codex_version_mismatch = allow_codex_version_mismatch;
|
||||||
config.config_lock_save_fields_resolved_from_model_catalog =
|
config.config_lock_save_fields_resolved_from_model_catalog =
|
||||||
@@ -1641,7 +1648,12 @@ impl Config {
|
|||||||
model_context_window: self.model_context_window,
|
model_context_window: self.model_context_window,
|
||||||
model_auto_compact_token_limit: self.model_auto_compact_token_limit,
|
model_auto_compact_token_limit: self.model_auto_compact_token_limit,
|
||||||
tool_output_token_limit: self.tool_output_token_limit,
|
tool_output_token_limit: self.tool_output_token_limit,
|
||||||
base_instructions: self.base_instructions.clone(),
|
base_instructions: self.base_instructions.clone().filter(|_| {
|
||||||
|
!matches!(
|
||||||
|
self.base_instructions_provenance,
|
||||||
|
Some(BaseInstructionsProvenance::Model { .. })
|
||||||
|
)
|
||||||
|
}),
|
||||||
personality_enabled: self.features.enabled(Feature::Personality),
|
personality_enabled: self.features.enabled(Feature::Personality),
|
||||||
personality: self.personality,
|
personality: self.personality,
|
||||||
model_catalog: self.model_catalog.clone(),
|
model_catalog: self.model_catalog.clone(),
|
||||||
@@ -3922,6 +3934,9 @@ impl Config {
|
|||||||
let base_instructions = base_instructions
|
let base_instructions = base_instructions
|
||||||
.or(file_base_instructions)
|
.or(file_base_instructions)
|
||||||
.or(cfg.instructions.clone());
|
.or(cfg.instructions.clone());
|
||||||
|
let base_instructions_provenance = base_instructions
|
||||||
|
.as_ref()
|
||||||
|
.map(|_| BaseInstructionsProvenance::Custom);
|
||||||
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
|
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
|
||||||
let include_permissions_instructions = cfg.include_permissions_instructions.unwrap_or(true);
|
let include_permissions_instructions = cfg.include_permissions_instructions.unwrap_or(true);
|
||||||
let include_apps_instructions = cfg.include_apps_instructions.unwrap_or(true);
|
let include_apps_instructions = cfg.include_apps_instructions.unwrap_or(true);
|
||||||
@@ -4128,6 +4143,7 @@ impl Config {
|
|||||||
enforce_residency: enforce_residency.value,
|
enforce_residency: enforce_residency.value,
|
||||||
notify: cfg.notify,
|
notify: cfg.notify,
|
||||||
base_instructions,
|
base_instructions,
|
||||||
|
base_instructions_provenance,
|
||||||
personality,
|
personality,
|
||||||
developer_instructions,
|
developer_instructions,
|
||||||
compact_prompt,
|
compact_prompt,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub(crate) fn config_lockfile(config: ConfigToml) -> ConfigLockfileToml {
|
|||||||
ConfigLockfileToml {
|
ConfigLockfileToml {
|
||||||
version: CONFIG_LOCK_VERSION,
|
version: CONFIG_LOCK_VERSION,
|
||||||
codex_version: env!("CARGO_PKG_VERSION").to_string(),
|
codex_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
base_instructions_provenance: None,
|
||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,7 +62,10 @@ pub(crate) fn validate_config_lock_replay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let expected_lock = config_lock_for_comparison(expected_lock, options);
|
let expected_lock = config_lock_for_comparison(expected_lock, options);
|
||||||
let actual_lock = config_lock_for_comparison(actual_lock, options);
|
let mut actual_lock = config_lock_for_comparison(actual_lock, options);
|
||||||
|
if expected_lock.base_instructions_provenance.is_none() {
|
||||||
|
actual_lock.base_instructions_provenance = None;
|
||||||
|
}
|
||||||
if expected_lock != actual_lock {
|
if expected_lock != actual_lock {
|
||||||
let diff = compact_diff("config", &expected_lock, &actual_lock)
|
let diff = compact_diff("config", &expected_lock, &actual_lock)
|
||||||
.unwrap_or_else(|err| format!("failed to build config lock diff: {err}"));
|
.unwrap_or_else(|err| format!("failed to build config lock diff: {err}"));
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ impl ContextManager {
|
|||||||
let personality = turn_context.personality.or(turn_context.config.personality);
|
let personality = turn_context.personality.or(turn_context.config.personality);
|
||||||
let base_instructions = BaseInstructions {
|
let base_instructions = BaseInstructions {
|
||||||
text: model_info.get_model_instructions(personality),
|
text: model_info.get_model_instructions(personality),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
self.estimate_token_count_with_base_instructions(&base_instructions)
|
self.estimate_token_count_with_base_instructions(&base_instructions)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -828,9 +828,11 @@ fn estimate_token_count_with_base_instructions_uses_provided_text() {
|
|||||||
let history = create_history_with_items(vec![assistant_msg("hello from history")]);
|
let history = create_history_with_items(vec![assistant_msg("hello from history")]);
|
||||||
let short_base = BaseInstructions {
|
let short_base = BaseInstructions {
|
||||||
text: "short".to_string(),
|
text: "short".to_string(),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
let long_base = BaseInstructions {
|
let long_base = BaseInstructions {
|
||||||
text: "x".repeat(1_000),
|
text: "x".repeat(1_000),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let short_estimate = history
|
let short_estimate = history
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use codex_protocol::ThreadId;
|
|||||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||||
use codex_protocol::config_types::Personality;
|
use codex_protocol::config_types::Personality;
|
||||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::openai_models::ModelMessages;
|
use codex_protocol::openai_models::ModelMessages;
|
||||||
@@ -1028,6 +1029,7 @@ pub(crate) fn build_guardian_review_session_config(
|
|||||||
tenant_policy_config,
|
tenant_policy_config,
|
||||||
policy_template,
|
policy_template,
|
||||||
));
|
));
|
||||||
|
guardian_config.base_instructions_provenance = Some(BaseInstructionsProvenance::Custom);
|
||||||
guardian_config.notify = None;
|
guardian_config.notify = None;
|
||||||
guardian_config.developer_instructions = None;
|
guardian_config.developer_instructions = None;
|
||||||
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use codex_features::RolloutBudgetConfigToml;
|
|||||||
use codex_features::TokenBudgetConfigToml;
|
use codex_features::TokenBudgetConfigToml;
|
||||||
use codex_features::ToolRegistryConfigToml;
|
use codex_features::ToolRegistryConfigToml;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::config_lock::ConfigLockReplayOptions;
|
use crate::config_lock::ConfigLockReplayOptions;
|
||||||
@@ -25,6 +26,7 @@ use super::SessionConfiguration;
|
|||||||
|
|
||||||
pub(crate) async fn validate_config_lock_if_configured(
|
pub(crate) async fn validate_config_lock_if_configured(
|
||||||
session_configuration: &SessionConfiguration,
|
session_configuration: &SessionConfiguration,
|
||||||
|
base_instructions_provenance: Option<&BaseInstructionsProvenance>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if session_configuration.session_source.is_non_root_agent() {
|
if session_configuration.session_source.is_non_root_agent() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -36,7 +38,10 @@ pub(crate) async fn validate_config_lock_if_configured(
|
|||||||
else {
|
else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let actual = session_configuration.to_config_lockfile_toml()?;
|
let mut actual = session_configuration.to_config_lockfile_toml()?;
|
||||||
|
if actual.config.instructions.is_some() {
|
||||||
|
actual.base_instructions_provenance = base_instructions_provenance.cloned();
|
||||||
|
}
|
||||||
let config = session_configuration.original_config_do_not_use.as_ref();
|
let config = session_configuration.original_config_do_not_use.as_ref();
|
||||||
let options = ConfigLockReplayOptions {
|
let options = ConfigLockReplayOptions {
|
||||||
allow_codex_version_mismatch: config.config_lock_allow_codex_version_mismatch,
|
allow_codex_version_mismatch: config.config_lock_allow_codex_version_mismatch,
|
||||||
@@ -49,13 +54,17 @@ pub(crate) async fn validate_config_lock_if_configured(
|
|||||||
pub(crate) async fn export_config_lock_if_configured(
|
pub(crate) async fn export_config_lock_if_configured(
|
||||||
session_configuration: &SessionConfiguration,
|
session_configuration: &SessionConfiguration,
|
||||||
conversation_id: ThreadId,
|
conversation_id: ThreadId,
|
||||||
|
base_instructions_provenance: Option<&BaseInstructionsProvenance>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let config = session_configuration.original_config_do_not_use.as_ref();
|
let config = session_configuration.original_config_do_not_use.as_ref();
|
||||||
let Some(export_dir) = config.config_lock_export_dir.as_ref() else {
|
let Some(export_dir) = config.config_lock_export_dir.as_ref() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let lock = session_configuration.to_config_lockfile_toml()?;
|
let mut lock = session_configuration.to_config_lockfile_toml()?;
|
||||||
|
if lock.config.instructions.is_some() {
|
||||||
|
lock.base_instructions_provenance = base_instructions_provenance.cloned();
|
||||||
|
}
|
||||||
let lock = toml::to_string_pretty(&lock).context("failed to serialize config lock")?;
|
let lock = toml::to_string_pretty(&lock).context("failed to serialize config lock")?;
|
||||||
let path = export_dir.join(format!("{conversation_id}.config.lock.toml"));
|
let path = export_dir.join(format!("{conversation_id}.config.lock.toml"));
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ use codex_protocol::items::UserMessageItem;
|
|||||||
use codex_protocol::models::ActivePermissionProfile;
|
use codex_protocol::models::ActivePermissionProfile;
|
||||||
use codex_protocol::models::AdditionalPermissionProfile;
|
use codex_protocol::models::AdditionalPermissionProfile;
|
||||||
use codex_protocol::models::BaseInstructions;
|
use codex_protocol::models::BaseInstructions;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::models::SandboxEnforcement;
|
use codex_protocol::models::SandboxEnforcement;
|
||||||
use codex_protocol::openai_models::ModelInfo;
|
use codex_protocol::openai_models::ModelInfo;
|
||||||
@@ -1263,6 +1264,7 @@ impl Session {
|
|||||||
let state = self.state.lock().await;
|
let state = self.state.lock().await;
|
||||||
BaseInstructions {
|
BaseInstructions {
|
||||||
text: state.session_configuration.base_instructions.clone(),
|
text: state.session_configuration.base_instructions.clone(),
|
||||||
|
provenance: state.base_instructions_provenance.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -546,6 +546,27 @@ impl Session {
|
|||||||
session_configuration.collaboration_mode.model(),
|
session_configuration.collaboration_mode.model(),
|
||||||
session_configuration.provider
|
session_configuration.provider
|
||||||
);
|
);
|
||||||
|
let base_instructions_provenance = if config.base_instructions.is_some() {
|
||||||
|
Some(
|
||||||
|
config
|
||||||
|
.base_instructions_provenance
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(BaseInstructionsProvenance::Custom),
|
||||||
|
)
|
||||||
|
} else if let Some(inherited_base_instructions) = initial_history.get_base_instructions() {
|
||||||
|
let BaseInstructions { text, provenance } = inherited_base_instructions;
|
||||||
|
provenance.or_else(|| {
|
||||||
|
(text == model_info.get_model_instructions(config.personality)).then(|| {
|
||||||
|
BaseInstructionsProvenance::Model {
|
||||||
|
model: model_info.slug.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Some(BaseInstructionsProvenance::Model {
|
||||||
|
model: model_info.slug.clone(),
|
||||||
|
})
|
||||||
|
};
|
||||||
let forked_from_id = session_configuration
|
let forked_from_id = session_configuration
|
||||||
.forked_from_thread_id
|
.forked_from_thread_id
|
||||||
.or_else(|| initial_history.forked_from_id());
|
.or_else(|| initial_history.forked_from_id());
|
||||||
@@ -660,6 +681,7 @@ impl Session {
|
|||||||
originator: session_configuration.originator.clone(),
|
originator: session_configuration.originator.clone(),
|
||||||
base_instructions: BaseInstructions {
|
base_instructions: BaseInstructions {
|
||||||
text: session_configuration.base_instructions.clone(),
|
text: session_configuration.base_instructions.clone(),
|
||||||
|
provenance: base_instructions_provenance.clone(),
|
||||||
},
|
},
|
||||||
dynamic_tools: session_configuration.dynamic_tools.clone(),
|
dynamic_tools: session_configuration.dynamic_tools.clone(),
|
||||||
selected_capability_roots: selected_capability_roots.clone(),
|
selected_capability_roots: selected_capability_roots.clone(),
|
||||||
@@ -1012,16 +1034,22 @@ impl Session {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
session_configuration.thread_name = thread_name.clone();
|
session_configuration.thread_name = thread_name.clone();
|
||||||
validate_config_lock_if_configured(&session_configuration).await?;
|
validate_config_lock_if_configured(
|
||||||
export_config_lock_if_configured(&session_configuration, thread_id).await?;
|
&session_configuration,
|
||||||
|
base_instructions_provenance.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
export_config_lock_if_configured(
|
||||||
|
&session_configuration,
|
||||||
|
thread_id,
|
||||||
|
base_instructions_provenance.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let mut state = SessionState::new_with_auto_compact_window_ids(
|
let mut state = SessionState::new_with_auto_compact_window_ids(
|
||||||
session_configuration.clone(),
|
session_configuration.clone(),
|
||||||
initial_auto_compact_window_ids,
|
initial_auto_compact_window_ids,
|
||||||
);
|
);
|
||||||
state.base_instructions_model = (config.base_instructions.is_none()
|
state.base_instructions_provenance = base_instructions_provenance.clone();
|
||||||
&& session_configuration.base_instructions
|
|
||||||
== model_info.get_model_instructions(config.personality))
|
|
||||||
.then(|| model_info.slug.clone());
|
|
||||||
let managed_network_requirements_configured = config
|
let managed_network_requirements_configured = config
|
||||||
.config_layer_stack
|
.config_layer_stack
|
||||||
.requirements_toml()
|
.requirements_toml()
|
||||||
|
|||||||
@@ -2647,6 +2647,7 @@ async fn recompute_token_usage_uses_session_base_instructions() {
|
|||||||
let history = session.clone_history().await;
|
let history = session.clone_history().await;
|
||||||
let session_base_instructions = BaseInstructions {
|
let session_base_instructions = BaseInstructions {
|
||||||
text: override_instructions,
|
text: override_instructions,
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
let expected_tokens = history
|
let expected_tokens = history
|
||||||
.estimate_token_count_with_base_instructions(&session_base_instructions)
|
.estimate_token_count_with_base_instructions(&session_base_instructions)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ use codex_extension_api::WorldStateContributionInput;
|
|||||||
use codex_features::Feature;
|
use codex_features::Feature;
|
||||||
use codex_protocol::error::CodexErr;
|
use codex_protocol::error::CodexErr;
|
||||||
use codex_protocol::error::Result as CodexResult;
|
use codex_protocol::error::Result as CodexResult;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
#[tracing::instrument(name = "world_state.build", level = "info", skip_all)]
|
#[tracing::instrument(name = "world_state.build", level = "info", skip_all)]
|
||||||
@@ -50,8 +51,12 @@ impl Session {
|
|||||||
.map(|previous| previous.model)
|
.map(|previous| previous.model)
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
state
|
state
|
||||||
.base_instructions_model
|
.base_instructions_provenance
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
.and_then(|provenance| match provenance {
|
||||||
|
BaseInstructionsProvenance::Model { model } => Some(model),
|
||||||
|
BaseInstructionsProvenance::Custom => None,
|
||||||
|
})
|
||||||
.filter(|_| base_instructions != model_instructions)
|
.filter(|_| base_instructions != model_instructions)
|
||||||
.cloned()
|
.cloned()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ async fn schedule_startup_prewarm_inner(
|
|||||||
startup_turn_context.as_ref(),
|
startup_turn_context.as_ref(),
|
||||||
BaseInstructions {
|
BaseInstructions {
|
||||||
text: base_instructions,
|
text: base_instructions,
|
||||||
|
provenance: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
startup_turn_context.session_telemetry.record_startup_phase(
|
startup_turn_context.session_telemetry.record_startup_phase(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! Session-wide mutable state.
|
//! Session-wide mutable state.
|
||||||
|
|
||||||
use codex_protocol::models::AdditionalPermissionProfile;
|
use codex_protocol::models::AdditionalPermissionProfile;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -25,8 +26,8 @@ use codex_utils_output_truncation::TruncationPolicy;
|
|||||||
/// Persistent, session-scoped state previously stored directly on `Session`.
|
/// Persistent, session-scoped state previously stored directly on `Session`.
|
||||||
pub(crate) struct SessionState {
|
pub(crate) struct SessionState {
|
||||||
pub(crate) session_configuration: SessionConfiguration,
|
pub(crate) session_configuration: SessionConfiguration,
|
||||||
/// Model that generated the base instructions, when their provenance is known.
|
/// Persisted origin of the session base instructions, when known.
|
||||||
pub(crate) base_instructions_model: Option<String>,
|
pub(crate) base_instructions_provenance: Option<BaseInstructionsProvenance>,
|
||||||
pub(crate) history: ContextManager,
|
pub(crate) history: ContextManager,
|
||||||
pub(crate) latest_rate_limits: Option<RateLimitSnapshot>,
|
pub(crate) latest_rate_limits: Option<RateLimitSnapshot>,
|
||||||
pub(crate) server_reasoning_included: bool,
|
pub(crate) server_reasoning_included: bool,
|
||||||
@@ -64,7 +65,7 @@ impl SessionState {
|
|||||||
let history = ContextManager::new();
|
let history = ContextManager::new();
|
||||||
Self {
|
Self {
|
||||||
session_configuration,
|
session_configuration,
|
||||||
base_instructions_model: None,
|
base_instructions_provenance: None,
|
||||||
history,
|
history,
|
||||||
latest_rate_limits: None,
|
latest_rate_limits: None,
|
||||||
server_reasoning_included: false,
|
server_reasoning_included: false,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use codex_protocol::ResponseItemId;
|
|||||||
use codex_protocol::config_types::WebSearchMode;
|
use codex_protocol::config_types::WebSearchMode;
|
||||||
use codex_protocol::items::ExitedReviewModeItem;
|
use codex_protocol::items::ExitedReviewModeItem;
|
||||||
use codex_protocol::items::TurnItem;
|
use codex_protocol::items::TurnItem;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::protocol::AgentMessageContentDeltaEvent;
|
use codex_protocol::protocol::AgentMessageContentDeltaEvent;
|
||||||
@@ -113,6 +114,7 @@ async fn start_review_conversation(
|
|||||||
|
|
||||||
// Set explicit review rubric for the sub-agent
|
// Set explicit review rubric for the sub-agent
|
||||||
sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string());
|
sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string());
|
||||||
|
sub_agent_config.base_instructions_provenance = Some(BaseInstructionsProvenance::Custom);
|
||||||
sub_agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
sub_agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||||
|
|
||||||
let model = config
|
let model = config
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ pub(crate) fn build_agent_spawn_config(
|
|||||||
) -> Result<Config, FunctionCallError> {
|
) -> Result<Config, FunctionCallError> {
|
||||||
let mut config = build_agent_shared_config(turn, environment)?;
|
let mut config = build_agent_shared_config(turn, environment)?;
|
||||||
config.base_instructions = Some(base_instructions.text.clone());
|
config.base_instructions = Some(base_instructions.text.clone());
|
||||||
|
config.base_instructions_provenance = base_instructions.provenance.clone();
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +193,7 @@ pub(crate) fn build_agent_resume_config(
|
|||||||
let mut config = build_agent_shared_config(turn, environment)?;
|
let mut config = build_agent_shared_config(turn, environment)?;
|
||||||
// For resume, keep base instructions sourced from rollout/session metadata.
|
// For resume, keep base instructions sourced from rollout/session metadata.
|
||||||
config.base_instructions = None;
|
config.base_instructions = None;
|
||||||
|
config.base_instructions_provenance = None;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ use codex_protocol::config_types::ServiceTier;
|
|||||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||||
use codex_protocol::mcp::ClientMcpExtensions;
|
use codex_protocol::mcp::ClientMcpExtensions;
|
||||||
use codex_protocol::models::BaseInstructions;
|
use codex_protocol::models::BaseInstructions;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::FunctionCallOutputBody;
|
use codex_protocol::models::FunctionCallOutputBody;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
@@ -4442,6 +4443,9 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
|||||||
let (_session, mut turn) = make_session_and_context().await;
|
let (_session, mut turn) = make_session_and_context().await;
|
||||||
let base_instructions = BaseInstructions {
|
let base_instructions = BaseInstructions {
|
||||||
text: "base".to_string(),
|
text: "base".to_string(),
|
||||||
|
provenance: Some(BaseInstructionsProvenance::Model {
|
||||||
|
model: turn.model_info.slug.clone(),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
turn.developer_instructions = Some("dev".to_string());
|
turn.developer_instructions = Some("dev".to_string());
|
||||||
let mut config = (*turn.config).clone();
|
let mut config = (*turn.config).clone();
|
||||||
@@ -4486,6 +4490,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
|||||||
let config = build_agent_spawn_config(&base_instructions, &turn, turn.environments.primary())
|
let config = build_agent_spawn_config(&base_instructions, &turn, turn.environments.primary())
|
||||||
.expect("spawn config");
|
.expect("spawn config");
|
||||||
let mut expected = (*turn.config).clone();
|
let mut expected = (*turn.config).clone();
|
||||||
|
expected.base_instructions_provenance = base_instructions.provenance.clone();
|
||||||
expected.base_instructions = Some(base_instructions.text);
|
expected.base_instructions = Some(base_instructions.text);
|
||||||
expected.model = Some(turn.model_info.slug.clone());
|
expected.model = Some(turn.model_info.slug.clone());
|
||||||
expected.model_provider = turn.provider.info().clone();
|
expected.model_provider = turn.provider.info().clone();
|
||||||
@@ -4513,6 +4518,9 @@ async fn build_agent_resume_config_clears_base_instructions() {
|
|||||||
let (_session, mut turn) = make_session_and_context().await;
|
let (_session, mut turn) = make_session_and_context().await;
|
||||||
let mut base_config = (*turn.config).clone();
|
let mut base_config = (*turn.config).clone();
|
||||||
base_config.base_instructions = Some("caller-base".to_string());
|
base_config.base_instructions = Some("caller-base".to_string());
|
||||||
|
base_config.base_instructions_provenance = Some(BaseInstructionsProvenance::Model {
|
||||||
|
model: turn.model_info.slug.clone(),
|
||||||
|
});
|
||||||
turn.config = Arc::new(base_config);
|
turn.config = Arc::new(base_config);
|
||||||
Arc::make_mut(&mut turn.config)
|
Arc::make_mut(&mut turn.config)
|
||||||
.permissions
|
.permissions
|
||||||
@@ -4541,6 +4549,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
|
|||||||
|
|
||||||
let mut expected = (*turn.config).clone();
|
let mut expected = (*turn.config).clone();
|
||||||
expected.base_instructions = None;
|
expected.base_instructions = None;
|
||||||
|
expected.base_instructions_provenance = None;
|
||||||
expected.model = Some(turn.model_info.slug.clone());
|
expected.model = Some(turn.model_info.slug.clone());
|
||||||
expected.model_provider = turn.provider.info().clone();
|
expected.model_provider = turn.provider.info().clone();
|
||||||
expected.model_reasoning_effort = turn.reasoning_effort.clone();
|
expected.model_reasoning_effort = turn.reasoning_effort.clone();
|
||||||
|
|||||||
@@ -2363,6 +2363,7 @@ fn prompt_with_input_and_instructions(input: Vec<ResponseItem>, instructions: &s
|
|||||||
let mut prompt = prompt_with_input(input);
|
let mut prompt = prompt_with_input(input);
|
||||||
prompt.base_instructions = BaseInstructions {
|
prompt.base_instructions = BaseInstructions {
|
||||||
text: instructions.to_string(),
|
text: instructions.to_string(),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
prompt
|
prompt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use codex_models_manager::manager::RefreshStrategy;
|
|||||||
use codex_protocol::config_types::ReasoningSummary;
|
use codex_protocol::config_types::ReasoningSummary;
|
||||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||||
use codex_protocol::config_types::ServiceTier;
|
use codex_protocol::config_types::ServiceTier;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::openai_models::ConfigShellToolType;
|
use codex_protocol::openai_models::ConfigShellToolType;
|
||||||
use codex_protocol::openai_models::InputModality;
|
use codex_protocol::openai_models::InputModality;
|
||||||
@@ -202,11 +203,14 @@ async fn first_turn_model_change_appends_model_instructions_developer_message(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_case(None; "model-generated base instructions")]
|
#[test_case(None, "gpt-5.2"; "model-generated base instructions and original model")]
|
||||||
#[test_case(Some("inherited custom base instructions"); "custom base instructions")]
|
#[test_case(None, "gpt-5.4"; "model-generated base instructions and fork model")]
|
||||||
|
#[test_case(Some("inherited custom base instructions"), "gpt-5.2"; "custom base instructions and original model")]
|
||||||
|
#[test_case(Some("inherited custom base instructions"), "gpt-5.4"; "custom base instructions and fork model")]
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn first_turn_after_empty_prefix_fork_preserves_inherited_base_instructions(
|
async fn first_turn_after_empty_prefix_fork_preserves_inherited_base_instructions(
|
||||||
custom_base_instructions: Option<&'static str>,
|
custom_base_instructions: Option<&'static str>,
|
||||||
|
turn_model: &'static str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
@@ -222,6 +226,21 @@ async fn first_turn_after_empty_prefix_fork_preserves_inherited_base_instruction
|
|||||||
let test = builder.build_with_auto_env(&server).await?;
|
let test = builder.build_with_auto_env(&server).await?;
|
||||||
test.codex.ensure_rollout_materialized().await;
|
test.codex.ensure_rollout_materialized().await;
|
||||||
test.codex.flush_rollout().await?;
|
test.codex.flush_rollout().await?;
|
||||||
|
let source_rollout_path = test.codex.rollout_path().expect("rollout path");
|
||||||
|
let source_history =
|
||||||
|
codex_rollout::RolloutRecorder::get_rollout_history(&source_rollout_path).await?;
|
||||||
|
let expected_provenance = match custom_base_instructions {
|
||||||
|
Some(_) => BaseInstructionsProvenance::Custom,
|
||||||
|
None => BaseInstructionsProvenance::Model {
|
||||||
|
model: initial_model.to_string(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
source_history
|
||||||
|
.get_base_instructions()
|
||||||
|
.and_then(|instructions| instructions.provenance),
|
||||||
|
Some(expected_provenance)
|
||||||
|
);
|
||||||
|
|
||||||
let mut fork_config = test.config.clone();
|
let mut fork_config = test.config.clone();
|
||||||
fork_config.model = Some("gpt-5.4".to_string());
|
fork_config.model = Some("gpt-5.4".to_string());
|
||||||
@@ -231,29 +250,27 @@ async fn first_turn_after_empty_prefix_fork_preserves_inherited_base_instruction
|
|||||||
.fork_thread(
|
.fork_thread(
|
||||||
ForkSnapshot::TruncateBeforeNthUserMessage(0),
|
ForkSnapshot::TruncateBeforeNthUserMessage(0),
|
||||||
fork_config,
|
fork_config,
|
||||||
test.codex.rollout_path().expect("rollout path"),
|
source_rollout_path,
|
||||||
/*thread_source*/ None,
|
/*thread_source*/ None,
|
||||||
/*parent_trace*/ None,
|
/*parent_trace*/ None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
submit_model_turn(
|
submit_model_turn(&fork.thread, turn_model, ThreadSettingsOverrides::default()).await?;
|
||||||
&fork.thread,
|
|
||||||
initial_model,
|
|
||||||
ThreadSettingsOverrides::default(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let request = resp_mock.single_request();
|
let request = resp_mock.single_request();
|
||||||
assert_eq!(request.body_json()["model"], initial_model);
|
assert_eq!(request.body_json()["model"], turn_model);
|
||||||
if let Some(instructions) = custom_base_instructions {
|
if let Some(instructions) = custom_base_instructions {
|
||||||
assert_eq!(request.instructions_text(), instructions);
|
assert_eq!(request.instructions_text(), instructions);
|
||||||
}
|
}
|
||||||
assert!(
|
let model_switch_count = request
|
||||||
request
|
.message_input_texts("developer")
|
||||||
.message_input_texts("developer")
|
.iter()
|
||||||
.iter()
|
.filter(|text| text.contains("<model_switch>"))
|
||||||
.all(|text| !text.contains("<model_switch>")),
|
.count();
|
||||||
"the inherited base instructions must not be replaced by catalog instructions"
|
assert_eq!(
|
||||||
|
model_switch_count,
|
||||||
|
usize::from(custom_base_instructions.is_none() && turn_model != initial_model),
|
||||||
|
"only inherited model-generated instructions should change models"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -304,7 +321,7 @@ async fn rollback_first_turn_model_change_removes_its_instructions(
|
|||||||
|
|
||||||
let test = match followup {
|
let test = match followup {
|
||||||
RollbackFollowup::ColdResume => {
|
RollbackFollowup::ColdResume => {
|
||||||
let mut resume_builder = test_codex().with_model(initial_model);
|
let mut resume_builder = test_codex().with_model(switched_model);
|
||||||
resume_builder.restart(&server, &test).await?
|
resume_builder.restart(&server, &test).await?
|
||||||
}
|
}
|
||||||
RollbackFollowup::StartupModel | RollbackFollowup::SwitchedModel => test,
|
RollbackFollowup::StartupModel | RollbackFollowup::SwitchedModel => test,
|
||||||
|
|||||||
@@ -470,18 +470,34 @@ async fn token_budget_model_defaults_survive_config_lock_replay() -> Result<()>
|
|||||||
})
|
})
|
||||||
.build_with_auto_env(&server)
|
.build_with_auto_env(&server)
|
||||||
.await?;
|
.await?;
|
||||||
|
core_test_support::submit_thread_settings(
|
||||||
|
&replay.codex,
|
||||||
|
ThreadSettingsOverrides {
|
||||||
|
model: Some("gpt-5.4".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
replay
|
replay
|
||||||
.submit_turn("inspect guidance after lock replay")
|
.submit_text_turn("inspect guidance after lock replay and model switch")
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let requests = responses.requests();
|
let requests = responses.requests();
|
||||||
assert_eq!(requests.len(), 2);
|
assert_eq!(requests.len(), 2);
|
||||||
for request in requests {
|
for request in &requests {
|
||||||
assert!(
|
assert!(
|
||||||
request.body_contains_text("Use the model-owned context-window guidance."),
|
request.body_contains_text("Use the model-owned context-window guidance."),
|
||||||
"exporting and replaying a config lock must preserve model-owned guidance"
|
"exporting and replaying a config lock must preserve model-owned guidance"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
assert_eq!(requests[1].body_json()["model"], "gpt-5.4");
|
||||||
|
assert!(
|
||||||
|
requests[1]
|
||||||
|
.message_input_texts("developer")
|
||||||
|
.iter()
|
||||||
|
.any(|text| text.contains("<model_switch>")),
|
||||||
|
"replaying model-owned instructions must not override the new model's template"
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ mod job {
|
|||||||
}];
|
}];
|
||||||
prompt.base_instructions = BaseInstructions {
|
prompt.base_instructions = BaseInstructions {
|
||||||
text: crate::stage_one::PROMPT.to_string(),
|
text: crate::stage_one::PROMPT.to_string(),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
prompt.output_schema = Some(output_schema());
|
prompt.output_schema = Some(output_schema());
|
||||||
prompt.output_schema_strict = true;
|
prompt.output_schema_strict = true;
|
||||||
|
|||||||
@@ -1272,17 +1272,33 @@ impl ResponseItem {
|
|||||||
|
|
||||||
pub const BASE_INSTRUCTIONS_DEFAULT: &str = include_str!("prompts/base_instructions/default.md");
|
pub const BASE_INSTRUCTIONS_DEFAULT: &str = include_str!("prompts/base_instructions/default.md");
|
||||||
|
|
||||||
|
/// Describes whether persisted base instructions were supplied by the user or generated for a model.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
#[ts(tag = "type")]
|
||||||
|
pub enum BaseInstructionsProvenance {
|
||||||
|
/// The instructions were explicitly configured and must survive model changes unchanged.
|
||||||
|
Custom,
|
||||||
|
/// The instructions were generated from this model's instruction template.
|
||||||
|
Model { model: String },
|
||||||
|
}
|
||||||
|
|
||||||
/// Base instructions for the model in a thread. Corresponds to the `instructions` field in the ResponsesAPI.
|
/// Base instructions for the model in a thread. Corresponds to the `instructions` field in the ResponsesAPI.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(rename = "base_instructions", rename_all = "snake_case")]
|
#[serde(rename = "base_instructions", rename_all = "snake_case")]
|
||||||
pub struct BaseInstructions {
|
pub struct BaseInstructions {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
/// Missing on rollouts written before base-instruction provenance was persisted.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[ts(optional)]
|
||||||
|
pub provenance: Option<BaseInstructionsProvenance>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BaseInstructions {
|
impl Default for BaseInstructions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: BASE_INSTRUCTIONS_DEFAULT.to_string(),
|
text: BASE_INSTRUCTIONS_DEFAULT.to_string(),
|
||||||
|
provenance: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2230,6 +2246,31 @@ mod tests {
|
|||||||
1, 122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
1, 122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn base_instructions_preserve_provenance_and_accept_legacy_rollouts() -> Result<()> {
|
||||||
|
let legacy: BaseInstructions =
|
||||||
|
serde_json::from_value(serde_json::json!({ "text": "legacy instructions" }))?;
|
||||||
|
assert_eq!(legacy.provenance, None);
|
||||||
|
|
||||||
|
for provenance in [
|
||||||
|
BaseInstructionsProvenance::Custom,
|
||||||
|
BaseInstructionsProvenance::Model {
|
||||||
|
model: "gpt-5.2".to_string(),
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
let instructions = BaseInstructions {
|
||||||
|
text: "persisted instructions".to_string(),
|
||||||
|
provenance: Some(provenance),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_value::<BaseInstructions>(serde_json::to_value(&instructions)?)?,
|
||||||
|
instructions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn plaintext_agent_message_content_rejects_mixed_encrypted_content() {
|
fn plaintext_agent_message_content_rejects_mixed_encrypted_content() {
|
||||||
let content = vec![
|
let content = vec![
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
|||||||
hide_agent_reasoning: false,
|
hide_agent_reasoning: false,
|
||||||
show_raw_agent_reasoning: false,
|
show_raw_agent_reasoning: false,
|
||||||
base_instructions: None,
|
base_instructions: None,
|
||||||
|
base_instructions_provenance: None,
|
||||||
developer_instructions: None,
|
developer_instructions: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
include_permissions_instructions: false,
|
include_permissions_instructions: false,
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ use codex_protocol::ThreadId;
|
|||||||
use codex_protocol::approvals::GuardianAssessmentEvent;
|
use codex_protocol::approvals::GuardianAssessmentEvent;
|
||||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||||
use codex_protocol::models::ActivePermissionProfile;
|
use codex_protocol::models::ActivePermissionProfile;
|
||||||
|
use codex_protocol::models::BaseInstructionsProvenance;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::openai_models::ModelAvailabilityNux;
|
use codex_protocol::openai_models::ModelAvailabilityNux;
|
||||||
@@ -1808,7 +1809,12 @@ fn thread_fork_params_from_config(
|
|||||||
sandbox,
|
sandbox,
|
||||||
permissions,
|
permissions,
|
||||||
config: config_request_overrides_from_config(&config),
|
config: config_request_overrides_from_config(&config),
|
||||||
base_instructions: config.base_instructions.clone(),
|
base_instructions: config.base_instructions.clone().filter(|_| {
|
||||||
|
!matches!(
|
||||||
|
config.base_instructions_provenance,
|
||||||
|
Some(BaseInstructionsProvenance::Model { .. })
|
||||||
|
)
|
||||||
|
}),
|
||||||
developer_instructions: with_terminal_visualization_instructions(
|
developer_instructions: with_terminal_visualization_instructions(
|
||||||
&config,
|
&config,
|
||||||
config.developer_instructions.clone(),
|
config.developer_instructions.clone(),
|
||||||
@@ -2918,11 +2924,12 @@ mod tests {
|
|||||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||||
let mut config = build_config(&temp_dir).await;
|
let mut config = build_config(&temp_dir).await;
|
||||||
config.base_instructions = Some("Base override.".to_string());
|
config.base_instructions = Some("Base override.".to_string());
|
||||||
|
config.base_instructions_provenance = Some(BaseInstructionsProvenance::Custom);
|
||||||
config.developer_instructions = Some("Developer override.".to_string());
|
config.developer_instructions = Some("Developer override.".to_string());
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
|
|
||||||
let params = thread_fork_params_from_config(
|
let params = thread_fork_params_from_config(
|
||||||
config,
|
config.clone(),
|
||||||
thread_id,
|
thread_id,
|
||||||
ThreadParamsMode::Embedded,
|
ThreadParamsMode::Embedded,
|
||||||
/*remote_cwd_override*/ None,
|
/*remote_cwd_override*/ None,
|
||||||
@@ -2933,6 +2940,18 @@ mod tests {
|
|||||||
params.developer_instructions.as_deref(),
|
params.developer_instructions.as_deref(),
|
||||||
Some("Developer override.")
|
Some("Developer override.")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
config.base_instructions_provenance = Some(BaseInstructionsProvenance::Model {
|
||||||
|
model: "gpt-5.2".to_string(),
|
||||||
|
});
|
||||||
|
let params = thread_fork_params_from_config(
|
||||||
|
config,
|
||||||
|
thread_id,
|
||||||
|
ThreadParamsMode::Remote,
|
||||||
|
/*remote_cwd_override*/ None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(params.base_instructions, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in new issue
Block a user