mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
feat: adopt global instructions contributors
This commit is contained in:
Generated
+3
@@ -1978,6 +1978,7 @@ dependencies = [
|
||||
"codex-git-utils",
|
||||
"codex-goal-extension",
|
||||
"codex-guardian",
|
||||
"codex-home",
|
||||
"codex-hooks",
|
||||
"codex-image-generation-extension",
|
||||
"codex-login",
|
||||
@@ -2598,6 +2599,7 @@ dependencies = [
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-git-utils",
|
||||
"codex-home",
|
||||
"codex-hooks",
|
||||
"codex-image-generation-extension",
|
||||
"codex-install-context",
|
||||
@@ -3266,6 +3268,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
"codex-home",
|
||||
"codex-login",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
|
||||
+2
-2
@@ -37,8 +37,8 @@ members = [
|
||||
"core-api",
|
||||
"core-plugins",
|
||||
"core-skills",
|
||||
"home",
|
||||
"hooks",
|
||||
"home",
|
||||
"secrets",
|
||||
"exec",
|
||||
"file-system",
|
||||
@@ -170,7 +170,6 @@ codex-execpolicy = { path = "execpolicy" }
|
||||
codex-extension-api = { path = "ext/extension-api" }
|
||||
codex-goal-extension = { path = "ext/goal" }
|
||||
codex-guardian = { path = "ext/guardian" }
|
||||
codex-home = { path = "home" }
|
||||
codex-image-generation-extension = { path = "ext/image-generation" }
|
||||
codex-external-agent-migration = { path = "external-agent-migration" }
|
||||
codex-external-agent-sessions = { path = "external-agent-sessions" }
|
||||
@@ -182,6 +181,7 @@ codex-file-search = { path = "file-search" }
|
||||
codex-file-watcher = { path = "file-watcher" }
|
||||
codex-git-utils = { path = "git-utils" }
|
||||
codex-hooks = { path = "hooks" }
|
||||
codex-home = { path = "home" }
|
||||
codex-keyring-store = { path = "keyring-store" }
|
||||
codex-linux-sandbox = { path = "linux-sandbox" }
|
||||
codex-lmstudio = { path = "lmstudio" }
|
||||
|
||||
@@ -46,6 +46,7 @@ codex-guardian = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-file-watcher = { workspace = true }
|
||||
codex-hooks = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
|
||||
@@ -14,12 +14,14 @@ use codex_extension_api::ExtensionEventSink;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_goal_extension::GoalService;
|
||||
use codex_home::CodexHomeInstructionsContributor;
|
||||
use codex_login::AuthManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::thread_state::ThreadListenerCommand;
|
||||
@@ -32,11 +34,15 @@ pub(crate) fn thread_extensions<S>(
|
||||
state_db: Option<StateDbHandle>,
|
||||
thread_manager: Weak<ThreadManager>,
|
||||
goal_service: Arc<GoalService>,
|
||||
codex_home: AbsolutePathBuf,
|
||||
) -> Arc<ExtensionRegistry<Config>>
|
||||
where
|
||||
S: AgentSpawner<StartThreadOptions, Spawned = NewThread, Error = CodexErr> + 'static,
|
||||
{
|
||||
let mut builder = ExtensionRegistryBuilder::<Config>::with_event_sink(event_sink);
|
||||
builder.global_instructions_contributor(Arc::new(CodexHomeInstructionsContributor::new(
|
||||
codex_home,
|
||||
)));
|
||||
if let Some(state_db) = state_db {
|
||||
codex_goal_extension::install_with_backend(
|
||||
&mut builder,
|
||||
|
||||
@@ -194,6 +194,7 @@ mod tests {
|
||||
Some(state_db.clone()),
|
||||
thread_manager.clone(),
|
||||
Arc::new(codex_goal_extension::GoalService::new()),
|
||||
good_config.codex_home.clone(),
|
||||
),
|
||||
/*analytics_events_client*/ None,
|
||||
Arc::clone(&thread_store),
|
||||
|
||||
@@ -320,6 +320,7 @@ impl MessageProcessor {
|
||||
state_db.clone(),
|
||||
thread_manager.clone(),
|
||||
Arc::clone(&goal_service),
|
||||
config.codex_home.clone(),
|
||||
),
|
||||
Some(analytics_events_client.clone()),
|
||||
Arc::clone(&thread_store),
|
||||
|
||||
@@ -30,6 +30,7 @@ use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
@@ -400,11 +401,13 @@ async fn thread_start_response_excludes_empty_project_instruction_source() -> Re
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_without_selected_environment_excludes_instruction_sources() -> Result<()> {
|
||||
async fn thread_start_without_selected_environment_includes_global_instruction_source() -> Result<()>
|
||||
{
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
std::fs::write(codex_home.path().join("AGENTS.md"), "global instructions")?;
|
||||
let global_agents_path = codex_home.path().join("AGENTS.md");
|
||||
std::fs::write(&global_agents_path, "global instructions")?;
|
||||
let workspace = TempDir::new()?;
|
||||
std::fs::write(workspace.path().join("AGENTS.md"), "project instructions")?;
|
||||
|
||||
@@ -428,7 +431,12 @@ async fn thread_start_without_selected_environment_excludes_instruction_sources(
|
||||
..
|
||||
} = to_response::<ThreadStartResponse>(response)?;
|
||||
|
||||
assert!(instruction_sources.is_empty());
|
||||
assert_eq!(
|
||||
instruction_sources,
|
||||
vec![AbsolutePathBuf::try_from(std::fs::canonicalize(
|
||||
global_agents_path
|
||||
)?)?]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ codex-analytics = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
|
||||
@@ -26,7 +26,6 @@ pub use codex_config::types::TuiPetAnchor;
|
||||
pub use codex_config::types::UriBasedFileOpener;
|
||||
pub use codex_core::CodexThread;
|
||||
pub use codex_core::ForkSnapshot;
|
||||
pub use codex_core::LoadedAgentsMd;
|
||||
pub use codex_core::McpManager;
|
||||
pub use codex_core::NewThread;
|
||||
pub use codex_core::StartThreadOptions;
|
||||
|
||||
@@ -48,6 +48,7 @@ codex-shell-command = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-hooks = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-install-context = { workspace = true }
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
|
||||
@@ -55,36 +55,6 @@ impl<'a> AgentsMdManager<'a> {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub(crate) async fn load_global_instructions(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
codex_dir: Option<&AbsolutePathBuf>,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> Option<LoadedAgentsMd> {
|
||||
let base = codex_dir?;
|
||||
for candidate in [LOCAL_AGENTS_MD_FILENAME, DEFAULT_AGENTS_MD_FILENAME] {
|
||||
let path = base.join(candidate);
|
||||
let data = match fs.read_file(&path, /*sandbox*/ None).await {
|
||||
Ok(data) => data,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(err) if err.kind() == io::ErrorKind::IsADirectory => continue,
|
||||
Err(err) => {
|
||||
startup_warnings.push(format!(
|
||||
"Failed to read global AGENTS.md instructions from `{}`: {err}",
|
||||
path.display()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
warn_invalid_utf8(&path, &data, "Global", startup_warnings);
|
||||
let contents = String::from_utf8_lossy(&data);
|
||||
let trimmed = contents.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(LoadedAgentsMd::new_user(trimmed.to_string(), path));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Combines global instructions and project AGENTS.md content into a
|
||||
/// single model-visible instruction snapshot.
|
||||
pub(crate) async fn user_instructions(
|
||||
@@ -321,27 +291,25 @@ impl<'a> AgentsMdManager<'a> {
|
||||
/// Model-visible instructions loaded from AGENTS.md files and internal
|
||||
/// guidance.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LoadedAgentsMd {
|
||||
pub(crate) struct LoadedAgentsMd {
|
||||
/// Ordered instructions and their provenance.
|
||||
entries: Vec<InstructionEntry>,
|
||||
}
|
||||
|
||||
impl LoadedAgentsMd {
|
||||
pub fn new_user(contents: String, path: AbsolutePathBuf) -> Self {
|
||||
if contents.trim().is_empty() {
|
||||
return Self::default();
|
||||
}
|
||||
let mut loaded = Self {
|
||||
entries: vec![InstructionEntry {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_user(contents: String, path: AbsolutePathBuf) -> Self {
|
||||
Self::from_global(codex_extension_api::GlobalInstructions {
|
||||
instructions: vec![codex_extension_api::GlobalInstruction::new(
|
||||
contents,
|
||||
provenance: InstructionProvenance::Global(Some(path)),
|
||||
}],
|
||||
};
|
||||
loaded.enforce_model_context_limit();
|
||||
loaded
|
||||
Some(path),
|
||||
)],
|
||||
warnings: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_text_for_testing(contents: impl Into<String>) -> Self {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn from_text_for_testing(contents: impl Into<String>) -> Self {
|
||||
let contents = contents.into();
|
||||
if contents.trim().is_empty() {
|
||||
return Self::default();
|
||||
@@ -354,6 +322,22 @@ impl LoadedAgentsMd {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_global(instructions: codex_extension_api::GlobalInstructions) -> Self {
|
||||
let mut loaded = Self {
|
||||
entries: instructions
|
||||
.instructions
|
||||
.into_iter()
|
||||
.filter(|instruction| !instruction.contents.trim().is_empty())
|
||||
.map(|instruction| InstructionEntry {
|
||||
contents: instruction.contents,
|
||||
provenance: InstructionProvenance::Global(instruction.source),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
loaded.enforce_model_context_limit();
|
||||
loaded
|
||||
}
|
||||
|
||||
pub(crate) fn from_snapshot(snapshot: UserInstructionsSnapshot) -> Self {
|
||||
let mut loaded = Self {
|
||||
entries: snapshot
|
||||
@@ -403,14 +387,14 @@ impl LoadedAgentsMd {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace_global(&mut self, instructions: LoadedAgentsMd) {
|
||||
pub(crate) fn replace_global(&mut self, instructions: codex_extension_api::GlobalInstructions) {
|
||||
let first_non_global = self
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| !matches!(entry.provenance, InstructionProvenance::Global(_)))
|
||||
.unwrap_or(self.entries.len());
|
||||
self.entries
|
||||
.splice(..first_non_global, instructions.entries);
|
||||
.splice(..first_non_global, Self::from_global(instructions).entries);
|
||||
self.enforce_model_context_limit();
|
||||
}
|
||||
|
||||
@@ -421,7 +405,7 @@ impl LoadedAgentsMd {
|
||||
}
|
||||
|
||||
/// Returns the concatenated model-visible instruction text.
|
||||
pub fn text(&self) -> String {
|
||||
pub(crate) fn text(&self) -> String {
|
||||
let mut output = String::new();
|
||||
let mut previous_provenance: Option<&InstructionProvenance> = None;
|
||||
for entry in &self.entries {
|
||||
@@ -445,7 +429,7 @@ impl LoadedAgentsMd {
|
||||
}
|
||||
|
||||
/// Returns the AGENTS.md files that supplied instruction entries.
|
||||
pub fn sources(&self) -> impl Iterator<Item = &AbsolutePathBuf> {
|
||||
pub(crate) fn sources(&self) -> impl Iterator<Item = &AbsolutePathBuf> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.provenance.path())
|
||||
|
||||
@@ -188,15 +188,15 @@ fn loaded_instructions_with_only_empty_or_whitespace_entries_are_empty() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_and_persisted_instructions_are_bounded_for_model_context() {
|
||||
fn contributor_and_persisted_instructions_are_bounded_for_model_context() {
|
||||
let oversized = "x".repeat(approx_bytes_for_tokens(MAX_USER_INSTRUCTIONS_TOKENS + 100));
|
||||
let mut global = LoadedAgentsMd {
|
||||
entries: vec![InstructionEntry {
|
||||
contents: oversized.clone(),
|
||||
provenance: InstructionProvenance::Global(None),
|
||||
}],
|
||||
};
|
||||
global.enforce_model_context_limit();
|
||||
let global = LoadedAgentsMd::from_global(codex_extension_api::GlobalInstructions {
|
||||
instructions: vec![codex_extension_api::GlobalInstruction::new(
|
||||
oversized.clone(),
|
||||
/*source*/ None,
|
||||
)],
|
||||
warnings: Vec::new(),
|
||||
});
|
||||
let restored = LoadedAgentsMd::from_snapshot(UserInstructionsSnapshot {
|
||||
instructions: vec![InstructionSnapshot {
|
||||
contents: oversized,
|
||||
@@ -227,7 +227,13 @@ fn replacing_global_instructions_preserves_project_entries_within_the_bound() {
|
||||
provenance: InstructionProvenance::Project(Some(project_source.clone())),
|
||||
});
|
||||
|
||||
loaded.replace_global(LoadedAgentsMd::from_text_for_testing("new global"));
|
||||
loaded.replace_global(codex_extension_api::GlobalInstructions {
|
||||
instructions: vec![codex_extension_api::GlobalInstruction::new(
|
||||
"new global",
|
||||
/*source*/ None,
|
||||
)],
|
||||
warnings: Vec::new(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
loaded,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use crate::agents_md::DEFAULT_AGENTS_MD_FILENAME;
|
||||
use crate::agents_md::LOCAL_AGENTS_MD_FILENAME;
|
||||
use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::config::edit::apply_blocking;
|
||||
@@ -205,61 +203,6 @@ async fn load_config_normalizes_relative_cwd_override() -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_loads_global_agents_instructions() -> std::io::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let global_agents_path = codex_home.abs().join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
std::fs::write(&global_agents_path, "\n global instructions \n")?;
|
||||
|
||||
let mut config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
let _ = config.features.enable(Feature::MemoryTool);
|
||||
|
||||
let user_instructions = config
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.expect("global instructions expected");
|
||||
assert_eq!(user_instructions.text(), "global instructions");
|
||||
assert_eq!(
|
||||
user_instructions.sources().collect::<Vec<_>>(),
|
||||
vec![&global_agents_path]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_prefers_global_agents_override_instructions() -> std::io::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(DEFAULT_AGENTS_MD_FILENAME),
|
||||
"global instructions",
|
||||
)?;
|
||||
let global_agents_override_path = codex_home.abs().join(LOCAL_AGENTS_MD_FILENAME);
|
||||
std::fs::write(&global_agents_override_path, "local override instructions")?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user_instructions = config
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.expect("global override instructions expected");
|
||||
assert_eq!(user_instructions.text(), "local override instructions");
|
||||
assert_eq!(
|
||||
user_instructions.sources().collect::<Vec<_>>(),
|
||||
vec![&global_agents_override_path]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_toml_parsing() {
|
||||
let history_with_persistence = r#"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use crate::agents_md::AgentsMdManager;
|
||||
pub use crate::agents_md::LoadedAgentsMd;
|
||||
use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::path_utils::normalize_for_native_workdir;
|
||||
@@ -646,9 +644,6 @@ pub struct Config {
|
||||
/// Defaults to `false`.
|
||||
pub show_raw_agent_reasoning: bool,
|
||||
|
||||
/// User-provided instructions from AGENTS.md.
|
||||
pub user_instructions: Option<LoadedAgentsMd>,
|
||||
|
||||
/// Base instructions override.
|
||||
pub base_instructions: Option<String>,
|
||||
|
||||
@@ -2602,12 +2597,6 @@ impl Config {
|
||||
.startup_warnings()
|
||||
.unwrap_or_default()
|
||||
.to_vec();
|
||||
let user_instructions = AgentsMdManager::load_global_instructions(
|
||||
LOCAL_FS.as_ref(),
|
||||
Some(&codex_home),
|
||||
&mut startup_warnings,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Destructure ConfigOverrides fully to ensure all overrides are applied.
|
||||
let ConfigOverrides {
|
||||
@@ -3454,7 +3443,6 @@ impl Config {
|
||||
approvals_reviewer: constrained_approvals_reviewer.value(),
|
||||
enforce_residency: enforce_residency.value,
|
||||
notify: cfg.notify,
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
personality,
|
||||
developer_instructions,
|
||||
|
||||
@@ -29,7 +29,6 @@ use tokio::sync::Semaphore;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::LoadedAgentsMd;
|
||||
use crate::codex_delegate::run_codex_thread_interactive;
|
||||
use crate::config::Config;
|
||||
use crate::config::Constrained;
|
||||
@@ -147,7 +146,6 @@ struct GuardianReviewSessionReuseKey {
|
||||
permissions: Permissions,
|
||||
developer_instructions: Option<String>,
|
||||
base_instructions: Option<String>,
|
||||
user_instructions: Option<LoadedAgentsMd>,
|
||||
compact_prompt: Option<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
mcp_servers: Constrained<HashMap<String, McpServerConfig>>,
|
||||
@@ -172,7 +170,6 @@ impl GuardianReviewSessionReuseKey {
|
||||
permissions: spawn_config.permissions.clone(),
|
||||
developer_instructions: spawn_config.developer_instructions.clone(),
|
||||
base_instructions: spawn_config.base_instructions.clone(),
|
||||
user_instructions: spawn_config.user_instructions.clone(),
|
||||
compact_prompt: spawn_config.compact_prompt.clone(),
|
||||
cwd: spawn_config.cwd.clone(),
|
||||
mcp_servers: spawn_config.mcp_servers.clone(),
|
||||
|
||||
@@ -179,7 +179,6 @@ async fn guardian_test_session_and_turn_with_base_url(
|
||||
session.thread_id = fixed_guardian_parent_session_id();
|
||||
let mut config = (*turn.config).clone();
|
||||
config.model_provider.base_url = Some(format!("{base_url}/v1"));
|
||||
config.user_instructions = None;
|
||||
let config = Arc::new(config);
|
||||
let models_manager = test_support::models_manager_with_provider(
|
||||
config.codex_home.to_path_buf(),
|
||||
@@ -1993,7 +1992,6 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() ->
|
||||
crate::session::tests::make_session_and_context_with_rx().await;
|
||||
let mut config = (*turn.config).clone();
|
||||
config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
|
||||
config.user_instructions = None;
|
||||
let config = Arc::new(config);
|
||||
let models_manager = test_support::models_manager_with_provider(
|
||||
config.codex_home.to_path_buf(),
|
||||
|
||||
@@ -124,10 +124,8 @@ pub type NewConversation = NewThread;
|
||||
#[deprecated(note = "use CodexThread")]
|
||||
pub type CodexConversation = CodexThread;
|
||||
pub(crate) mod agents_md;
|
||||
pub use agents_md::AgentsMdManager;
|
||||
pub use agents_md::DEFAULT_AGENTS_MD_FILENAME;
|
||||
pub use agents_md::LOCAL_AGENTS_MD_FILENAME;
|
||||
pub use agents_md::LoadedAgentsMd;
|
||||
mod rollout;
|
||||
pub(crate) mod safety;
|
||||
mod session_rollout_init_error;
|
||||
|
||||
@@ -19,7 +19,8 @@ use crate::session::turn::built_tools;
|
||||
use crate::state_db_bridge::StateDbHandle;
|
||||
use crate::thread_manager::ThreadManager;
|
||||
use crate::thread_manager::thread_store_from_config;
|
||||
use codex_extension_api::empty_extension_registry;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_home::CodexHomeInstructionsContributor;
|
||||
|
||||
/// Build the model-visible `input` list for a single debug turn.
|
||||
#[doc(hidden)]
|
||||
@@ -40,6 +41,10 @@ pub async fn build_prompt_input(
|
||||
|
||||
let thread_store = thread_store_from_config(&config, state_db.clone());
|
||||
let installation_id = resolve_installation_id(&config.codex_home).await?;
|
||||
let mut extension_builder = ExtensionRegistryBuilder::<Config>::new();
|
||||
extension_builder.global_instructions_contributor(Arc::new(
|
||||
CodexHomeInstructionsContributor::new(config.codex_home.clone()),
|
||||
));
|
||||
let thread_manager = ThreadManager::new(
|
||||
&config,
|
||||
Arc::clone(&auth_manager),
|
||||
@@ -52,7 +57,7 @@ pub async fn build_prompt_input(
|
||||
.await
|
||||
.map_err(|err| CodexErr::Fatal(err.to_string()))?,
|
||||
),
|
||||
empty_extension_registry(),
|
||||
Arc::new(extension_builder.build()),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store,
|
||||
state_db.clone(),
|
||||
|
||||
@@ -511,20 +511,27 @@ impl Codex {
|
||||
InitialHistory::New | InitialHistory::Cleared
|
||||
);
|
||||
let user_instructions = if fresh_history {
|
||||
if let Some(primary_environment) = environment_selections.primary_environment() {
|
||||
let mut user_instruction_warnings = Vec::new();
|
||||
let loaded = AgentsMdManager::new(&config)
|
||||
.user_instructions(
|
||||
Some(primary_environment.as_ref()),
|
||||
config.user_instructions.clone().unwrap_or_default(),
|
||||
&mut user_instruction_warnings,
|
||||
)
|
||||
.await;
|
||||
config.startup_warnings.extend(user_instruction_warnings);
|
||||
Some(loaded.unwrap_or_default())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let global_instructions =
|
||||
if let Some(contributor) = extensions.global_instructions_contributor() {
|
||||
contributor.contribute().await.map_err(|err| {
|
||||
CodexErr::Fatal(format!("failed to resolve global instructions: {err}"))
|
||||
})?
|
||||
} else {
|
||||
codex_extension_api::GlobalInstructions::default()
|
||||
};
|
||||
config
|
||||
.startup_warnings
|
||||
.extend(global_instructions.warnings.clone());
|
||||
let mut user_instruction_warnings = Vec::new();
|
||||
let loaded = AgentsMdManager::new(&config)
|
||||
.user_instructions(
|
||||
environment_selections.primary_environment().as_deref(),
|
||||
LoadedAgentsMd::from_global(global_instructions),
|
||||
&mut user_instruction_warnings,
|
||||
)
|
||||
.await;
|
||||
config.startup_warnings.extend(user_instruction_warnings);
|
||||
Some(loaded.unwrap_or_default())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -3090,47 +3097,70 @@ impl Session {
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_global_instructions(&self, turn_context: &TurnContext) {
|
||||
let configured_global = turn_context
|
||||
.config
|
||||
.user_instructions
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
let needs_full_resolution = {
|
||||
let state = self.state.lock().await;
|
||||
state.session_configuration.user_instructions.is_none()
|
||||
let contributor = self
|
||||
.services
|
||||
.extensions
|
||||
.global_instructions_contributor()
|
||||
.cloned();
|
||||
let resolved = match contributor {
|
||||
Some(contributor) => contributor.contribute().await,
|
||||
None => Ok(codex_extension_api::GlobalInstructions::default()),
|
||||
};
|
||||
if needs_full_resolution {
|
||||
let Some(primary_environment) = turn_context.environments.primary_environment() else {
|
||||
match resolved {
|
||||
Ok(global_instructions) => {
|
||||
for warning in &global_instructions.warnings {
|
||||
self.send_event(
|
||||
turn_context,
|
||||
EventMsg::Warning(WarningEvent {
|
||||
message: warning.clone(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let needs_full_resolution = {
|
||||
let state = self.state.lock().await;
|
||||
state.session_configuration.user_instructions.is_none()
|
||||
};
|
||||
if needs_full_resolution {
|
||||
let mut warnings = Vec::new();
|
||||
let loaded = AgentsMdManager::new(turn_context.config.as_ref())
|
||||
.user_instructions(
|
||||
turn_context.environments.primary_environment().as_deref(),
|
||||
LoadedAgentsMd::from_global(global_instructions),
|
||||
&mut warnings,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
for warning in warnings {
|
||||
self.send_event(
|
||||
turn_context,
|
||||
EventMsg::Warning(WarningEvent { message: warning }),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let mut state = self.state.lock().await;
|
||||
state.session_configuration.user_instructions = Some(loaded);
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.lock().await;
|
||||
state.session_configuration.user_instructions = Some(LoadedAgentsMd::default());
|
||||
return;
|
||||
};
|
||||
let mut warnings = Vec::new();
|
||||
let loaded = AgentsMdManager::new(turn_context.config.as_ref())
|
||||
.user_instructions(
|
||||
Some(primary_environment.as_ref()),
|
||||
configured_global,
|
||||
&mut warnings,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
for warning in warnings {
|
||||
let instructions = state
|
||||
.session_configuration
|
||||
.user_instructions
|
||||
.get_or_insert_with(LoadedAgentsMd::default);
|
||||
instructions.replace_global(global_instructions);
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_event(
|
||||
turn_context,
|
||||
EventMsg::Warning(WarningEvent { message: warning }),
|
||||
EventMsg::Warning(WarningEvent {
|
||||
message: format!(
|
||||
"Failed to refresh global instructions; continuing with the previous snapshot: {err}"
|
||||
),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let mut state = self.state.lock().await;
|
||||
state.session_configuration.user_instructions = Some(loaded);
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.lock().await;
|
||||
let instructions = state
|
||||
.session_configuration
|
||||
.user_instructions
|
||||
.get_or_insert_with(LoadedAgentsMd::default);
|
||||
instructions.replace_global(configured_global);
|
||||
}
|
||||
|
||||
pub(crate) async fn update_token_usage_info(
|
||||
|
||||
@@ -23,6 +23,11 @@ use codex_config::Sourced;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::GlobalInstruction;
|
||||
use codex_extension_api::GlobalInstructions;
|
||||
use codex_extension_api::GlobalInstructionsContributor;
|
||||
use codex_extension_api::GlobalInstructionsFuture;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
@@ -170,6 +175,8 @@ use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration as StdDuration;
|
||||
|
||||
mod guardian_tests;
|
||||
@@ -5042,6 +5049,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
initial_history: InitialHistory,
|
||||
session_source: SessionSource,
|
||||
agent_control: AgentControl,
|
||||
extensions: Arc<ExtensionRegistry<Config>>,
|
||||
) -> anyhow::Result<(Arc<Session>, async_channel::Receiver<Event>)> {
|
||||
let skip_global_instructions_refresh_on_first_turn = matches!(
|
||||
initial_history,
|
||||
@@ -5134,7 +5142,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
skills_manager,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
extensions,
|
||||
agent_control,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
@@ -5158,6 +5166,20 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
Ok((session, rx_event))
|
||||
}
|
||||
|
||||
struct CountingGlobalInstructionsContributor {
|
||||
calls: AtomicUsize,
|
||||
}
|
||||
|
||||
impl GlobalInstructionsContributor for CountingGlobalInstructionsContributor {
|
||||
fn contribute(&self) -> GlobalInstructionsFuture<'_> {
|
||||
self.calls.fetch_add(1, Ordering::SeqCst);
|
||||
Box::pin(std::future::ready(Ok(GlobalInstructions {
|
||||
instructions: vec![GlobalInstruction::new("refreshed instructions", None)],
|
||||
warnings: Vec::new(),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn history_sharing_first_full_injection_uses_inherited_instruction_snapshot() {
|
||||
let inherited_snapshot = UserInstructionsSnapshot {
|
||||
@@ -5167,6 +5189,12 @@ async fn history_sharing_first_full_injection_uses_inherited_instruction_snapsho
|
||||
source: None,
|
||||
}],
|
||||
};
|
||||
let contributor = Arc::new(CountingGlobalInstructionsContributor {
|
||||
calls: AtomicUsize::new(0),
|
||||
});
|
||||
let mut extension_builder = codex_extension_api::ExtensionRegistryBuilder::<Config>::new();
|
||||
extension_builder.global_instructions_contributor(contributor.clone());
|
||||
let extensions = Arc::new(extension_builder.build());
|
||||
let (session, _rx_event) = make_session_with_history_source_and_agent_control_and_rx(
|
||||
InitialHistory::Forked(vec![RolloutItem::TurnContext(TurnContextItem {
|
||||
turn_id: None,
|
||||
@@ -5191,10 +5219,12 @@ async fn history_sharing_first_full_injection_uses_inherited_instruction_snapsho
|
||||
})]),
|
||||
SessionSource::Exec,
|
||||
AgentControl::default(),
|
||||
extensions,
|
||||
)
|
||||
.await
|
||||
.expect("forked session should start");
|
||||
|
||||
assert_eq!(contributor.calls.load(Ordering::SeqCst), 0);
|
||||
assert!(session.reference_context_item().await.is_none());
|
||||
|
||||
let first_turn = session.new_default_turn().await;
|
||||
@@ -5211,6 +5241,80 @@ async fn history_sharing_first_full_injection_uses_inherited_instruction_snapsho
|
||||
.map(LoadedAgentsMd::snapshot)
|
||||
};
|
||||
assert_eq!(first_turn_snapshot, Some(inherited_snapshot));
|
||||
assert_eq!(contributor.calls.load(Ordering::SeqCst), 0);
|
||||
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.set_reference_context_item(/*item*/ None);
|
||||
}
|
||||
let later_turn = session.new_default_turn().await;
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(later_turn.as_ref())
|
||||
.await;
|
||||
|
||||
let refreshed_snapshot = {
|
||||
let state = session.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.map(LoadedAgentsMd::snapshot)
|
||||
};
|
||||
assert_eq!(
|
||||
refreshed_snapshot,
|
||||
Some(UserInstructionsSnapshot {
|
||||
instructions: vec![InstructionSnapshot {
|
||||
contents: "refreshed instructions".to_string(),
|
||||
provenance: InstructionProvenance::Global,
|
||||
source: None,
|
||||
}],
|
||||
})
|
||||
);
|
||||
assert_eq!(contributor.calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_history_refreshes_when_first_turn_requires_full_context() {
|
||||
let contributor = Arc::new(CountingGlobalInstructionsContributor {
|
||||
calls: AtomicUsize::new(0),
|
||||
});
|
||||
let mut extension_builder = codex_extension_api::ExtensionRegistryBuilder::<Config>::new();
|
||||
extension_builder.global_instructions_contributor(contributor.clone());
|
||||
let (session, _rx_event) = make_session_with_history_source_and_agent_control_and_rx(
|
||||
InitialHistory::Forked(Vec::new()),
|
||||
SessionSource::Exec,
|
||||
AgentControl::default(),
|
||||
Arc::new(extension_builder.build()),
|
||||
)
|
||||
.await
|
||||
.expect("legacy forked session should start");
|
||||
|
||||
assert_eq!(contributor.calls.load(Ordering::SeqCst), 0);
|
||||
|
||||
let turn_context = session.new_default_turn().await;
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||
.await;
|
||||
|
||||
let snapshot = {
|
||||
let state = session.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.map(LoadedAgentsMd::snapshot)
|
||||
};
|
||||
assert_eq!(
|
||||
snapshot,
|
||||
Some(UserInstructionsSnapshot {
|
||||
instructions: vec![InstructionSnapshot {
|
||||
contents: "refreshed instructions".to_string(),
|
||||
provenance: InstructionProvenance::Global,
|
||||
source: None,
|
||||
}],
|
||||
})
|
||||
);
|
||||
assert_eq!(contributor.calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -5224,6 +5328,7 @@ async fn resumed_root_session_uses_thread_id_as_session_id() {
|
||||
}),
|
||||
SessionSource::Exec,
|
||||
AgentControl::default(),
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
)
|
||||
.await
|
||||
.expect("resume should succeed");
|
||||
@@ -5259,6 +5364,7 @@ async fn resumed_subagent_session_keeps_inherited_session_id() {
|
||||
}),
|
||||
session_source,
|
||||
AgentControl::default().with_session_id(parent_session_id, /*max_threads*/ usize::MAX),
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
)
|
||||
.await
|
||||
.expect("resume should succeed");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use crate::LoadedAgentsMd;
|
||||
use crate::ThreadManager;
|
||||
use crate::config::AgentRoleConfig;
|
||||
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
|
||||
@@ -4516,24 +4515,6 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
assert_eq!(config, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_agent_spawn_config_preserves_base_user_instructions() {
|
||||
let (_session, mut turn) = make_session_and_context().await;
|
||||
let mut base_config = (*turn.config).clone();
|
||||
base_config.user_instructions = Some(LoadedAgentsMd::new_user(
|
||||
"base-user".to_string(),
|
||||
base_config.codex_home.join("AGENTS.md"),
|
||||
));
|
||||
turn.config = Arc::new(base_config.clone());
|
||||
let base_instructions = BaseInstructions {
|
||||
text: "base".to_string(),
|
||||
};
|
||||
|
||||
let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config");
|
||||
|
||||
assert_eq!(config.user_instructions, base_config.user_instructions);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_agent_resume_config_clears_base_instructions() {
|
||||
let (_session, mut turn) = make_session_and_context().await;
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -24,6 +25,11 @@ use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::RemoveOptions;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::GlobalInstruction;
|
||||
use codex_extension_api::GlobalInstructions;
|
||||
use codex_extension_api::GlobalInstructionsContributor;
|
||||
use codex_extension_api::GlobalInstructionsFuture;
|
||||
use codex_extension_api::empty_extension_registry;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
@@ -221,6 +227,48 @@ pub struct TestCodexBuilder {
|
||||
extensions: Arc<ExtensionRegistry<Config>>,
|
||||
}
|
||||
|
||||
pub struct TestGlobalInstructionsContributor {
|
||||
responses: Vec<std::result::Result<GlobalInstructions, String>>,
|
||||
calls: AtomicUsize,
|
||||
}
|
||||
|
||||
impl TestGlobalInstructionsContributor {
|
||||
pub fn new(responses: Vec<std::result::Result<GlobalInstructions, String>>) -> Self {
|
||||
assert!(
|
||||
!responses.is_empty(),
|
||||
"test contributor requires at least one response"
|
||||
);
|
||||
Self {
|
||||
responses,
|
||||
calls: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn static_instructions(contents: impl Into<String>) -> Self {
|
||||
Self::new(vec![Ok(GlobalInstructions {
|
||||
instructions: vec![GlobalInstruction::new(contents, None)],
|
||||
warnings: Vec::new(),
|
||||
})])
|
||||
}
|
||||
|
||||
pub fn calls(&self) -> usize {
|
||||
self.calls.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalInstructionsContributor for TestGlobalInstructionsContributor {
|
||||
fn contribute(&self) -> GlobalInstructionsFuture<'_> {
|
||||
let call = self.calls.fetch_add(1, Ordering::SeqCst);
|
||||
let response = self
|
||||
.responses
|
||||
.get(call)
|
||||
.or_else(|| self.responses.last())
|
||||
.expect("test contributor responses are non-empty")
|
||||
.clone();
|
||||
Box::pin(std::future::ready(response))
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCodexBuilder {
|
||||
pub fn with_config<T>(mut self, mutator: T) -> Self
|
||||
where
|
||||
@@ -308,6 +356,14 @@ impl TestCodexBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_global_instructions(self, contents: impl Into<String>) -> Self {
|
||||
let mut builder = ExtensionRegistryBuilder::<Config>::new();
|
||||
builder.global_instructions_contributor(Arc::new(
|
||||
TestGlobalInstructionsContributor::static_instructions(contents),
|
||||
));
|
||||
self.with_extensions(Arc::new(builder.build()))
|
||||
}
|
||||
|
||||
pub fn with_windows_cmd_shell(self) -> Self {
|
||||
if cfg!(windows) {
|
||||
self.with_user_shell(get_shell_by_model_provided_path(&PathBuf::from("cmd.exe")))
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use codex_core::config::Config;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_home::CodexHomeInstructionsContributor;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::create_directory_symlink;
|
||||
use core_test_support::responses::ev_completed;
|
||||
@@ -236,9 +239,16 @@ async fn selected_environment_sources_match_model_visible_instructions() -> Resu
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let global_agents = home.path().join("AGENTS.md");
|
||||
std::fs::write(&global_agents, "global doc")?;
|
||||
let mut extension_builder = ExtensionRegistryBuilder::<Config>::new();
|
||||
extension_builder.global_instructions_contributor(Arc::new(
|
||||
CodexHomeInstructionsContributor::new(AbsolutePathBuf::try_from(
|
||||
home.path().to_path_buf(),
|
||||
)?),
|
||||
));
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_home(home)
|
||||
.with_extensions(Arc::new(extension_builder.build()))
|
||||
.with_workspace_setup(|cwd, fs| async move {
|
||||
fs.write_file(
|
||||
&cwd.join("AGENTS.md"),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::LoadedAgentsMd;
|
||||
use codex_core::ModelClient;
|
||||
use codex_core::NewThread;
|
||||
use codex_core::Prompt;
|
||||
@@ -363,10 +362,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
|
||||
let codex_home = Arc::new(TempDir::new().unwrap());
|
||||
let mut builder = test_codex()
|
||||
.with_home(codex_home.clone())
|
||||
.with_config(|config| {
|
||||
// Ensure user instructions are NOT delivered on resume.
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing("be nice"));
|
||||
});
|
||||
.with_global_instructions("be nice");
|
||||
let test = builder
|
||||
.resume(&server, codex_home, session_path.clone())
|
||||
.await
|
||||
@@ -1180,9 +1176,7 @@ async fn includes_user_instructions_message_in_request() {
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing("be nice"));
|
||||
});
|
||||
.with_global_instructions("be nice");
|
||||
let codex = builder
|
||||
.build(&server)
|
||||
.await
|
||||
@@ -2267,8 +2261,8 @@ async fn includes_developer_instructions_message_in_request() {
|
||||
.await;
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_global_instructions("be nice")
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing("be nice"));
|
||||
config.developer_instructions = Some("be useful".to_string());
|
||||
});
|
||||
let codex = builder
|
||||
|
||||
@@ -4,9 +4,14 @@ use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::LoadedAgentsMd;
|
||||
use codex_core::config::Config;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::GlobalInstruction;
|
||||
use codex_extension_api::GlobalInstructions;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
@@ -21,6 +26,7 @@ use core_test_support::responses::ResponseMock;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
use core_test_support::test_codex::TestCodexHarness;
|
||||
use core_test_support::test_codex::TestGlobalInstructionsContributor;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -34,7 +40,8 @@ const FIXED_CWD: &str = "/tmp/codex_remote_compaction_parity_workspace";
|
||||
const IMAGE_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
|
||||
const SUMMARY: &str = "REMOTE_COMPACTION_PARITY_ENCRYPTED_SUMMARY";
|
||||
const DUMMY_FUNCTION_NAME: &str = "test_tool";
|
||||
const USER_INSTRUCTIONS: &str = "PARITY_USER_INSTRUCTIONS";
|
||||
const INITIAL_USER_INSTRUCTIONS: &str = "PARITY_USER_INSTRUCTIONS";
|
||||
const REFRESHED_USER_INSTRUCTIONS: &str = "PARITY_REFRESHED_USER_INSTRUCTIONS";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum Mode {
|
||||
@@ -327,11 +334,11 @@ fn assert_capture_eq(
|
||||
&legacy_follow_up,
|
||||
&v2_follow_up,
|
||||
);
|
||||
assert_single_user_instruction_message(
|
||||
assert_single_refreshed_user_instruction_message(
|
||||
&legacy.follow_up_body,
|
||||
&format!("legacy follow-up for {label}"),
|
||||
);
|
||||
assert_single_user_instruction_message(
|
||||
assert_single_refreshed_user_instruction_message(
|
||||
&v2.follow_up_body,
|
||||
&format!("v2 follow-up for {label}"),
|
||||
);
|
||||
@@ -403,11 +410,11 @@ fn assert_follow_up_and_history_eq(
|
||||
&legacy_follow_up,
|
||||
&v2_follow_up,
|
||||
);
|
||||
assert_single_user_instruction_message(
|
||||
assert_single_refreshed_user_instruction_message(
|
||||
&legacy.follow_up_body,
|
||||
&format!("legacy follow-up for {label}"),
|
||||
);
|
||||
assert_single_user_instruction_message(
|
||||
assert_single_refreshed_user_instruction_message(
|
||||
&v2.follow_up_body,
|
||||
&format!("v2 follow-up for {label}"),
|
||||
);
|
||||
@@ -734,6 +741,7 @@ async fn build_harness_inner(
|
||||
prepare_fixed_cwd()?;
|
||||
TestCodexHarness::with_builder(
|
||||
parity_builder(mode, settings, hooks)
|
||||
.with_extensions(global_instruction_extensions())
|
||||
.with_config(move |config| config.model_auto_compact_token_limit = auto_compact_limit),
|
||||
)
|
||||
.await
|
||||
@@ -749,7 +757,6 @@ fn parity_builder(mode: Mode, settings: RunSettings, hooks: bool) -> TestCodexBu
|
||||
FIXED_CWD,
|
||||
))
|
||||
.expect("fixed cwd should be absolute");
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(USER_INSTRUCTIONS));
|
||||
config.developer_instructions = Some("PARITY_DEVELOPER_INSTRUCTIONS".to_string());
|
||||
if settings.service_tier_fast {
|
||||
config.service_tier = Some(ServiceTier::Fast.request_value().to_string());
|
||||
@@ -775,6 +782,28 @@ fn prepare_fixed_cwd() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn global_instruction_extensions() -> Arc<ExtensionRegistry<Config>> {
|
||||
let contributor = TestGlobalInstructionsContributor::new(vec![
|
||||
Ok(GlobalInstructions {
|
||||
instructions: vec![GlobalInstruction::new(
|
||||
INITIAL_USER_INSTRUCTIONS,
|
||||
/*source*/ None,
|
||||
)],
|
||||
warnings: Vec::new(),
|
||||
}),
|
||||
Ok(GlobalInstructions {
|
||||
instructions: vec![GlobalInstruction::new(
|
||||
REFRESHED_USER_INSTRUCTIONS,
|
||||
/*source*/ None,
|
||||
)],
|
||||
warnings: Vec::new(),
|
||||
}),
|
||||
]);
|
||||
let mut builder = ExtensionRegistryBuilder::new();
|
||||
builder.global_instructions_contributor(Arc::new(contributor));
|
||||
Arc::new(builder.build())
|
||||
}
|
||||
|
||||
fn rollout_path(harness: &TestCodexHarness) -> PathBuf {
|
||||
harness
|
||||
.test()
|
||||
@@ -1017,21 +1046,48 @@ fn follow_up_request_view(body: &Value) -> Value {
|
||||
}
|
||||
|
||||
fn assert_single_user_instruction_message(body: &Value, label: &str) {
|
||||
assert_single_user_instruction_message_with_contents(body, label, INITIAL_USER_INSTRUCTIONS);
|
||||
}
|
||||
|
||||
fn assert_single_refreshed_user_instruction_message(body: &Value, label: &str) {
|
||||
assert_single_user_instruction_message_with_contents(body, label, REFRESHED_USER_INSTRUCTIONS);
|
||||
}
|
||||
|
||||
fn assert_single_user_instruction_message_with_contents(
|
||||
body: &Value,
|
||||
label: &str,
|
||||
expected_instructions: &str,
|
||||
) {
|
||||
let input = body
|
||||
.get("input")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
assert_user_instruction_messages_in_items(
|
||||
assert_user_instruction_messages_in_items_with_contents(
|
||||
&Value::Array(input),
|
||||
label,
|
||||
/*expected_count*/ 1,
|
||||
expected_instructions,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_user_instruction_messages_in_items(items: &Value, label: &str, expected_count: usize) {
|
||||
assert_user_instruction_messages_in_items_with_contents(
|
||||
items,
|
||||
label,
|
||||
expected_count,
|
||||
INITIAL_USER_INSTRUCTIONS,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_user_instruction_messages_in_items_with_contents(
|
||||
items: &Value,
|
||||
label: &str,
|
||||
expected_count: usize,
|
||||
expected_instructions: &str,
|
||||
) {
|
||||
let expected_text = format!(
|
||||
"# AGENTS.md instructions for {FIXED_CWD}\n\n<INSTRUCTIONS>\n{USER_INSTRUCTIONS}\n</INSTRUCTIONS>"
|
||||
"# AGENTS.md instructions for {FIXED_CWD}\n\n<INSTRUCTIONS>\n{expected_instructions}\n</INSTRUCTIONS>"
|
||||
);
|
||||
let instruction_messages = items
|
||||
.as_array()
|
||||
|
||||
@@ -6,7 +6,13 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use codex_core::ForkSnapshot;
|
||||
use codex_core::config::Config;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::GlobalInstruction;
|
||||
use codex_extension_api::GlobalInstructions;
|
||||
use codex_features::Feature;
|
||||
use codex_home::CodexHomeInstructionsContributor;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::built_in_model_providers;
|
||||
@@ -19,6 +25,8 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
use core_test_support::test_codex::TestGlobalInstructionsContributor;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use core_test_support::wait_for_event_match;
|
||||
@@ -60,6 +68,40 @@ fn write_global_file(
|
||||
AbsolutePathBuf::try_from(path).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn test_codex_with_home(home: Arc<TempDir>) -> TestCodexBuilder {
|
||||
let codex_home = AbsolutePathBuf::try_from(home.path().to_path_buf())
|
||||
.expect("temporary Codex home should be absolute");
|
||||
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
|
||||
extensions.global_instructions_contributor(Arc::new(
|
||||
CodexHomeInstructionsContributor::new(codex_home),
|
||||
));
|
||||
test_codex()
|
||||
.with_home(home)
|
||||
.with_extensions(Arc::new(extensions.build()))
|
||||
}
|
||||
|
||||
fn test_instruction_extensions(
|
||||
contributor: Arc<TestGlobalInstructionsContributor>,
|
||||
) -> Arc<ExtensionRegistry<Config>> {
|
||||
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
|
||||
extensions.global_instructions_contributor(contributor);
|
||||
Arc::new(extensions.build())
|
||||
}
|
||||
|
||||
fn contributed_instructions(
|
||||
contents: &str,
|
||||
source: Option<AbsolutePathBuf>,
|
||||
warnings: &[&str],
|
||||
) -> GlobalInstructions {
|
||||
GlobalInstructions {
|
||||
instructions: vec![GlobalInstruction::new(contents, source)],
|
||||
warnings: warnings
|
||||
.iter()
|
||||
.map(|warning| (*warning).to_string())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn instruction_fragments(request: &responses::ResponsesRequest) -> Vec<String> {
|
||||
request
|
||||
.message_input_texts("user")
|
||||
@@ -204,8 +246,7 @@ async fn fresh_thread_composes_global_before_project_and_reports_sources() -> Re
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let global_source = write_global(home.as_ref(), GLOBAL_INSTRUCTIONS)?;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
let mut builder = test_codex_with_home(Arc::clone(&home))
|
||||
.with_workspace_setup(|cwd, fs| async move {
|
||||
fs.write_file(
|
||||
&cwd.join("AGENTS.md"),
|
||||
@@ -305,8 +346,7 @@ async fn global_instruction_context_item_is_currently_not_limited_by_project_doc
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let oversized_global = vec!["global instruction item remains uncapped"; 512].join("\n");
|
||||
let source = write_global(home.as_ref(), &oversized_global)?;
|
||||
let mut builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
let mut builder = test_codex_with_home(Arc::clone(&home))
|
||||
.with_config(|config| config.project_doc_max_bytes = 1);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -346,7 +386,7 @@ async fn global_loading_warning_surfaces_during_thread_creation() -> Result<()>
|
||||
let source = write_global(home.as_ref(), b"global\xFFinstructions")?;
|
||||
|
||||
// Create the thread, capture its load warning, and submit one turn for rendered output.
|
||||
let mut builder = test_codex().with_home(home);
|
||||
let mut builder = test_codex_with_home(home);
|
||||
let test = builder.build(&server).await?;
|
||||
let warning = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::Warning(warning)
|
||||
@@ -375,6 +415,48 @@ async fn global_loading_warning_surfaces_during_thread_creation() -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn contributor_warnings_surface_and_failure_aborts_fresh_thread_creation() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let warning_contributor = Arc::new(TestGlobalInstructionsContributor::new(vec![Ok(
|
||||
contributed_instructions("global instructions", /*source*/ None, &["global warning"]),
|
||||
)]));
|
||||
let mut warning_builder =
|
||||
test_codex().with_extensions(test_instruction_extensions(Arc::clone(
|
||||
&warning_contributor,
|
||||
)));
|
||||
let warning_test = warning_builder.build(&server).await?;
|
||||
|
||||
let warning = wait_for_event_match(&warning_test.codex, |event| match event {
|
||||
EventMsg::Warning(warning) if warning.message == "global warning" => {
|
||||
Some(warning.message.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(warning, "global warning");
|
||||
assert_eq!(warning_contributor.calls(), 1);
|
||||
|
||||
let failure_contributor = Arc::new(TestGlobalInstructionsContributor::new(vec![Err(
|
||||
"contributor failed".to_string(),
|
||||
)]));
|
||||
let mut failure_builder =
|
||||
test_codex().with_extensions(test_instruction_extensions(Arc::clone(
|
||||
&failure_contributor,
|
||||
)));
|
||||
let error = match failure_builder.build(&server).await {
|
||||
Ok(_) => panic!("thread creation should fail"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert!(
|
||||
error.to_string().contains("contributor failed"),
|
||||
"unexpected creation error: {error:#}"
|
||||
);
|
||||
assert_eq!(failure_contributor.calls(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn cold_resume_restores_persisted_instruction_sources() -> Result<()> {
|
||||
// Set up an initial turn and a later cold-resumed turn against the same rollout.
|
||||
@@ -397,7 +479,7 @@ async fn cold_resume_restores_persisted_instruction_sources() -> Result<()> {
|
||||
let old_source = write_global(home.as_ref(), OLD_GLOBAL_INSTRUCTIONS)?;
|
||||
|
||||
// Create the initial thread and persist its creation-time instruction snapshot.
|
||||
let mut initial_builder = test_codex().with_home(Arc::clone(&home));
|
||||
let mut initial_builder = test_codex_with_home(Arc::clone(&home));
|
||||
let initial = initial_builder.build(&server).await?;
|
||||
|
||||
// Assert the pre-resume thread reports the source used to create its snapshot.
|
||||
@@ -421,7 +503,7 @@ async fn cold_resume_restores_persisted_instruction_sources() -> Result<()> {
|
||||
// Add a preferred override source, then cold-resume with freshly loaded configuration.
|
||||
let new_source = write_global_override(home.as_ref(), NEW_GLOBAL_INSTRUCTIONS)?;
|
||||
assert_ne!(old_source, new_source);
|
||||
let mut resume_builder = test_codex().with_home(Arc::clone(&home));
|
||||
let mut resume_builder = test_codex_with_home(Arc::clone(&home));
|
||||
let resumed = resume_builder
|
||||
.resume(&server, Arc::clone(&home), rollout_path)
|
||||
.await?;
|
||||
@@ -474,7 +556,7 @@ async fn fork_replays_rendered_instructions_from_shared_history() -> Result<()>
|
||||
let source = write_global(home.as_ref(), OLD_GLOBAL_INSTRUCTIONS)?;
|
||||
|
||||
// Create the parent and persist its creation-time instruction snapshot.
|
||||
let mut builder = test_codex().with_home(Arc::clone(&home));
|
||||
let mut builder = test_codex_with_home(Arc::clone(&home));
|
||||
let parent = builder.build(&server).await?;
|
||||
|
||||
// Assert the parent reports the source used to create its snapshot.
|
||||
@@ -628,8 +710,7 @@ async fn run_subagent_global_instruction_case(fork_context: bool) -> Result<()>
|
||||
// Create the parent thread, record its source, and seed the history inherited by the child.
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let source = write_global(home.as_ref(), OLD_GLOBAL_INSTRUCTIONS)?;
|
||||
let mut builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
let mut builder = test_codex_with_home(Arc::clone(&home))
|
||||
.with_config(|config| {
|
||||
let _ = config.features.enable(Feature::Collab);
|
||||
let _ = config.features.disable(Feature::EnableRequestCompression);
|
||||
@@ -671,12 +752,21 @@ async fn run_subagent_global_instruction_case(fork_context: bool) -> Result<()>
|
||||
.await
|
||||
.map_err(|_| anyhow!("timed out waiting for the subagent request"))?;
|
||||
|
||||
// Assert parent and child report and render the parent's creation-time snapshot exactly once.
|
||||
let expected_fragment =
|
||||
// The parent and forked child inherit the creation-time snapshot. A fresh child resolves the
|
||||
// contributor again and sees the new preferred override.
|
||||
let parent_fragment =
|
||||
expected_instruction_fragment(&test.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
||||
assert_single_instruction_fragment(&seed_request, &expected_fragment);
|
||||
assert_single_instruction_fragment(&spawn_request, &expected_fragment);
|
||||
assert_single_instruction_fragment(&child_request, &expected_fragment);
|
||||
let (child_source, child_fragment) = if fork_context {
|
||||
(source.clone(), parent_fragment.clone())
|
||||
} else {
|
||||
(
|
||||
new_source,
|
||||
expected_instruction_fragment(&test.config.cwd, NEW_GLOBAL_INSTRUCTIONS),
|
||||
)
|
||||
};
|
||||
assert_single_instruction_fragment(&seed_request, &parent_fragment);
|
||||
assert_single_instruction_fragment(&spawn_request, &parent_fragment);
|
||||
assert_single_instruction_fragment(&child_request, &child_fragment);
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source.clone()],
|
||||
@@ -684,8 +774,8 @@ async fn run_subagent_global_instruction_case(fork_context: bool) -> Result<()>
|
||||
);
|
||||
assert_eq!(
|
||||
child_thread.instruction_sources().await,
|
||||
vec![source],
|
||||
"subagent reports the parent's creation-time source"
|
||||
vec![child_source],
|
||||
"subagent reports the source selected for its history mode"
|
||||
);
|
||||
if fork_context {
|
||||
let seed_input = seed_request.input();
|
||||
@@ -719,7 +809,7 @@ async fn run_subagent_global_instruction_case(fork_context: bool) -> Result<()>
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn manual_compaction_keeps_the_creation_time_global_instructions() -> Result<()> {
|
||||
async fn manual_compaction_refreshes_global_instructions_on_the_next_full_rebuild() -> Result<()> {
|
||||
// Set up an initial turn, a manual compaction response, and a post-compaction turn.
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_sequence(
|
||||
@@ -746,8 +836,7 @@ async fn manual_compaction_keeps_the_creation_time_global_instructions() -> Resu
|
||||
let provider = local_compaction_provider(&server);
|
||||
|
||||
// Create the thread with the old global source loaded into its instruction snapshot.
|
||||
let mut builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
let mut builder = test_codex_with_home(Arc::clone(&home))
|
||||
.with_config(move |config| {
|
||||
config.model_provider = provider;
|
||||
});
|
||||
@@ -772,26 +861,27 @@ async fn manual_compaction_keeps_the_creation_time_global_instructions() -> Resu
|
||||
.await;
|
||||
test.submit_turn("after compact").await?;
|
||||
|
||||
// Assert ordinary and compact turns keep the old rendering even though the reported source
|
||||
// path now contains new text.
|
||||
// The compact request uses the existing snapshot. The following full-context rebuild resolves
|
||||
// the contributor again and installs the current contents.
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
let expected_fragment =
|
||||
let old_fragment =
|
||||
expected_instruction_fragment(&test.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
||||
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
||||
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
||||
assert_single_instruction_fragment(&requests[2], &expected_fragment);
|
||||
let new_fragment = expected_instruction_fragment(&test.config.cwd, NEW_GLOBAL_INSTRUCTIONS);
|
||||
assert_single_instruction_fragment(&requests[0], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[1], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[2], &new_fragment);
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source],
|
||||
"thread retains the creation-time global source after compaction"
|
||||
"same-path refresh keeps the selected source path"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn mid_turn_compaction_keeps_the_creation_time_global_instructions() -> Result<()> {
|
||||
async fn mid_turn_compaction_refreshes_global_instructions_for_the_resumed_request() -> Result<()> {
|
||||
// Set up a turn that crosses the auto-compaction limit and a post-compaction response.
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_sequence(
|
||||
@@ -817,8 +907,7 @@ async fn mid_turn_compaction_keeps_the_creation_time_global_instructions() -> Re
|
||||
let provider = local_compaction_provider(&server);
|
||||
|
||||
// Create the thread with the old global source loaded into its instruction snapshot.
|
||||
let mut builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
let mut builder = test_codex_with_home(Arc::clone(&home))
|
||||
.with_config(move |config| {
|
||||
config.model_provider = provider;
|
||||
config.model_context_window = Some(100);
|
||||
@@ -838,18 +927,20 @@ async fn mid_turn_compaction_keeps_the_creation_time_global_instructions() -> Re
|
||||
assert_ne!(source, new_source);
|
||||
test.submit_turn("trigger mid-turn compaction").await?;
|
||||
|
||||
// Assert the initial, compact, and resumed requests all keep the old snapshot and source.
|
||||
// The initial and compact requests use the old snapshot. The resumed request resolves the
|
||||
// contributor again and adopts the preferred override.
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
let expected_fragment =
|
||||
let old_fragment =
|
||||
expected_instruction_fragment(&test.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
||||
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
||||
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
||||
assert_single_instruction_fragment(&requests[2], &expected_fragment);
|
||||
let new_fragment = expected_instruction_fragment(&test.config.cwd, NEW_GLOBAL_INSTRUCTIONS);
|
||||
assert_single_instruction_fragment(&requests[0], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[1], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[2], &new_fragment);
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![source],
|
||||
"thread retains the creation-time global source after mid-turn compaction"
|
||||
vec![new_source],
|
||||
"mid-turn full-context rebuild reports the refreshed source"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -859,6 +950,89 @@ async fn mid_turn_compaction_keeps_the_creation_time_global_instructions() -> Re
|
||||
// later full-context rebuilds. Reloading file contents into historical context currently rewrites
|
||||
// model-visible history and invalidates the cached prefix; decide whether the original item should
|
||||
// remain stable instead.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn mid_turn_refresh_failure_warns_and_retains_the_previous_snapshot() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
responses::sse(vec![
|
||||
responses::ev_function_call("call-1", "unsupported_tool", "{}"),
|
||||
responses::ev_completed_with_tokens("first-response", /*total_tokens*/ 96),
|
||||
]),
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("compact-message", "summary"),
|
||||
responses::ev_completed_with_tokens("compact-response", /*total_tokens*/ 10),
|
||||
]),
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("final-message", "done"),
|
||||
responses::ev_completed_with_tokens(
|
||||
"follow-up-response",
|
||||
/*total_tokens*/ 10,
|
||||
),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let contributor = Arc::new(TestGlobalInstructionsContributor::new(vec![
|
||||
Ok(contributed_instructions(
|
||||
OLD_GLOBAL_INSTRUCTIONS,
|
||||
/*source*/ None,
|
||||
&[],
|
||||
)),
|
||||
Err("refresh failed".to_string()),
|
||||
]));
|
||||
let provider = local_compaction_provider(&server);
|
||||
let mut builder = test_codex()
|
||||
.with_extensions(test_instruction_extensions(Arc::clone(&contributor)))
|
||||
.with_config(move |config| {
|
||||
config.model_provider = provider;
|
||||
config.model_context_window = Some(100);
|
||||
config.model_auto_compact_token_limit = Some(90);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "trigger mid-turn compaction".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
let warning = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::Warning(warning) if warning.message.contains("refresh failed") => {
|
||||
Some(warning.message.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
warning.contains("continuing with the previous snapshot"),
|
||||
"unexpected refresh warning: {warning}"
|
||||
);
|
||||
assert_eq!(contributor.calls(), 2);
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
let old_fragment =
|
||||
expected_instruction_fragment(&test.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
||||
assert_single_instruction_fragment(&requests[0], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[1], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[2], &old_fragment);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn cold_resume_then_full_context_rebuild_uses_current_instructions() -> Result<()> {
|
||||
// Set up an initial turn, a cold-resumed turn, manual compaction, and the later full-context
|
||||
@@ -892,7 +1066,7 @@ async fn cold_resume_then_full_context_rebuild_uses_current_instructions() -> Re
|
||||
let source = write_global(home.as_ref(), OLD_GLOBAL_INSTRUCTIONS)?;
|
||||
|
||||
// Create the initial thread and persist its creation-time instruction snapshot.
|
||||
let mut initial_builder = test_codex().with_home(Arc::clone(&home)).with_config({
|
||||
let mut initial_builder = test_codex_with_home(Arc::clone(&home)).with_config({
|
||||
let provider = provider.clone();
|
||||
move |config| config.model_provider = provider
|
||||
});
|
||||
@@ -919,8 +1093,7 @@ async fn cold_resume_then_full_context_rebuild_uses_current_instructions() -> Re
|
||||
// Rewrite the selected AGENTS.md in place, then cold-resume with freshly loaded configuration.
|
||||
let rewritten_source = write_global(home.as_ref(), NEW_GLOBAL_INSTRUCTIONS)?;
|
||||
assert_eq!(source, rewritten_source);
|
||||
let mut resume_builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
let mut resume_builder = test_codex_with_home(Arc::clone(&home))
|
||||
.with_config(move |config| config.model_provider = provider);
|
||||
let resumed = resume_builder
|
||||
.resume(&server, Arc::clone(&home), rollout_path)
|
||||
@@ -1003,7 +1176,7 @@ async fn legacy_compaction_without_replacement_history_rebuilds_current_instruct
|
||||
let provider = local_compaction_provider(&server);
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let source = write_global(home.as_ref(), OLD_GLOBAL_INSTRUCTIONS)?;
|
||||
let mut initial_builder = test_codex().with_home(Arc::clone(&home)).with_config({
|
||||
let mut initial_builder = test_codex_with_home(Arc::clone(&home)).with_config({
|
||||
let provider = provider.clone();
|
||||
move |config| config.model_provider = provider
|
||||
});
|
||||
@@ -1032,8 +1205,7 @@ async fn legacy_compaction_without_replacement_history_rebuilds_current_instruct
|
||||
// Rewrite the selected file in place and cold-resume with current configuration.
|
||||
let rewritten_source = write_global(home.as_ref(), NEW_GLOBAL_INSTRUCTIONS)?;
|
||||
assert_eq!(source, rewritten_source);
|
||||
let mut resume_builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
let mut resume_builder = test_codex_with_home(Arc::clone(&home))
|
||||
.with_config(move |config| config.model_provider = provider);
|
||||
let resumed = resume_builder
|
||||
.resume(&server, Arc::clone(&home), rollout_path)
|
||||
@@ -1086,8 +1258,7 @@ async fn remote_v2_compaction_keeps_creation_time_instructions_after_same_path_m
|
||||
.await;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let source = write_global(home.as_ref(), OLD_GLOBAL_INSTRUCTIONS)?;
|
||||
let mut builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
let mut builder = test_codex_with_home(Arc::clone(&home))
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
let _ = config.features.enable(Feature::RemoteCompactionV2);
|
||||
@@ -1106,14 +1277,15 @@ async fn remote_v2_compaction_keeps_creation_time_instructions_after_same_path_m
|
||||
test.submit_turn("after remote v2 compaction").await?;
|
||||
test.codex.flush_rollout().await?;
|
||||
|
||||
// Assert the compact request, installed replacement history, and follow-up all keep the
|
||||
// creation-time item despite the file-backed source now containing new text.
|
||||
// The compact request retains the old snapshot. The post-compact full-context rebuild resolves
|
||||
// the contributor again and installs the rewritten contents.
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
let old_fragment = expected_instruction_fragment(&test.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
||||
let new_fragment = expected_instruction_fragment(&test.config.cwd, NEW_GLOBAL_INSTRUCTIONS);
|
||||
assert_single_instruction_fragment(&requests[0], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[1], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[2], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[2], &new_fragment);
|
||||
assert_eq!(
|
||||
requests[1].input().last(),
|
||||
Some(&json!({"type": "compaction_trigger"})),
|
||||
@@ -1144,8 +1316,7 @@ async fn remote_v2_compaction_keeps_creation_time_instructions_after_same_path_m
|
||||
})
|
||||
.await;
|
||||
let resumed_cwd = test.config.cwd.clone();
|
||||
let mut resume_builder = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
let mut resume_builder = test_codex_with_home(Arc::clone(&home))
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
config.cwd = resumed_cwd;
|
||||
@@ -1158,11 +1329,11 @@ async fn remote_v2_compaction_keeps_creation_time_instructions_after_same_path_m
|
||||
.submit_turn("after remote v2 compaction cold resume")
|
||||
.await?;
|
||||
|
||||
// Modern replacement-history resume replays the persisted checkpoint and its later old-context
|
||||
// suffix even though the same source path now contains new text.
|
||||
// Modern replacement-history resume replays the persisted checkpoint and its later refreshed
|
||||
// full-context suffix.
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 4);
|
||||
assert_single_instruction_fragment(&requests[3], &old_fragment);
|
||||
assert_single_instruction_fragment(&requests[3], &new_fragment);
|
||||
let resumed_input = requests[3].input();
|
||||
assert_eq!(
|
||||
resumed_input.get(..replacement_history.len()),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use codex_core::LoadedAgentsMd;
|
||||
use codex_core::shell::default_user_shell;
|
||||
use codex_features::Feature;
|
||||
use codex_prompts::APPLY_PATCH_TOOL_INSTRUCTIONS;
|
||||
@@ -122,10 +121,8 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
|
||||
thread_manager,
|
||||
..
|
||||
} = test_codex()
|
||||
.with_global_instructions("be consistent and helpful")
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
|
||||
"be consistent and helpful",
|
||||
));
|
||||
config.model = Some("gpt-5.2".to_string());
|
||||
// Keep tool expectations stable when the default web_search mode changes.
|
||||
config
|
||||
@@ -236,10 +233,8 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an
|
||||
.await;
|
||||
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_global_instructions("be consistent and helpful")
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
|
||||
"be consistent and helpful",
|
||||
));
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CollaborationModes)
|
||||
@@ -320,10 +315,8 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
|
||||
.await;
|
||||
|
||||
let TestCodex { codex, config, .. } = test_codex()
|
||||
.with_global_instructions("be consistent and helpful")
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
|
||||
"be consistent and helpful",
|
||||
));
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CollaborationModes)
|
||||
@@ -420,10 +413,8 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
|
||||
.await;
|
||||
|
||||
let TestCodex { codex, config, .. } = test_codex()
|
||||
.with_global_instructions("be consistent and helpful")
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
|
||||
"be consistent and helpful",
|
||||
));
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CollaborationModes)
|
||||
@@ -714,10 +705,8 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
|
||||
.await;
|
||||
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_global_instructions("be consistent and helpful")
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
|
||||
"be consistent and helpful",
|
||||
));
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CollaborationModes)
|
||||
@@ -851,10 +840,8 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
session_configured,
|
||||
..
|
||||
} = test_codex()
|
||||
.with_global_instructions("be consistent and helpful")
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
|
||||
"be consistent and helpful",
|
||||
));
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CollaborationModes)
|
||||
@@ -994,10 +981,8 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
|
||||
session_configured,
|
||||
..
|
||||
} = test_codex()
|
||||
.with_global_instructions("be consistent and helpful")
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
|
||||
"be consistent and helpful",
|
||||
));
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CollaborationModes)
|
||||
|
||||
@@ -23,6 +23,7 @@ codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
|
||||
@@ -6,7 +6,8 @@ use codex_core::StateDbHandle;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_extension_api::empty_extension_registry;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_home::CodexHomeInstructionsContributor;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::default_client::USER_AGENT_SUFFIX;
|
||||
use codex_login::default_client::get_codex_user_agent;
|
||||
@@ -62,12 +63,16 @@ impl MessageProcessor {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await;
|
||||
let mut extension_builder = ExtensionRegistryBuilder::<Config>::new();
|
||||
extension_builder.global_instructions_contributor(Arc::new(
|
||||
CodexHomeInstructionsContributor::new(config.codex_home.clone()),
|
||||
));
|
||||
let thread_manager = Arc::new(ThreadManager::new(
|
||||
config.as_ref(),
|
||||
auth_manager,
|
||||
SessionSource::Mcp,
|
||||
environment_manager,
|
||||
empty_extension_registry(),
|
||||
Arc::new(extension_builder.build()),
|
||||
/*analytics_events_client*/ None,
|
||||
codex_core::thread_store_from_config(config.as_ref(), state_db.clone()),
|
||||
state_db.clone(),
|
||||
|
||||
@@ -16,6 +16,7 @@ use codex_core_api::AskForApproval;
|
||||
use codex_core_api::AuthCredentialsStoreMode;
|
||||
use codex_core_api::AuthManager;
|
||||
use codex_core_api::AutoCompactTokenLimitScope;
|
||||
use codex_core_api::CodexHomeInstructionsContributor;
|
||||
use codex_core_api::CodexThread;
|
||||
use codex_core_api::Config;
|
||||
use codex_core_api::ConfigLayerStack;
|
||||
@@ -23,6 +24,7 @@ use codex_core_api::Constrained;
|
||||
use codex_core_api::EnvironmentManager;
|
||||
use codex_core_api::EventMsg;
|
||||
use codex_core_api::ExecServerRuntimePaths;
|
||||
use codex_core_api::ExtensionRegistryBuilder;
|
||||
use codex_core_api::Features;
|
||||
use codex_core_api::GhostSnapshotConfig;
|
||||
use codex_core_api::History;
|
||||
@@ -54,7 +56,6 @@ use codex_core_api::UserInput;
|
||||
use codex_core_api::WebSearchMode;
|
||||
use codex_core_api::arg0_dispatch_or_else;
|
||||
use codex_core_api::built_in_model_providers;
|
||||
use codex_core_api::empty_extension_registry;
|
||||
use codex_core_api::find_codex_home;
|
||||
use codex_core_api::init_state_db;
|
||||
use codex_core_api::item_event_to_server_notification;
|
||||
@@ -120,12 +121,16 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
.await?,
|
||||
);
|
||||
let installation_id = resolve_installation_id(&config.codex_home).await?;
|
||||
let mut extension_builder = ExtensionRegistryBuilder::<Config>::new();
|
||||
extension_builder.global_instructions_contributor(Arc::new(
|
||||
CodexHomeInstructionsContributor::new(config.codex_home.clone()),
|
||||
));
|
||||
let thread_manager = ThreadManager::new(
|
||||
&config,
|
||||
auth_manager,
|
||||
SessionSource::Exec,
|
||||
environment_manager,
|
||||
empty_extension_registry(),
|
||||
Arc::new(extension_builder.build()),
|
||||
/*analytics_events_client*/ None,
|
||||
Arc::clone(&thread_store),
|
||||
state_db,
|
||||
@@ -184,7 +189,6 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
|
||||
hide_agent_reasoning: false,
|
||||
show_raw_agent_reasoning: false,
|
||||
user_instructions: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
guardian_policy_config: None,
|
||||
|
||||
Reference in New Issue
Block a user