Route AGENTS.md loading through environment filesystems (#26205)

## Why

Workspace-specific `AGENTS.md` loading needs to use the selected
environment filesystem so remote workspaces and child agents read
instructions from their actual environment instead of the host
filesystem. The app-server should report the same instruction sources
the initialized thread actually loaded, rather than independently
rescanning configuration and filesystem state.

## What changed

- Introduce `LoadedAgentsMd` to retain ordered user, project, and
internal instructions with their provenance.
- Load and canonicalize workspace `AGENTS.md` paths through the primary
`EnvironmentManager` environment, then render the loaded instructions
when constructing turn context.
- Expose cached loaded instruction sources from initialized threads and
use them for app-server start, resume, and fork responses.
- Preserve global `CODEX_HOME` loading and separator behavior while
excluding empty project files that did not supply model-visible
instructions.
- Add integration coverage for CLI injection, selected-environment
provenance and rendering, empty environment selection, and cached
sources on loaded-thread resume.

## Validation

- `just test -p codex-core agents_md`
- `just test -p codex-core
selected_environment_sources_match_model_visible_instructions`
- `just test -p codex-exec agents_md`
- `just test -p codex-app-server instruction_sources`
- `just test -p codex-app-server --status-level fail`
This commit is contained in:
Adam Perry @ OpenAI
2026-06-04 12:43:07 -07:00
committed by GitHub
Unverified
parent c3fcb0e745
commit e64b469bbc
22 changed files with 740 additions and 137 deletions
+136 -68
View File
@@ -23,11 +23,9 @@ use codex_config::merge_toml_values;
use codex_config::project_root_markers_from_config;
use codex_exec_server::Environment;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
use codex_prompts::HIERARCHICAL_AGENTS_MESSAGE;
use codex_utils_absolute_path::AbsolutePathBuf;
use dunce::canonicalize as normalize_path;
use std::io;
use toml::Value as TomlValue;
use tracing::error;
@@ -37,8 +35,8 @@ pub const DEFAULT_AGENTS_MD_FILENAME: &str = "AGENTS.md";
/// Preferred local override for AGENTS.md instructions.
pub const LOCAL_AGENTS_MD_FILENAME: &str = "AGENTS.override.md";
/// When both `Config::instructions` and AGENTS.md docs are present, they will
/// be concatenated with the following separator.
/// When both user and project AGENTS.md docs are present, they will be
/// concatenated with the following separator.
const AGENTS_MD_SEPARATOR: &str = "\n\n--- project-doc ---\n\n";
/// Resolves AGENTS.md files into model-visible user instructions and source
@@ -47,11 +45,6 @@ pub struct AgentsMdManager<'a> {
config: &'a Config,
}
pub(crate) struct LoadedAgentsMd {
pub(crate) contents: String,
pub(crate) path: AbsolutePathBuf,
}
impl<'a> AgentsMdManager<'a> {
pub fn new(config: &'a Config) -> Self {
Self { config }
@@ -81,10 +74,7 @@ impl<'a> AgentsMdManager<'a> {
let contents = String::from_utf8_lossy(&data);
let trimmed = contents.trim();
if !trimmed.is_empty() {
return Some(LoadedAgentsMd {
contents: trimmed.to_string(),
path,
});
return Some(LoadedAgentsMd::new_user(trimmed.to_string(), path));
}
}
None
@@ -94,10 +84,10 @@ impl<'a> AgentsMdManager<'a> {
/// single model-visible instruction string.
pub(crate) async fn user_instructions(
&self,
environment: Option<&Environment>,
environment: &Environment,
startup_warnings: &mut Vec<String>,
) -> Option<String> {
let fs = environment?.get_filesystem();
) -> Option<LoadedAgentsMd> {
let fs = environment.get_filesystem();
self.user_instructions_with_fs(fs.as_ref(), startup_warnings)
.await
}
@@ -106,22 +96,13 @@ impl<'a> AgentsMdManager<'a> {
&self,
fs: &dyn ExecutorFileSystem,
startup_warnings: &mut Vec<String>,
) -> Option<String> {
) -> Option<LoadedAgentsMd> {
let agents_md_docs = self.read_agents_md(fs, startup_warnings).await;
let mut output = String::new();
if let Some(instructions) = self.config.user_instructions.clone() {
output.push_str(&instructions);
}
let mut loaded = self.config.user_instructions.clone().unwrap_or_default();
match agents_md_docs {
Ok(Some(docs)) => {
if !output.is_empty() {
output.push_str(AGENTS_MD_SEPARATOR);
}
output.push_str(&docs);
}
Ok(Some(docs)) => loaded.entries.extend(docs.entries),
Ok(None) => {}
Err(e) => {
error!("error trying to find AGENTS.md docs: {e:#}");
@@ -129,50 +110,26 @@ impl<'a> AgentsMdManager<'a> {
};
if self.config.features.enabled(Feature::ChildAgentsMd) {
if !output.is_empty() {
output.push_str("\n\n");
}
output.push_str(HIERARCHICAL_AGENTS_MESSAGE);
loaded.entries.push(InstructionEntry {
contents: HIERARCHICAL_AGENTS_MESSAGE.to_string(),
provenance: InstructionProvenance::Internal,
});
}
if !output.is_empty() {
Some(output)
} else {
None
}
}
/// Returns all instruction source files included in the current config.
pub async fn instruction_sources(&self, fs: &dyn ExecutorFileSystem) -> Vec<AbsolutePathBuf> {
let mut global_instruction_warnings = Vec::new();
let mut paths = Self::load_global_instructions(
LOCAL_FS.as_ref(),
Some(&self.config.codex_home),
&mut global_instruction_warnings,
)
.await
.map(|loaded| vec![loaded.path])
.unwrap_or_default();
match self.agents_md_paths(fs).await {
Ok(agents_md_paths) => paths.extend(agents_md_paths),
Err(err) => {
tracing::warn!(error = %err, "failed to discover AGENTS.md docs for instruction sources");
}
}
paths
(!loaded.is_empty()).then_some(loaded)
}
/// Attempt to locate and load AGENTS.md documentation.
///
/// On success returns `Ok(Some(contents))` where `contents` is the
/// concatenation of all discovered docs. If no documentation file is found
/// the function returns `Ok(None)`. Unexpected I/O failures bubble up as
/// `Err` so callers can decide how to handle them.
/// On success returns `Ok(Some(loaded))` where `loaded` contains every
/// discovered doc. If no documentation file is found the function returns
/// `Ok(None)`. Unexpected I/O failures bubble up as `Err` so callers can
/// decide how to handle them.
async fn read_agents_md(
&self,
fs: &dyn ExecutorFileSystem,
startup_warnings: &mut Vec<String>,
) -> io::Result<Option<String>> {
) -> io::Result<Option<LoadedAgentsMd>> {
let max_total = self.config.project_doc_max_bytes;
if max_total == 0 {
@@ -185,7 +142,7 @@ impl<'a> AgentsMdManager<'a> {
}
let mut remaining: u64 = max_total as u64;
let mut parts: Vec<String> = Vec::new();
let mut loaded = LoadedAgentsMd::default();
for p in paths {
if remaining == 0 {
@@ -221,15 +178,18 @@ impl<'a> AgentsMdManager<'a> {
let text = String::from_utf8_lossy(&data).to_string();
if !text.trim().is_empty() {
parts.push(text);
loaded.entries.push(InstructionEntry {
contents: text,
provenance: InstructionProvenance::Project(p),
});
remaining = remaining.saturating_sub(data.len() as u64);
}
}
if parts.is_empty() {
if loaded.is_empty() {
Ok(None)
} else {
Ok(Some(parts.join("\n\n")))
Ok(Some(loaded))
}
}
@@ -247,8 +207,8 @@ impl<'a> AgentsMdManager<'a> {
}
let mut dir = self.config.cwd.clone();
if let Ok(canon) = normalize_path(&dir) {
dir = AbsolutePathBuf::try_from(canon)?;
if let Ok(canonical_dir) = fs.canonicalize(&dir, /*sandbox*/ None).await {
dir = canonical_dir;
}
let mut merged = TomlValue::Table(toml::map::Map::new());
@@ -348,6 +308,114 @@ impl<'a> AgentsMdManager<'a> {
}
}
/// Model-visible instructions loaded from AGENTS.md files and internal
/// guidance.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LoadedAgentsMd {
/// Ordered instructions and their provenance.
entries: Vec<InstructionEntry>,
}
impl LoadedAgentsMd {
/// Creates loaded instructions containing one user-level AGENTS.md entry.
pub fn new_user(contents: String, path: AbsolutePathBuf) -> Self {
if contents.trim().is_empty() {
return Self::default();
}
Self {
entries: vec![InstructionEntry {
contents,
provenance: InstructionProvenance::User(path),
}],
}
}
/// Creates source-less user instructions for tests.
///
/// This cannot be gated with `#[cfg(test)]` because integration tests
/// compile `codex-core` as a normal dependency without that configuration.
pub fn from_text_for_testing(contents: impl Into<String>) -> Self {
let contents = contents.into();
if contents.trim().is_empty() {
return Self::default();
}
Self {
entries: vec![InstructionEntry {
contents,
provenance: InstructionProvenance::Internal,
}],
}
}
fn is_empty(&self) -> bool {
self.entries
.iter()
.all(|entry| entry.contents.trim().is_empty())
}
/// Returns the concatenated model-visible instruction text.
pub fn text(&self) -> String {
let mut output = String::new();
let mut previous_provenance: Option<&InstructionProvenance> = None;
for entry in &self.entries {
if let Some(previous_provenance) = previous_provenance {
// The project-doc marker tells the model where workspace-scoped
// instructions begin, so it is only needed on the transition
// from user or internal instructions to project instructions.
let separator = match (previous_provenance, &entry.provenance) {
(
InstructionProvenance::User(_) | InstructionProvenance::Internal,
InstructionProvenance::Project(_),
) => AGENTS_MD_SEPARATOR,
_ => "\n\n",
};
output.push_str(separator);
}
output.push_str(&entry.contents);
previous_provenance = Some(&entry.provenance);
}
output
}
/// Returns the AGENTS.md files that supplied instruction entries.
pub fn sources(&self) -> impl Iterator<Item = &AbsolutePathBuf> {
self.entries
.iter()
.filter_map(|entry| entry.provenance.path())
}
}
/// One model-visible instruction and its provenance.
#[derive(Clone, Debug, PartialEq, Eq)]
struct InstructionEntry {
/// Model-visible instruction text.
contents: String,
/// Origin of the instruction.
provenance: InstructionProvenance,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum InstructionProvenance {
/// User-level instructions, normally loaded from CODEX_HOME.
User(AbsolutePathBuf),
/// Workspace instructions discovered from project AGENTS.md files.
Project(AbsolutePathBuf),
/// Instructions without a file source, including internally defined guidance.
Internal,
}
impl InstructionProvenance {
fn path(&self) -> Option<&AbsolutePathBuf> {
match self {
Self::User(path) | Self::Project(path) => Some(path),
Self::Internal => None,
}
}
}
fn warn_invalid_utf8(
path: &AbsolutePathBuf,
data: &[u8],
+222 -20
View File
@@ -16,6 +16,7 @@ async fn get_user_instructions(config: &Config) -> Option<String> {
AgentsMdManager::new(config)
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
.await
.map(|loaded| loaded.text())
}
async fn agents_md_paths(config: &Config) -> std::io::Result<Vec<AbsolutePathBuf>> {
@@ -53,7 +54,12 @@ async fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -
config.cwd = root.abs();
config.project_doc_max_bytes = limit;
config.user_instructions = instructions.map(ToOwned::to_owned);
config.user_instructions = instructions.map(|text| {
LoadedAgentsMd::new_user(
text.to_owned(),
config.codex_home.join(DEFAULT_AGENTS_MD_FILENAME),
)
});
config
}
@@ -96,7 +102,12 @@ async fn make_config_with_project_root_markers(
config.cwd = root.abs();
config.project_doc_max_bytes = limit;
config.user_instructions = instructions.map(ToOwned::to_owned);
config.user_instructions = instructions.map(|text| {
LoadedAgentsMd::new_user(
text.to_owned(),
config.codex_home.join(DEFAULT_AGENTS_MD_FILENAME),
)
});
config
}
@@ -115,17 +126,46 @@ async fn no_doc_file_returns_none() {
assert!(res.is_none(), "Expected None when AGENTS.md is absent");
}
#[tokio::test]
async fn no_environment_returns_none() {
let tmp = tempfile::tempdir().expect("tempdir");
let config = make_config(&tmp, /*limit*/ 4096, Some("user instructions")).await;
#[test]
fn empty_loaded_instructions_are_empty() {
let source =
AbsolutePathBuf::from_absolute_path("/tmp/AGENTS.md").expect("absolute source path");
let mut warnings = Vec::new();
let res = AgentsMdManager::new(&config)
.user_instructions(/*environment*/ None, &mut warnings)
.await;
assert_eq!(
LoadedAgentsMd::new_user(String::new(), source.clone()),
LoadedAgentsMd::default()
);
assert_eq!(
LoadedAgentsMd::new_user(" \n\t".to_string(), source),
LoadedAgentsMd::default()
);
assert_eq!(
LoadedAgentsMd::from_text_for_testing(String::new()),
LoadedAgentsMd::default()
);
assert_eq!(
LoadedAgentsMd::from_text_for_testing(" \n\t"),
LoadedAgentsMd::default()
);
}
assert_eq!(res, None);
#[test]
fn loaded_instructions_with_only_empty_or_whitespace_entries_are_empty() {
let empty = LoadedAgentsMd {
entries: vec![InstructionEntry {
contents: String::new(),
provenance: InstructionProvenance::Internal,
}],
};
let whitespace = LoadedAgentsMd {
entries: vec![InstructionEntry {
contents: " \n\t".to_string(),
provenance: InstructionProvenance::Internal,
}],
};
assert!(empty.is_empty());
assert!(whitespace.is_empty());
}
/// Small file within the byte-limit is returned unmodified.
@@ -161,8 +201,10 @@ async fn global_doc_invalid_utf8_warns_and_uses_lossy_text() {
.await
.expect("global doc expected");
assert_eq!(loaded.contents, "global\u{FFFD} doc");
assert_eq!(loaded.path, path);
assert_eq!(
loaded,
LoadedAgentsMd::new_user("global\u{FFFD} doc".to_string(), path.clone())
);
assert_invalid_utf8_warning(&warnings, "Global", path.as_path());
}
@@ -177,7 +219,8 @@ async fn project_doc_invalid_utf8_warns_and_uses_lossy_text() {
let res = AgentsMdManager::new(&config)
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
.await
.expect("doc expected");
.expect("doc expected")
.text();
assert_eq!(res, "project\u{FFFD} doc");
let canonical_path = dunce::canonicalize(&path).expect("canonical doc path");
@@ -272,6 +315,33 @@ async fn merges_existing_instructions_with_agents_md() {
assert_eq!(res, expected);
}
#[tokio::test]
async fn sourceless_user_instructions_preserve_separator_without_reporting_a_source() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap();
let mut cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
cfg.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"user instructions".to_string(),
));
let mut warnings = Vec::new();
let loaded = AgentsMdManager::new(&cfg)
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
.await
.expect("instructions expected");
let project_agents = AbsolutePathBuf::try_from(
dunce::canonicalize(cfg.cwd.join("AGENTS.md")).expect("canonical project doc path"),
)
.expect("absolute project doc path");
assert_eq!(
loaded.text(),
format!("user instructions{AGENTS_MD_SEPARATOR}project doc")
);
assert_eq!(loaded.sources().collect::<Vec<_>>(), vec![&project_agents]);
}
/// If there are existing system instructions but AGENTS.md docs are
/// missing we expect the original instructions to be returned unchanged.
#[tokio::test]
@@ -310,8 +380,38 @@ async fn concatenates_root_and_cwd_docs() {
let mut cfg = make_config(&repo, /*limit*/ 4096, /*instructions*/ None).await;
cfg.cwd = nested.abs();
let res = get_user_instructions(&cfg).await.expect("doc expected");
assert_eq!(res, "root doc\n\ncrate doc");
let mut warnings = Vec::new();
let loaded = AgentsMdManager::new(&cfg)
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
.await
.expect("doc expected");
let root_agents = AbsolutePathBuf::try_from(
dunce::canonicalize(repo.path().join("AGENTS.md")).expect("canonical root doc path"),
)
.expect("absolute root doc path");
let crate_agents = AbsolutePathBuf::try_from(
dunce::canonicalize(cfg.cwd.join("AGENTS.md")).expect("canonical crate doc path"),
)
.expect("absolute crate doc path");
let expected = LoadedAgentsMd {
entries: vec![
InstructionEntry {
contents: "root doc".to_string(),
provenance: InstructionProvenance::Project(root_agents.clone()),
},
InstructionEntry {
contents: "crate doc".to_string(),
provenance: InstructionProvenance::Project(crate_agents.clone()),
},
],
};
assert_eq!(loaded, expected);
assert_eq!(loaded.text(), "root doc\n\ncrate doc");
assert_eq!(
loaded.sources().collect::<Vec<_>>(),
vec![&root_agents, &crate_agents]
);
}
#[tokio::test]
@@ -350,6 +450,38 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
assert_eq!(res, "parent doc\n\nchild doc");
}
#[tokio::test]
async fn child_agents_message_after_global_instructions_uses_plain_separator() {
let tmp = tempfile::tempdir().expect("tempdir");
let mut cfg = make_config(&tmp, /*limit*/ 4096, Some("global doc")).await;
cfg.features.enable(Feature::ChildAgentsMd).unwrap();
let mut warnings = Vec::new();
let loaded = AgentsMdManager::new(&cfg)
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
.await
.expect("instructions expected");
let global_agents = cfg.codex_home.join(DEFAULT_AGENTS_MD_FILENAME);
let expected = LoadedAgentsMd {
entries: vec![
InstructionEntry {
contents: "global doc".to_string(),
provenance: InstructionProvenance::User(global_agents),
},
InstructionEntry {
contents: HIERARCHICAL_AGENTS_MESSAGE.to_string(),
provenance: InstructionProvenance::Internal,
},
],
};
assert_eq!(loaded, expected);
assert_eq!(
loaded.text(),
format!("global doc\n\n{HIERARCHICAL_AGENTS_MESSAGE}")
);
}
#[tokio::test]
async fn instruction_sources_include_global_before_agents_md_docs() {
let tmp = tempfile::tempdir().expect("tempdir");
@@ -360,15 +492,85 @@ async fn instruction_sources_include_global_before_agents_md_docs() {
fs::create_dir_all(&cfg.codex_home).unwrap();
fs::write(&global_agents, "global doc").unwrap();
let sources = AgentsMdManager::new(&cfg)
.instruction_sources(LOCAL_FS.as_ref())
.await;
let mut warnings = Vec::new();
let loaded = AgentsMdManager::new(&cfg)
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
.await
.expect("instructions expected");
let project_agents = AbsolutePathBuf::try_from(
dunce::canonicalize(cfg.cwd.join("AGENTS.md")).expect("canonical project doc path"),
)
.expect("absolute project doc path");
assert_eq!(sources, vec![global_agents, project_agents]);
let expected = LoadedAgentsMd {
entries: vec![
InstructionEntry {
contents: "global doc".to_string(),
provenance: InstructionProvenance::User(global_agents.clone()),
},
InstructionEntry {
contents: "project doc".to_string(),
provenance: InstructionProvenance::Project(project_agents.clone()),
},
],
};
assert_eq!(loaded, expected);
assert_eq!(
loaded.sources().collect::<Vec<_>>(),
vec![&global_agents, &project_agents]
);
assert_eq!(
loaded.text(),
format!("global doc{AGENTS_MD_SEPARATOR}project doc")
);
}
#[tokio::test]
async fn child_agents_message_after_project_docs_is_not_an_instruction_source() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap();
let mut cfg = make_config(&tmp, /*limit*/ 4096, Some("global doc")).await;
cfg.features.enable(Feature::ChildAgentsMd).unwrap();
let global_agents = cfg.codex_home.join(DEFAULT_AGENTS_MD_FILENAME);
fs::create_dir_all(&cfg.codex_home).unwrap();
fs::write(&global_agents, "global doc").unwrap();
let mut warnings = Vec::new();
let loaded = AgentsMdManager::new(&cfg)
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
.await
.expect("instructions expected");
let project_agents = AbsolutePathBuf::try_from(
dunce::canonicalize(cfg.cwd.join("AGENTS.md")).expect("canonical project doc path"),
)
.expect("absolute project doc path");
let expected = LoadedAgentsMd {
entries: vec![
InstructionEntry {
contents: "global doc".to_string(),
provenance: InstructionProvenance::User(global_agents.clone()),
},
InstructionEntry {
contents: "project doc".to_string(),
provenance: InstructionProvenance::Project(project_agents.clone()),
},
InstructionEntry {
contents: HIERARCHICAL_AGENTS_MESSAGE.to_string(),
provenance: InstructionProvenance::Internal,
},
],
};
assert_eq!(loaded, expected);
assert_eq!(
loaded.sources().collect::<Vec<_>>(),
vec![&global_agents, &project_agents]
);
assert_eq!(
loaded.text(),
format!("global doc{AGENTS_MD_SEPARATOR}project doc\n\n{HIERARCHICAL_AGENTS_MESSAGE}")
);
}
/// AGENTS.override.md is preferred over AGENTS.md when both are present.
+5
View File
@@ -549,6 +549,11 @@ impl CodexThread {
self.codex.thread_config_snapshot().await
}
/// Returns the files that supplied the thread's loaded model instructions.
pub async fn instruction_sources(&self) -> Vec<AbsolutePathBuf> {
self.codex.instruction_sources().await
}
pub async fn config(&self) -> Arc<crate::config::Config> {
self.codex.session.get_config().await
}
+17 -9
View File
@@ -208,10 +208,8 @@ async fn load_config_normalizes_relative_cwd_override() -> std::io::Result<()> {
#[tokio::test]
async fn load_config_loads_global_agents_instructions() -> std::io::Result<()> {
let codex_home = tempdir()?;
std::fs::write(
codex_home.path().join(DEFAULT_AGENTS_MD_FILENAME),
"\n global instructions \n",
)?;
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(),
@@ -221,9 +219,14 @@ async fn load_config_loads_global_agents_instructions() -> std::io::Result<()> {
.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!(
config.user_instructions.as_deref(),
Some("global instructions")
user_instructions.sources().collect::<Vec<_>>(),
vec![&global_agents_path]
);
Ok(())
}
@@ -235,7 +238,7 @@ async fn load_config_prefers_global_agents_override_instructions() -> std::io::R
codex_home.path().join(DEFAULT_AGENTS_MD_FILENAME),
"global instructions",
)?;
let global_agents_override_path = codex_home.path().join(LOCAL_AGENTS_MD_FILENAME);
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(
@@ -245,9 +248,14 @@ async fn load_config_prefers_global_agents_override_instructions() -> std::io::R
)
.await?;
let user_instructions = config
.user_instructions
.as_ref()
.expect("global override instructions expected");
assert_eq!(user_instructions.text(), "local override instructions");
assert_eq!(
config.user_instructions.as_deref(),
Some("local override instructions")
user_instructions.sources().collect::<Vec<_>>(),
vec![&global_agents_override_path]
);
Ok(())
}
+3 -3
View File
@@ -1,4 +1,5 @@
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;
@@ -645,7 +646,7 @@ pub struct Config {
pub show_raw_agent_reasoning: bool,
/// User-provided instructions from AGENTS.md.
pub user_instructions: Option<String>,
pub user_instructions: Option<LoadedAgentsMd>,
/// Base instructions override.
pub base_instructions: Option<String>,
@@ -2605,8 +2606,7 @@ impl Config {
Some(&codex_home),
&mut startup_warnings,
)
.await
.map(|loaded| loaded.contents);
.await;
// Destructure ConfigOverrides fully to ensure all overrides are applied.
let ConfigOverrides {
+2 -1
View File
@@ -29,6 +29,7 @@ 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;
@@ -146,7 +147,7 @@ struct GuardianReviewSessionReuseKey {
permissions: Permissions,
developer_instructions: Option<String>,
base_instructions: Option<String>,
user_instructions: Option<String>,
user_instructions: Option<LoadedAgentsMd>,
compact_prompt: Option<String>,
cwd: AbsolutePathBuf,
mcp_servers: Constrained<HashMap<String, McpServerConfig>>,
+1
View File
@@ -131,6 +131,7 @@ 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;
+18 -6
View File
@@ -507,12 +507,13 @@ impl Codex {
let primary_environment = environment_selections.primary_environment();
let mut user_instruction_warnings = Vec::new();
let user_instructions = AgentsMdManager::new(&config)
.user_instructions(
primary_environment.as_deref(),
&mut user_instruction_warnings,
)
.await;
let user_instructions = if let Some(primary_environment) = primary_environment {
AgentsMdManager::new(&config)
.user_instructions(primary_environment.as_ref(), &mut user_instruction_warnings)
.await
} else {
None
};
config.startup_warnings.extend(user_instruction_warnings);
let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) {
@@ -800,6 +801,17 @@ impl Codex {
state.session_configuration.thread_config_snapshot()
}
pub(crate) async fn instruction_sources(&self) -> Vec<AbsolutePathBuf> {
let state = self.session.state.lock().await;
state
.session_configuration
.user_instructions
.as_ref()
.map_or_else(Vec::new, |instructions| {
instructions.sources().cloned().collect()
})
}
pub(crate) async fn thread_environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
let state = self.session.state.lock().await;
state.session_configuration.environments.clone()
+4 -2
View File
@@ -1,5 +1,6 @@
use super::input_queue::InputQueue;
use super::*;
use crate::agents_md::LoadedAgentsMd;
use crate::config::ConstraintError;
use crate::goals::GoalRuntimeState;
use crate::skills::SkillError;
@@ -54,8 +55,9 @@ pub(crate) struct SessionConfiguration {
/// Developer instructions that supplement the base instructions.
pub(super) developer_instructions: Option<String>,
/// Model instructions that are appended to the base instructions.
pub(super) user_instructions: Option<String>,
/// Model instructions that are appended to the base instructions and the
/// files that supplied them.
pub(super) user_instructions: Option<LoadedAgentsMd>,
/// Personality preference for the model.
pub(super) personality: Option<Personality>,
+5 -1
View File
@@ -1,5 +1,6 @@
use super::*;
use crate::SkillLoadOutcome;
use crate::agents_md::LoadedAgentsMd;
use crate::config::GhostSnapshotConfig;
use crate::environment_selection::ResolvedTurnEnvironments;
use codex_core_skills::HostLoadedSkills;
@@ -551,7 +552,10 @@ impl Session {
app_server_client_name: session_configuration.app_server_client_name.clone(),
developer_instructions: session_configuration.developer_instructions.clone(),
compact_prompt: session_configuration.compact_prompt.clone(),
user_instructions: session_configuration.user_instructions.clone(),
user_instructions: session_configuration
.user_instructions
.as_ref()
.map(LoadedAgentsMd::text),
collaboration_mode: session_configuration.collaboration_mode.clone(),
multi_agent_version,
personality: session_configuration.personality,
@@ -1,4 +1,5 @@
use super::*;
use crate::LoadedAgentsMd;
use crate::ThreadManager;
use crate::config::AgentRoleConfig;
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
@@ -4399,7 +4400,10 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
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("base-user".to_string());
base_config.user_instructions = Some(LoadedAgentsMd::new_user(
"base-user".to_string(),
base_config.codex_home.join("AGENTS.md"),
));
turn.user_instructions = Some("resolved-user".to_string());
turn.config = Arc::new(base_config.clone());
let base_instructions = BaseInstructions {