mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] Load AGENTS.md from all bound environments (#27696)
## Why We already have the machinery to support multiple environments on a single thread, but we only show the model the contents of `AGENTS.md` files in the primary environment. We should show the model all of the relevant project instructions when we know there's more than one environment. ## Known Gaps As discussed in the RFC, this implementation: 1. doesn't handle environments being added/removed to/from the thread after its creation 2. it doesn't enforce an aggregate context budget across environments, and instead applies the configured project maximum independently to each environment ## Implementation - Discover project instructions in environment order with an independent byte budget per environment and preserve source provenance/order. - Keep the legacy fragment byte-for-byte when exactly one environment contributes project instructions; use environment-labeled sections when two or more environments contribute. - Freeze the complete rendered fragment in `LoadedAgentsMd`, insert it directly into requests, and recognize both layouts in contextual and memory filtering. - Add exact rendering, independent-budget, source-order, creation-snapshot, and consumer coverage without changing app-server schemas.
This commit is contained in:
1 parent
7a19b14229
commit
bf667c7003
15 files changed
+639
-75
No files matched your search
+131
-10
@@ -16,6 +16,9 @@
|
|||||||
//! 3. We do **not** walk past the project root.
|
//! 3. We do **not** walk past the project root.
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::context::ContextualUserFragment;
|
||||||
|
use crate::context::UserInstructions as ContextUserInstructions;
|
||||||
|
use crate::environment_selection::ResolvedTurnEnvironments;
|
||||||
use codex_app_server_protocol::ConfigLayerSource;
|
use codex_app_server_protocol::ConfigLayerSource;
|
||||||
use codex_config::ConfigLayerStackOrdering;
|
use codex_config::ConfigLayerStackOrdering;
|
||||||
use codex_config::default_project_root_markers;
|
use codex_config::default_project_root_markers;
|
||||||
@@ -45,15 +48,26 @@ const AGENTS_MD_SEPARATOR: &str = "\n\n--- project-doc ---\n\n";
|
|||||||
pub(crate) async fn load_project_instructions(
|
pub(crate) async fn load_project_instructions(
|
||||||
config: &mut Config,
|
config: &mut Config,
|
||||||
user_instructions: Option<UserInstructions>,
|
user_instructions: Option<UserInstructions>,
|
||||||
fs: Option<&dyn ExecutorFileSystem>,
|
environments: &ResolvedTurnEnvironments,
|
||||||
) -> Option<LoadedAgentsMd> {
|
) -> Option<LoadedAgentsMd> {
|
||||||
let mut loaded = LoadedAgentsMd::from_user_instructions(user_instructions);
|
let mut loaded = LoadedAgentsMd::from_user_instructions(user_instructions);
|
||||||
if let Some(fs) = fs {
|
for turn_environment in &environments.turn_environments {
|
||||||
match read_agents_md(config, fs).await {
|
let filesystem = turn_environment.environment.get_filesystem();
|
||||||
|
match read_agents_md(
|
||||||
|
config,
|
||||||
|
filesystem.as_ref(),
|
||||||
|
&turn_environment.environment_id,
|
||||||
|
&turn_environment.cwd,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(Some(docs)) => loaded.entries.extend(docs.entries),
|
Ok(Some(docs)) => loaded.entries.extend(docs.entries),
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("error trying to find AGENTS.md docs: {e:#}");
|
error!(
|
||||||
|
environment_id = turn_environment.environment_id,
|
||||||
|
"error trying to find AGENTS.md docs: {e:#}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +91,8 @@ pub(crate) async fn load_project_instructions(
|
|||||||
async fn read_agents_md(
|
async fn read_agents_md(
|
||||||
config: &mut Config,
|
config: &mut Config,
|
||||||
fs: &dyn ExecutorFileSystem,
|
fs: &dyn ExecutorFileSystem,
|
||||||
|
environment_id: &str,
|
||||||
|
cwd: &AbsolutePathBuf,
|
||||||
) -> io::Result<Option<LoadedAgentsMd>> {
|
) -> io::Result<Option<LoadedAgentsMd>> {
|
||||||
let max_total = config.project_doc_max_bytes;
|
let max_total = config.project_doc_max_bytes;
|
||||||
|
|
||||||
@@ -84,7 +100,7 @@ async fn read_agents_md(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let paths = agents_md_paths(config, fs).await?;
|
let paths = agents_md_paths(config, cwd, fs).await?;
|
||||||
if paths.is_empty() {
|
if paths.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -129,7 +145,11 @@ async fn read_agents_md(
|
|||||||
if !text.trim().is_empty() {
|
if !text.trim().is_empty() {
|
||||||
loaded.entries.push(InstructionEntry {
|
loaded.entries.push(InstructionEntry {
|
||||||
contents: text,
|
contents: text,
|
||||||
provenance: InstructionProvenance::Project(p),
|
provenance: InstructionProvenance::Project {
|
||||||
|
source_path: p,
|
||||||
|
environment_id: environment_id.to_string(),
|
||||||
|
cwd: cwd.clone(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
remaining = remaining.saturating_sub(data.len() as u64);
|
remaining = remaining.saturating_sub(data.len() as u64);
|
||||||
}
|
}
|
||||||
@@ -146,9 +166,10 @@ async fn read_agents_md(
|
|||||||
/// directory, inclusive. Symlinks are allowed.
|
/// directory, inclusive. Symlinks are allowed.
|
||||||
async fn agents_md_paths(
|
async fn agents_md_paths(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
cwd: &AbsolutePathBuf,
|
||||||
fs: &dyn ExecutorFileSystem,
|
fs: &dyn ExecutorFileSystem,
|
||||||
) -> io::Result<Vec<AbsolutePathBuf>> {
|
) -> io::Result<Vec<AbsolutePathBuf>> {
|
||||||
let dir = config.cwd.clone();
|
let dir = cwd.clone();
|
||||||
|
|
||||||
let mut merged = TomlValue::Table(toml::map::Map::new());
|
let mut merged = TomlValue::Table(toml::map::Map::new());
|
||||||
for layer in config.config_layer_stack.get_layers(
|
for layer in config.config_layer_stack.get_layers(
|
||||||
@@ -309,6 +330,14 @@ impl LoadedAgentsMd {
|
|||||||
|
|
||||||
/// Returns the concatenated model-visible instruction text.
|
/// Returns the concatenated model-visible instruction text.
|
||||||
pub fn text(&self) -> String {
|
pub fn text(&self) -> String {
|
||||||
|
if self.has_multiple_project_environments() {
|
||||||
|
self.environment_labeled_text()
|
||||||
|
} else {
|
||||||
|
self.legacy_text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn legacy_text(&self) -> String {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
let mut has_previous = false;
|
let mut has_previous = false;
|
||||||
let mut previous_was_project = false;
|
let mut previous_was_project = false;
|
||||||
@@ -317,7 +346,7 @@ impl LoadedAgentsMd {
|
|||||||
has_previous = true;
|
has_previous = true;
|
||||||
}
|
}
|
||||||
for entry in &self.entries {
|
for entry in &self.entries {
|
||||||
let is_project = matches!(&entry.provenance, InstructionProvenance::Project(_));
|
let is_project = matches!(&entry.provenance, InstructionProvenance::Project { .. });
|
||||||
if has_previous {
|
if has_previous {
|
||||||
// The project-doc marker tells the model where workspace-scoped
|
// The project-doc marker tells the model where workspace-scoped
|
||||||
// instructions begin, so it is only needed on the transition
|
// instructions begin, so it is only needed on the transition
|
||||||
@@ -336,6 +365,68 @@ impl LoadedAgentsMd {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn environment_labeled_text(&self) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut has_previous = false;
|
||||||
|
let mut previous_environment: Option<(&str, &AbsolutePathBuf)> = None;
|
||||||
|
if let Some(instructions) = &self.user_instructions {
|
||||||
|
output.push_str(&instructions.text);
|
||||||
|
has_previous = true;
|
||||||
|
}
|
||||||
|
for entry in &self.entries {
|
||||||
|
match &entry.provenance {
|
||||||
|
InstructionProvenance::Project {
|
||||||
|
environment_id,
|
||||||
|
cwd,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if has_previous {
|
||||||
|
output.push_str("\n\n");
|
||||||
|
}
|
||||||
|
// One environment can contribute several hierarchical AGENTS.md files from
|
||||||
|
// its project root through its cwd. Label that environment once for the
|
||||||
|
// complete group rather than repeating the label before every file.
|
||||||
|
let environment = (environment_id.as_str(), cwd);
|
||||||
|
if previous_environment != Some(environment) {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"for `{}` with root {}\n\n",
|
||||||
|
environment_id,
|
||||||
|
cwd.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
output.push_str(&entry.contents);
|
||||||
|
previous_environment = Some(environment);
|
||||||
|
}
|
||||||
|
InstructionProvenance::Internal => {
|
||||||
|
if has_previous {
|
||||||
|
output.push_str("\n\n");
|
||||||
|
}
|
||||||
|
output.push_str(&entry.contents);
|
||||||
|
previous_environment = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
has_previous = true;
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the complete model-visible contextual user fragment.
|
||||||
|
pub(crate) fn render(&self) -> String {
|
||||||
|
// One contributing project environment retains the legacy cwd wrapper. With two or more,
|
||||||
|
// the body labels every contributing environment itself, so the outer cwd is omitted.
|
||||||
|
let directory = if self.has_multiple_project_environments() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.single_project_cwd()
|
||||||
|
.map(|cwd| cwd.to_string_lossy().into_owned())
|
||||||
|
};
|
||||||
|
ContextUserInstructions {
|
||||||
|
directory,
|
||||||
|
text: self.text(),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the host-provided user instructions.
|
/// Returns the host-provided user instructions.
|
||||||
pub(crate) fn user_instructions(&self) -> Option<&UserInstructions> {
|
pub(crate) fn user_instructions(&self) -> Option<&UserInstructions> {
|
||||||
self.user_instructions.as_ref()
|
self.user_instructions.as_ref()
|
||||||
@@ -352,6 +443,31 @@ impl LoadedAgentsMd {
|
|||||||
.filter_map(|entry| entry.provenance.path()),
|
.filter_map(|entry| entry.provenance.path()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn has_multiple_project_environments(&self) -> bool {
|
||||||
|
let mut first_environment_id = None;
|
||||||
|
self.entries.iter().any(|entry| {
|
||||||
|
let InstructionProvenance::Project { environment_id, .. } = &entry.provenance else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match first_environment_id {
|
||||||
|
Some(first_environment_id) => first_environment_id != environment_id,
|
||||||
|
None => {
|
||||||
|
first_environment_id = Some(environment_id);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn single_project_cwd(&self) -> Option<&AbsolutePathBuf> {
|
||||||
|
self.entries
|
||||||
|
.iter()
|
||||||
|
.find_map(|entry| match &entry.provenance {
|
||||||
|
InstructionProvenance::Project { cwd, .. } => Some(cwd),
|
||||||
|
InstructionProvenance::Internal => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One model-visible instruction and its provenance.
|
/// One model-visible instruction and its provenance.
|
||||||
@@ -367,7 +483,12 @@ struct InstructionEntry {
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
enum InstructionProvenance {
|
enum InstructionProvenance {
|
||||||
/// Workspace instructions discovered from project AGENTS.md files.
|
/// Workspace instructions discovered from project AGENTS.md files.
|
||||||
Project(AbsolutePathBuf),
|
Project {
|
||||||
|
/// Exact AGENTS.md file, distinct from the environment's selected cwd.
|
||||||
|
source_path: AbsolutePathBuf,
|
||||||
|
environment_id: String,
|
||||||
|
cwd: AbsolutePathBuf,
|
||||||
|
},
|
||||||
|
|
||||||
/// Instructions without a file source, including internally defined guidance.
|
/// Instructions without a file source, including internally defined guidance.
|
||||||
Internal,
|
Internal,
|
||||||
@@ -376,7 +497,7 @@ enum InstructionProvenance {
|
|||||||
impl InstructionProvenance {
|
impl InstructionProvenance {
|
||||||
fn path(&self) -> Option<&AbsolutePathBuf> {
|
fn path(&self) -> Option<&AbsolutePathBuf> {
|
||||||
match self {
|
match self {
|
||||||
Self::Project(path) => Some(path),
|
Self::Project { source_path, .. } => Some(source_path),
|
||||||
Self::Internal => None,
|
Self::Internal => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::ConfigBuilder;
|
use crate::config::ConfigBuilder;
|
||||||
|
use crate::environment_selection::ResolvedTurnEnvironments;
|
||||||
|
use crate::session::turn_context::TurnEnvironment;
|
||||||
use codex_config::ConfigLayerEntry;
|
use codex_config::ConfigLayerEntry;
|
||||||
use codex_config::ConfigLayerStack;
|
use codex_config::ConfigLayerStack;
|
||||||
use codex_config::ConfigRequirements;
|
use codex_config::ConfigRequirements;
|
||||||
use codex_config::ConfigRequirementsToml;
|
use codex_config::ConfigRequirementsToml;
|
||||||
use codex_exec_server::CopyOptions;
|
use codex_exec_server::CopyOptions;
|
||||||
use codex_exec_server::CreateDirectoryOptions;
|
use codex_exec_server::CreateDirectoryOptions;
|
||||||
|
use codex_exec_server::Environment;
|
||||||
use codex_exec_server::ExecutorFileSystemFuture;
|
use codex_exec_server::ExecutorFileSystemFuture;
|
||||||
use codex_exec_server::FileMetadata;
|
use codex_exec_server::FileMetadata;
|
||||||
use codex_exec_server::FileSystemSandboxContext;
|
use codex_exec_server::FileSystemSandboxContext;
|
||||||
@@ -26,6 +29,7 @@ use std::ops::Deref;
|
|||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -228,10 +232,11 @@ async fn get_user_instructions(config: &TestConfig) -> Option<String> {
|
|||||||
async fn load_agents_md(config: &TestConfig, warnings: &mut Vec<String>) -> Option<LoadedAgentsMd> {
|
async fn load_agents_md(config: &TestConfig, warnings: &mut Vec<String>) -> Option<LoadedAgentsMd> {
|
||||||
let mut core_config = config.config.clone();
|
let mut core_config = config.config.clone();
|
||||||
let existing_warning_count = core_config.startup_warnings.len();
|
let existing_warning_count = core_config.startup_warnings.len();
|
||||||
|
let environments = resolved_local_environments([("local", core_config.cwd.clone())]);
|
||||||
let loaded = load_project_instructions(
|
let loaded = load_project_instructions(
|
||||||
&mut core_config,
|
&mut core_config,
|
||||||
config.user_instructions.clone(),
|
config.user_instructions.clone(),
|
||||||
Some(LOCAL_FS.as_ref()),
|
&environments,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
warnings.extend(
|
warnings.extend(
|
||||||
@@ -244,7 +249,34 @@ async fn load_agents_md(config: &TestConfig, warnings: &mut Vec<String>) -> Opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn agents_md_paths(config: &TestConfig) -> std::io::Result<Vec<AbsolutePathBuf>> {
|
async fn agents_md_paths(config: &TestConfig) -> std::io::Result<Vec<AbsolutePathBuf>> {
|
||||||
super::agents_md_paths(&config.config, LOCAL_FS.as_ref()).await
|
super::agents_md_paths(&config.config, &config.cwd, LOCAL_FS.as_ref()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolved_local_environments<const N: usize>(
|
||||||
|
environments: [(&str, AbsolutePathBuf); N],
|
||||||
|
) -> ResolvedTurnEnvironments {
|
||||||
|
ResolvedTurnEnvironments {
|
||||||
|
turn_environments: environments
|
||||||
|
.into_iter()
|
||||||
|
.map(|(environment_id, cwd)| TurnEnvironment {
|
||||||
|
environment_id: environment_id.to_string(),
|
||||||
|
environment: Arc::new(
|
||||||
|
Environment::create_for_tests(/*exec_server_url*/ None)
|
||||||
|
.expect("local environment"),
|
||||||
|
),
|
||||||
|
cwd,
|
||||||
|
shell: None,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_provenance(path: AbsolutePathBuf, cwd: AbsolutePathBuf) -> InstructionProvenance {
|
||||||
|
InstructionProvenance::Project {
|
||||||
|
source_path: path,
|
||||||
|
environment_id: "local".to_string(),
|
||||||
|
cwd,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_invalid_utf8_warning(warnings: &[String], source: &str, path: &Path) {
|
fn assert_invalid_utf8_warning(warnings: &[String], source: &str, path: &Path) {
|
||||||
@@ -466,11 +498,14 @@ async fn total_byte_limit_truncates_later_project_docs() {
|
|||||||
entries: vec![
|
entries: vec![
|
||||||
InstructionEntry {
|
InstructionEntry {
|
||||||
contents: "root".to_string(),
|
contents: "root".to_string(),
|
||||||
provenance: InstructionProvenance::Project(repo.path().join("AGENTS.md").abs()),
|
provenance: project_provenance(
|
||||||
|
repo.path().join("AGENTS.md").abs(),
|
||||||
|
config.cwd.clone(),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
InstructionEntry {
|
InstructionEntry {
|
||||||
contents: "abc".to_string(),
|
contents: "abc".to_string(),
|
||||||
provenance: InstructionProvenance::Project(config.cwd.join("AGENTS.md")),
|
provenance: project_provenance(config.cwd.join("AGENTS.md"), config.cwd.clone()),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -490,7 +525,8 @@ async fn read_agents_md_propagates_metadata_errors() {
|
|||||||
failure: InjectedFailure::Metadata(io::ErrorKind::PermissionDenied),
|
failure: InjectedFailure::Metadata(io::ErrorKind::PermissionDenied),
|
||||||
};
|
};
|
||||||
|
|
||||||
let err = read_agents_md(&mut config.config, &fs)
|
let cwd = config.cwd.clone();
|
||||||
|
let err = read_agents_md(&mut config.config, &fs, "local", &cwd)
|
||||||
.await
|
.await
|
||||||
.expect_err("metadata error");
|
.expect_err("metadata error");
|
||||||
|
|
||||||
@@ -507,7 +543,8 @@ async fn read_agents_md_propagates_read_errors() {
|
|||||||
failure: InjectedFailure::Read(io::ErrorKind::PermissionDenied),
|
failure: InjectedFailure::Read(io::ErrorKind::PermissionDenied),
|
||||||
};
|
};
|
||||||
|
|
||||||
let err = read_agents_md(&mut config.config, &fs)
|
let cwd = config.cwd.clone();
|
||||||
|
let err = read_agents_md(&mut config.config, &fs, "local", &cwd)
|
||||||
.await
|
.await
|
||||||
.expect_err("read error");
|
.expect_err("read error");
|
||||||
|
|
||||||
@@ -524,7 +561,8 @@ async fn read_agents_md_ignores_files_removed_after_discovery() {
|
|||||||
failure: InjectedFailure::Read(io::ErrorKind::NotFound),
|
failure: InjectedFailure::Read(io::ErrorKind::NotFound),
|
||||||
};
|
};
|
||||||
|
|
||||||
let loaded = read_agents_md(&mut config.config, &fs)
|
let cwd = config.cwd.clone();
|
||||||
|
let loaded = read_agents_md(&mut config.config, &fs, "local", &cwd)
|
||||||
.await
|
.await
|
||||||
.expect("removed file is recoverable");
|
.expect("removed file is recoverable");
|
||||||
|
|
||||||
@@ -591,6 +629,240 @@ async fn merges_existing_instructions_with_agents_md() {
|
|||||||
assert_eq!(res, expected);
|
assert_eq!(res, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multiple_environment_docs_use_labeled_layout_and_preserve_source_order() {
|
||||||
|
let primary = tempfile::tempdir().expect("primary tempdir");
|
||||||
|
let secondary = tempfile::tempdir().expect("secondary tempdir");
|
||||||
|
fs::create_dir(primary.path().join(".git")).unwrap();
|
||||||
|
fs::write(primary.path().join("AGENTS.md"), "primary root doc").unwrap();
|
||||||
|
let primary_nested = primary.path().join("nested");
|
||||||
|
fs::create_dir(&primary_nested).unwrap();
|
||||||
|
fs::write(primary_nested.join("AGENTS.md"), "primary nested doc").unwrap();
|
||||||
|
fs::write(secondary.path().join("AGENTS.md"), "secondary doc").unwrap();
|
||||||
|
let mut config = make_config(&primary, /*limit*/ 4096, Some("global instructions")).await;
|
||||||
|
config.cwd = primary_nested.abs();
|
||||||
|
let environments = resolved_local_environments([
|
||||||
|
("primary", config.cwd.clone()),
|
||||||
|
("secondary", secondary.abs()),
|
||||||
|
]);
|
||||||
|
let user_instructions = config.user_instructions.clone();
|
||||||
|
|
||||||
|
let loaded = load_project_instructions(&mut config.config, user_instructions, &environments)
|
||||||
|
.await
|
||||||
|
.expect("instructions expected");
|
||||||
|
let inner = format!(
|
||||||
|
r#"global instructions
|
||||||
|
|
||||||
|
for `primary` with root {}
|
||||||
|
|
||||||
|
primary root doc
|
||||||
|
|
||||||
|
primary nested doc
|
||||||
|
|
||||||
|
for `secondary` with root {}
|
||||||
|
|
||||||
|
secondary doc"#,
|
||||||
|
primary_nested.display(),
|
||||||
|
secondary.path().display(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(loaded.environment_labeled_text(), inner);
|
||||||
|
assert_eq!(loaded.text(), inner);
|
||||||
|
let expected_fragment = format!(
|
||||||
|
r#"# AGENTS.md instructions
|
||||||
|
|
||||||
|
<INSTRUCTIONS>
|
||||||
|
{inner}
|
||||||
|
</INSTRUCTIONS>"#
|
||||||
|
);
|
||||||
|
assert_eq!(loaded.render(), expected_fragment);
|
||||||
|
assert_eq!(
|
||||||
|
loaded.sources().cloned().collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
config
|
||||||
|
.user_instructions
|
||||||
|
.as_ref()
|
||||||
|
.expect("global instructions")
|
||||||
|
.source
|
||||||
|
.clone(),
|
||||||
|
primary.path().join("AGENTS.md").abs(),
|
||||||
|
primary_nested.join("AGENTS.md").abs(),
|
||||||
|
secondary.path().join("AGENTS.md").abs(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn secondary_only_project_doc_uses_single_contributor_layout() {
|
||||||
|
let primary = tempfile::tempdir().expect("primary tempdir");
|
||||||
|
let secondary = tempfile::tempdir().expect("secondary tempdir");
|
||||||
|
fs::write(secondary.path().join("AGENTS.md"), "secondary doc").unwrap();
|
||||||
|
let mut config = make_config(&primary, /*limit*/ 4096, Some("global instructions")).await;
|
||||||
|
let environments = resolved_local_environments([
|
||||||
|
("primary", config.cwd.clone()),
|
||||||
|
("secondary", secondary.abs()),
|
||||||
|
]);
|
||||||
|
let user_instructions = config.user_instructions.clone();
|
||||||
|
|
||||||
|
let loaded = load_project_instructions(&mut config.config, user_instructions, &environments)
|
||||||
|
.await
|
||||||
|
.expect("instructions expected");
|
||||||
|
let inner = format!("global instructions{AGENTS_MD_SEPARATOR}secondary doc");
|
||||||
|
|
||||||
|
assert_eq!(loaded.legacy_text(), inner);
|
||||||
|
assert_eq!(loaded.text(), inner);
|
||||||
|
let expected_fragment = format!(
|
||||||
|
"# AGENTS.md instructions for {}\n\n<INSTRUCTIONS>\n{inner}\n</INSTRUCTIONS>",
|
||||||
|
secondary.path().display()
|
||||||
|
);
|
||||||
|
assert_eq!(loaded.render(), expected_fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn primary_only_project_doc_preserves_legacy_layout_with_multiple_bound_environments() {
|
||||||
|
let primary = tempfile::tempdir().expect("primary tempdir");
|
||||||
|
let secondary = tempfile::tempdir().expect("secondary tempdir");
|
||||||
|
fs::write(primary.path().join("AGENTS.md"), "primary doc").unwrap();
|
||||||
|
let mut config = make_config(&primary, /*limit*/ 4096, Some("global instructions")).await;
|
||||||
|
let environments = resolved_local_environments([
|
||||||
|
("primary", config.cwd.clone()),
|
||||||
|
("secondary", secondary.abs()),
|
||||||
|
]);
|
||||||
|
let user_instructions = config.user_instructions.clone();
|
||||||
|
|
||||||
|
let loaded = load_project_instructions(&mut config.config, user_instructions, &environments)
|
||||||
|
.await
|
||||||
|
.expect("instructions expected");
|
||||||
|
let inner = format!("global instructions{AGENTS_MD_SEPARATOR}primary doc");
|
||||||
|
|
||||||
|
assert_eq!(loaded.legacy_text(), inner);
|
||||||
|
assert_eq!(loaded.text(), inner);
|
||||||
|
let expected_fragment = format!(
|
||||||
|
"# AGENTS.md instructions for {}\n\n<INSTRUCTIONS>\n{inner}\n</INSTRUCTIONS>",
|
||||||
|
primary.path().display()
|
||||||
|
);
|
||||||
|
assert_eq!(loaded.render(), expected_fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn project_doc_byte_limit_is_applied_independently_per_environment() {
|
||||||
|
let primary = tempfile::tempdir().expect("primary tempdir");
|
||||||
|
let secondary = tempfile::tempdir().expect("secondary tempdir");
|
||||||
|
fs::write(primary.path().join("AGENTS.md"), "ABCDE").unwrap();
|
||||||
|
fs::write(secondary.path().join("AGENTS.md"), "VWXYZ").unwrap();
|
||||||
|
let mut config = make_config(&primary, /*limit*/ 3, /*instructions*/ None).await;
|
||||||
|
let environments = resolved_local_environments([
|
||||||
|
("primary", config.cwd.clone()),
|
||||||
|
("secondary", secondary.abs()),
|
||||||
|
]);
|
||||||
|
let user_instructions = config.user_instructions.clone();
|
||||||
|
|
||||||
|
let loaded = load_project_instructions(&mut config.config, user_instructions, &environments)
|
||||||
|
.await
|
||||||
|
.expect("instructions expected");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
loaded.text(),
|
||||||
|
format!(
|
||||||
|
"for `primary` with root {}\n\nABC\n\nfor `secondary` with root {}\n\nVWX",
|
||||||
|
primary.path().display(),
|
||||||
|
secondary.path().display()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multiple_environments_can_exceed_single_environment_project_doc_limit() {
|
||||||
|
// TODO(anp): Add an aggregate cap across environments instead of allowing the combined
|
||||||
|
// project instructions to grow by one full per-environment budget for every binding.
|
||||||
|
const LIMIT: usize = 8;
|
||||||
|
let primary = tempfile::tempdir().expect("primary tempdir");
|
||||||
|
let secondary = tempfile::tempdir().expect("secondary tempdir");
|
||||||
|
let primary_doc = "P".repeat(LIMIT);
|
||||||
|
let secondary_doc = "S".repeat(LIMIT);
|
||||||
|
fs::write(primary.path().join("AGENTS.md"), &primary_doc).unwrap();
|
||||||
|
fs::write(secondary.path().join("AGENTS.md"), &secondary_doc).unwrap();
|
||||||
|
let mut config = make_config(&primary, LIMIT, /*instructions*/ None).await;
|
||||||
|
let environments = resolved_local_environments([
|
||||||
|
("primary", config.cwd.clone()),
|
||||||
|
("secondary", secondary.abs()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let loaded = load_project_instructions(
|
||||||
|
&mut config.config,
|
||||||
|
/*user_instructions*/ None,
|
||||||
|
&environments,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("instructions expected");
|
||||||
|
let project_bytes = loaded
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| matches!(&entry.provenance, InstructionProvenance::Project { .. }))
|
||||||
|
.map(|entry| entry.contents.len())
|
||||||
|
.sum::<usize>();
|
||||||
|
|
||||||
|
assert_eq!(project_bytes, LIMIT * 2);
|
||||||
|
assert!(project_bytes > config.project_doc_max_bytes);
|
||||||
|
assert!(loaded.text().contains(&primary_doc));
|
||||||
|
assert!(loaded.text().contains(&secondary_doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn secondary_environment_invalid_utf8_warns_without_suppressing_other_docs() {
|
||||||
|
let primary = tempfile::tempdir().expect("primary tempdir");
|
||||||
|
let secondary = tempfile::tempdir().expect("secondary tempdir");
|
||||||
|
fs::write(primary.path().join("AGENTS.md"), "primary doc").unwrap();
|
||||||
|
fs::write(secondary.path().join("AGENTS.md"), b"secondary\xFFdoc").unwrap();
|
||||||
|
let mut config = make_config(&primary, /*limit*/ 4096, /*instructions*/ None).await;
|
||||||
|
let environments = resolved_local_environments([
|
||||||
|
("primary", config.cwd.clone()),
|
||||||
|
("secondary", secondary.abs()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let loaded = load_project_instructions(
|
||||||
|
&mut config.config,
|
||||||
|
/*user_instructions*/ None,
|
||||||
|
&environments,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("instructions expected");
|
||||||
|
|
||||||
|
assert!(loaded.text().contains("primary doc"));
|
||||||
|
assert!(loaded.text().contains("secondary\u{FFFD}doc"));
|
||||||
|
assert_invalid_utf8_warning(
|
||||||
|
&config.startup_warnings,
|
||||||
|
"Project",
|
||||||
|
secondary.path().join("AGENTS.md").as_path(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn child_agents_guidance_is_appended_once_after_environment_groups() {
|
||||||
|
let primary = tempfile::tempdir().expect("primary tempdir");
|
||||||
|
let secondary = tempfile::tempdir().expect("secondary tempdir");
|
||||||
|
fs::write(primary.path().join("AGENTS.md"), "primary doc").unwrap();
|
||||||
|
fs::write(secondary.path().join("AGENTS.md"), "secondary doc").unwrap();
|
||||||
|
let mut config = make_config(&primary, /*limit*/ 4096, /*instructions*/ None).await;
|
||||||
|
config.features.enable(Feature::ChildAgentsMd).unwrap();
|
||||||
|
let environments = resolved_local_environments([
|
||||||
|
("primary", config.cwd.clone()),
|
||||||
|
("secondary", secondary.abs()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let loaded = load_project_instructions(
|
||||||
|
&mut config.config,
|
||||||
|
/*user_instructions*/ None,
|
||||||
|
&environments,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("instructions expected");
|
||||||
|
let text = loaded.text();
|
||||||
|
|
||||||
|
assert_eq!(text.matches(HIERARCHICAL_AGENTS_MESSAGE).count(), 1);
|
||||||
|
assert!(text.ends_with(HIERARCHICAL_AGENTS_MESSAGE));
|
||||||
|
}
|
||||||
|
|
||||||
/// If there are existing system instructions but AGENTS.md docs are
|
/// If there are existing system instructions but AGENTS.md docs are
|
||||||
/// missing we expect the original instructions to be returned unchanged.
|
/// missing we expect the original instructions to be returned unchanged.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -598,7 +870,6 @@ async fn keeps_existing_instructions_when_doc_missing() {
|
|||||||
let tmp = tempfile::tempdir().expect("tempdir");
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
||||||
const INSTRUCTIONS: &str = "some instructions";
|
const INSTRUCTIONS: &str = "some instructions";
|
||||||
|
|
||||||
let res =
|
let res =
|
||||||
get_user_instructions(&make_config(&tmp, /*limit*/ 4096, Some(INSTRUCTIONS)).await).await;
|
get_user_instructions(&make_config(&tmp, /*limit*/ 4096, Some(INSTRUCTIONS)).await).await;
|
||||||
|
|
||||||
@@ -640,11 +911,11 @@ async fn concatenates_root_and_cwd_docs() {
|
|||||||
entries: vec![
|
entries: vec![
|
||||||
InstructionEntry {
|
InstructionEntry {
|
||||||
contents: "root doc".to_string(),
|
contents: "root doc".to_string(),
|
||||||
provenance: InstructionProvenance::Project(root_agents.clone()),
|
provenance: project_provenance(root_agents.clone(), cfg.cwd.clone()),
|
||||||
},
|
},
|
||||||
InstructionEntry {
|
InstructionEntry {
|
||||||
contents: "crate doc".to_string(),
|
contents: "crate doc".to_string(),
|
||||||
provenance: InstructionProvenance::Project(crate_agents.clone()),
|
provenance: project_provenance(crate_agents.clone(), cfg.cwd.clone()),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -804,7 +1075,7 @@ async fn instruction_sources_include_global_before_agents_md_docs() {
|
|||||||
}),
|
}),
|
||||||
entries: vec![InstructionEntry {
|
entries: vec![InstructionEntry {
|
||||||
contents: "project doc".to_string(),
|
contents: "project doc".to_string(),
|
||||||
provenance: InstructionProvenance::Project(project_agents.clone()),
|
provenance: project_provenance(project_agents.clone(), cfg.cwd.clone()),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
assert_eq!(loaded, expected);
|
assert_eq!(loaded, expected);
|
||||||
@@ -844,7 +1115,7 @@ async fn child_agents_message_after_project_docs_is_not_an_instruction_source()
|
|||||||
entries: vec![
|
entries: vec![
|
||||||
InstructionEntry {
|
InstructionEntry {
|
||||||
contents: "project doc".to_string(),
|
contents: "project doc".to_string(),
|
||||||
provenance: InstructionProvenance::Project(project_agents.clone()),
|
provenance: project_provenance(project_agents.clone(), cfg.cwd.clone()),
|
||||||
},
|
},
|
||||||
InstructionEntry {
|
InstructionEntry {
|
||||||
contents: HIERARCHICAL_AGENTS_MESSAGE.to_string(),
|
contents: HIERARCHICAL_AGENTS_MESSAGE.to_string(),
|
||||||
|
|||||||
@@ -17,11 +17,39 @@ fn detects_environment_context_fragment() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detects_agents_instructions_fragment() {
|
fn detects_agents_instructions_fragment() {
|
||||||
|
for text in [
|
||||||
|
"# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>",
|
||||||
|
"# AGENTS.md instructions\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>",
|
||||||
|
] {
|
||||||
assert!(is_contextual_user_fragment(&ContentItem::InputText {
|
assert!(is_contextual_user_fragment(&ContentItem::InputText {
|
||||||
text: "# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
|
text: text.to_string(),
|
||||||
.to_string(),
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renders_agents_instructions_with_legacy_directory_header() {
|
||||||
|
assert_eq!(
|
||||||
|
UserInstructions {
|
||||||
|
directory: Some("/tmp".to_string()),
|
||||||
|
text: "body".to_string(),
|
||||||
|
}
|
||||||
|
.render(),
|
||||||
|
"# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renders_agents_instructions_without_directory_header() {
|
||||||
|
assert_eq!(
|
||||||
|
UserInstructions {
|
||||||
|
directory: None,
|
||||||
|
text: "body".to_string(),
|
||||||
|
}
|
||||||
|
.render(),
|
||||||
|
"# AGENTS.md instructions\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detects_subagent_notification_fragment_case_insensitively() {
|
fn detects_subagent_notification_fragment_case_insensitively() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::ContextualUserFragment;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub(crate) struct UserInstructions {
|
pub(crate) struct UserInstructions {
|
||||||
pub(crate) directory: String,
|
pub(crate) directory: Option<String>,
|
||||||
pub(crate) text: String,
|
pub(crate) text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,10 +16,15 @@ impl ContextualUserFragment for UserInstructions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn type_markers() -> (&'static str, &'static str) {
|
fn type_markers() -> (&'static str, &'static str) {
|
||||||
("# AGENTS.md instructions for ", "</INSTRUCTIONS>")
|
("# AGENTS.md instructions", "</INSTRUCTIONS>")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn body(&self) -> String {
|
fn body(&self) -> String {
|
||||||
format!("{}\n\n<INSTRUCTIONS>\n{}\n", self.directory, self.text)
|
let directory = self
|
||||||
|
.directory
|
||||||
|
.as_ref()
|
||||||
|
.map(|directory| format!(" for {directory}"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
format!("{directory}\n\n<INSTRUCTIONS>\n{}\n", self.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,7 @@ impl ResolvedTurnEnvironments {
|
|||||||
self.turn_environments.first()
|
self.turn_environments.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn primary_environment(&self) -> Option<Arc<codex_exec_server::Environment>> {
|
pub(crate) fn primary_environment(&self) -> Option<Arc<codex_exec_server::Environment>> {
|
||||||
self.primary()
|
self.primary()
|
||||||
.map(|environment| Arc::clone(&environment.environment))
|
.map(|environment| Arc::clone(&environment.environment))
|
||||||
|
|||||||
@@ -294,7 +294,6 @@ use crate::SkillLoadOutcome;
|
|||||||
use crate::SkillMetadata;
|
use crate::SkillMetadata;
|
||||||
use crate::SkillsManager;
|
use crate::SkillsManager;
|
||||||
use crate::agents_md::load_project_instructions;
|
use crate::agents_md::load_project_instructions;
|
||||||
use crate::context::UserInstructions;
|
|
||||||
use crate::exec_policy::ExecPolicyUpdateError;
|
use crate::exec_policy::ExecPolicyUpdateError;
|
||||||
use crate::guardian::GuardianReviewSessionManager;
|
use crate::guardian::GuardianReviewSessionManager;
|
||||||
use crate::mcp::McpManager;
|
use crate::mcp::McpManager;
|
||||||
@@ -527,13 +526,9 @@ impl Codex {
|
|||||||
config
|
config
|
||||||
.startup_warnings
|
.startup_warnings
|
||||||
.extend(user_instruction_provider_warnings);
|
.extend(user_instruction_provider_warnings);
|
||||||
// TODO(anp) assemble instructions from multiple environments
|
|
||||||
let primary_environment = environment_selections.primary_environment();
|
|
||||||
let primary_fs = primary_environment
|
|
||||||
.as_ref()
|
|
||||||
.map(|environment| environment.get_filesystem());
|
|
||||||
let loaded_agents_md =
|
let loaded_agents_md =
|
||||||
load_project_instructions(&mut config, user_instructions, primary_fs.as_deref()).await;
|
load_project_instructions(&mut config, user_instructions, &environment_selections)
|
||||||
|
.await;
|
||||||
|
|
||||||
let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) {
|
let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) {
|
||||||
// Guardian review should rely on the built-in shell safety checks,
|
// Guardian review should rely on the built-in shell safety checks,
|
||||||
@@ -2978,14 +2973,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(user_instructions) = turn_context.user_instructions.as_deref() {
|
if let Some(user_instructions) = turn_context.user_instructions.as_deref() {
|
||||||
contextual_user_sections.push(
|
contextual_user_sections.push(user_instructions.to_string());
|
||||||
UserInstructions {
|
|
||||||
text: user_instructions.to_string(),
|
|
||||||
#[allow(deprecated)]
|
|
||||||
directory: turn_context.cwd.to_string_lossy().into_owned(),
|
|
||||||
}
|
|
||||||
.render(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// This is full-context metadata. Steady-state context diffs should not re-emit it.
|
// This is full-context metadata. Steady-state context diffs should not re-emit it.
|
||||||
if turn_context.features.enabled(Feature::TokenBudget)
|
if turn_context.features.enabled(Feature::TokenBudget)
|
||||||
|
|||||||
@@ -553,7 +553,7 @@ impl Session {
|
|||||||
user_instructions: session_configuration
|
user_instructions: session_configuration
|
||||||
.loaded_agents_md
|
.loaded_agents_md
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(LoadedAgentsMd::text),
|
.map(LoadedAgentsMd::render),
|
||||||
collaboration_mode: session_configuration.collaboration_mode.clone(),
|
collaboration_mode: session_configuration.collaboration_mode.clone(),
|
||||||
multi_agent_version,
|
multi_agent_version,
|
||||||
personality: session_configuration.personality,
|
personality: session_configuration.personality,
|
||||||
|
|||||||
@@ -491,14 +491,17 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
|||||||
);
|
);
|
||||||
let selected_cwd =
|
let selected_cwd =
|
||||||
AbsolutePathBuf::try_from(config.cwd.as_path().join("selected")).expect("absolute path");
|
AbsolutePathBuf::try_from(config.cwd.as_path().join("selected")).expect("absolute path");
|
||||||
|
std::fs::create_dir_all(&selected_cwd).expect("create selected cwd");
|
||||||
let environments = vec![TurnEnvironmentSelection {
|
let environments = vec![TurnEnvironmentSelection {
|
||||||
environment_id: "local".to_string(),
|
environment_id: "local".to_string(),
|
||||||
cwd: selected_cwd.clone(),
|
cwd: selected_cwd.clone(),
|
||||||
}];
|
}];
|
||||||
let default_cwd = config.cwd.clone();
|
let default_cwd = config.cwd.clone();
|
||||||
|
let mut source_config = config.clone();
|
||||||
|
source_config.cwd = selected_cwd.clone();
|
||||||
let source = manager
|
let source = manager
|
||||||
.start_thread_with_options(StartThreadOptions {
|
.start_thread_with_options(StartThreadOptions {
|
||||||
config: config.clone(),
|
config: source_config,
|
||||||
initial_history: InitialHistory::New,
|
initial_history: InitialHistory::New,
|
||||||
session_source: None,
|
session_source: None,
|
||||||
thread_source: None,
|
thread_source: None,
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshot
|
|||||||
}
|
}
|
||||||
if options.strip_agents_md_user_context
|
if options.strip_agents_md_user_context
|
||||||
&& role == "user"
|
&& role == "user"
|
||||||
&& text.starts_with("# AGENTS.md instructions for ")
|
&& text.starts_with("# AGENTS.md instructions")
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -381,7 +381,7 @@ fn canonicalize_snapshot_text(text: &str) -> String {
|
|||||||
if text.starts_with(PLUGINS_INSTRUCTIONS_OPEN_TAG) {
|
if text.starts_with(PLUGINS_INSTRUCTIONS_OPEN_TAG) {
|
||||||
return "<PLUGINS_INSTRUCTIONS>".to_string();
|
return "<PLUGINS_INSTRUCTIONS>".to_string();
|
||||||
}
|
}
|
||||||
if text.starts_with("# AGENTS.md instructions for ") {
|
if text.starts_with("# AGENTS.md instructions") {
|
||||||
return "<AGENTS_MD>".to_string();
|
return "<AGENTS_MD>".to_string();
|
||||||
}
|
}
|
||||||
if text.starts_with("<environment_context>") {
|
if text.starts_with("<environment_context>") {
|
||||||
|
|||||||
@@ -3,16 +3,20 @@ use anyhow::anyhow;
|
|||||||
use codex_core::ForkSnapshot;
|
use codex_core::ForkSnapshot;
|
||||||
use codex_core::StartThreadOptions;
|
use codex_core::StartThreadOptions;
|
||||||
use codex_exec_server::CreateDirectoryOptions;
|
use codex_exec_server::CreateDirectoryOptions;
|
||||||
|
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||||
|
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
|
||||||
use codex_features::Feature;
|
use codex_features::Feature;
|
||||||
use codex_home::CodexHomeUserInstructionsProvider;
|
use codex_home::CodexHomeUserInstructionsProvider;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
use codex_protocol::protocol::InitialHistory;
|
use codex_protocol::protocol::InitialHistory;
|
||||||
use codex_protocol::protocol::Op;
|
use codex_protocol::protocol::Op;
|
||||||
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||||
use codex_protocol::user_input::UserInput;
|
use codex_protocol::user_input::UserInput;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
use core_test_support::PathBufExt;
|
use core_test_support::PathBufExt;
|
||||||
use core_test_support::create_directory_symlink;
|
use core_test_support::create_directory_symlink;
|
||||||
|
use core_test_support::get_remote_test_env;
|
||||||
use core_test_support::load_default_config_for_test;
|
use core_test_support::load_default_config_for_test;
|
||||||
use core_test_support::responses;
|
use core_test_support::responses;
|
||||||
use core_test_support::responses::ev_completed;
|
use core_test_support::responses::ev_completed;
|
||||||
@@ -61,7 +65,7 @@ async fn agents_instructions(mut builder: TestCodexBuilder) -> Result<String> {
|
|||||||
request
|
request
|
||||||
.message_input_texts("user")
|
.message_input_texts("user")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
.find(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
.ok_or_else(|| anyhow::anyhow!("instructions message not found"))
|
.ok_or_else(|| anyhow::anyhow!("instructions message not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +83,7 @@ fn instruction_fragments(request: &responses::ResponsesRequest) -> Vec<String> {
|
|||||||
request
|
request
|
||||||
.message_input_texts("user")
|
.message_input_texts("user")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|text| text.starts_with("# AGENTS.md instructions for "))
|
.filter(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,10 +92,31 @@ fn expected_instruction_fragment(cwd: &AbsolutePathBuf, contents: &str) -> Strin
|
|||||||
format!("# AGENTS.md instructions for {cwd}\n\n<INSTRUCTIONS>\n{contents}\n</INSTRUCTIONS>")
|
format!("# AGENTS.md instructions for {cwd}\n\n<INSTRUCTIONS>\n{contents}\n</INSTRUCTIONS>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn expected_provider_only_instruction_fragment(contents: &str) -> String {
|
||||||
|
format!("# AGENTS.md instructions\n\n<INSTRUCTIONS>\n{contents}\n</INSTRUCTIONS>")
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_single_instruction_fragment(request: &responses::ResponsesRequest, expected: &str) {
|
fn assert_single_instruction_fragment(request: &responses::ResponsesRequest, expected: &str) {
|
||||||
assert_eq!(instruction_fragments(request), vec![expected.to_string()]);
|
assert_eq!(instruction_fragments(request), vec![expected.to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn submit_thread_turn(thread: &Arc<codex_core::CodexThread>, prompt: &str) -> Result<()> {
|
||||||
|
thread
|
||||||
|
.submit(Op::UserInput {
|
||||||
|
items: vec![UserInput::Text {
|
||||||
|
text: prompt.to_string(),
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
}],
|
||||||
|
final_output_json_schema: None,
|
||||||
|
responsesapi_client_metadata: None,
|
||||||
|
additional_context: Default::default(),
|
||||||
|
thread_settings: Default::default(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn request_body_contains(request: &wiremock::Request, text: &str) -> bool {
|
fn request_body_contains(request: &wiremock::Request, text: &str) -> bool {
|
||||||
let is_zstd = request
|
let is_zstd = request
|
||||||
.headers
|
.headers
|
||||||
@@ -315,7 +340,7 @@ async fn symlinked_cwd_uses_logical_parent_for_agents_discovery() -> Result<()>
|
|||||||
.single_request()
|
.single_request()
|
||||||
.message_input_texts("user")
|
.message_input_texts("user")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
.find(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
.expect("instructions message");
|
.expect("instructions message");
|
||||||
assert!(instructions.contains("logical parent doc"));
|
assert!(instructions.contains("logical parent doc"));
|
||||||
assert!(instructions.contains("workspace doc"));
|
assert!(instructions.contains("workspace doc"));
|
||||||
@@ -362,7 +387,7 @@ async fn selected_environment_sources_match_model_visible_instructions() -> Resu
|
|||||||
.single_request()
|
.single_request()
|
||||||
.message_input_texts("user")
|
.message_input_texts("user")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
.find(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
.expect("instructions message");
|
.expect("instructions message");
|
||||||
assert!(instructions.contains("global doc\n\n--- project-doc ---\n\nproject doc"));
|
assert!(instructions.contains("global doc\n\n--- project-doc ---\n\nproject doc"));
|
||||||
|
|
||||||
@@ -561,6 +586,126 @@ async fn fresh_thread_composes_global_before_project_and_reports_sources() -> Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapshot() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
let Some(_remote_env) = get_remote_test_env() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let server = responses::start_mock_server().await;
|
||||||
|
let response_mock = responses::mount_sse_sequence(
|
||||||
|
&server,
|
||||||
|
vec![
|
||||||
|
responses::sse(vec![
|
||||||
|
responses::ev_response_created("multi-env-response-1"),
|
||||||
|
responses::ev_completed("multi-env-response-1"),
|
||||||
|
]),
|
||||||
|
responses::sse(vec![
|
||||||
|
responses::ev_response_created("multi-env-response-2"),
|
||||||
|
responses::ev_completed("multi-env-response-2"),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let home = Arc::new(TempDir::new()?);
|
||||||
|
let global_source =
|
||||||
|
write_global_file(home.as_ref(), GLOBAL_AGENTS_FILENAME, GLOBAL_INSTRUCTIONS)?;
|
||||||
|
let provider = Arc::new(RecordingUserInstructionsProvider::new(Arc::new(
|
||||||
|
CodexHomeUserInstructionsProvider::new(AbsolutePathBuf::try_from(
|
||||||
|
home.path().to_path_buf(),
|
||||||
|
)?),
|
||||||
|
)));
|
||||||
|
let local_root = TempDir::new()?;
|
||||||
|
let local_source = local_root.path().join(GLOBAL_AGENTS_FILENAME);
|
||||||
|
std::fs::write(&local_source, "local project instructions")?;
|
||||||
|
let mut builder = test_codex()
|
||||||
|
.with_home(Arc::clone(&home))
|
||||||
|
.with_user_instructions_provider(provider.clone())
|
||||||
|
.with_workspace_setup(|cwd, fs| async move {
|
||||||
|
fs.write_file(
|
||||||
|
&PathUri::from_path(cwd.join(GLOBAL_AGENTS_FILENAME))?,
|
||||||
|
b"remote project instructions".to_vec(),
|
||||||
|
/*sandbox*/ None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
let test = builder.build_with_remote_and_local_env(&server).await?;
|
||||||
|
let remote_source = test.config.cwd.join(GLOBAL_AGENTS_FILENAME);
|
||||||
|
let thread = test
|
||||||
|
.thread_manager
|
||||||
|
.start_thread_with_options(StartThreadOptions {
|
||||||
|
config: test.config.clone(),
|
||||||
|
initial_history: InitialHistory::New,
|
||||||
|
session_source: None,
|
||||||
|
thread_source: None,
|
||||||
|
dynamic_tools: Vec::new(),
|
||||||
|
metrics_service_name: None,
|
||||||
|
parent_trace: None,
|
||||||
|
environments: vec![
|
||||||
|
TurnEnvironmentSelection {
|
||||||
|
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||||
|
cwd: test.config.cwd.clone(),
|
||||||
|
},
|
||||||
|
TurnEnvironmentSelection {
|
||||||
|
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||||
|
cwd: local_root.path().to_path_buf().try_into()?,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
thread_extension_init: Default::default(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
assert_eq!(provider.load_count(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
thread.thread.instruction_sources().await,
|
||||||
|
vec![
|
||||||
|
global_source.clone(),
|
||||||
|
remote_source.clone(),
|
||||||
|
local_source.clone().try_into()?,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
submit_thread_turn(&thread.thread, "first multi-environment turn").await?;
|
||||||
|
|
||||||
|
write_global_file(
|
||||||
|
home.as_ref(),
|
||||||
|
GLOBAL_AGENTS_OVERRIDE_FILENAME,
|
||||||
|
NEW_GLOBAL_INSTRUCTIONS,
|
||||||
|
)?;
|
||||||
|
test.fs()
|
||||||
|
.write_file(
|
||||||
|
&PathUri::from_path(test.config.cwd.join(GLOBAL_AGENTS_OVERRIDE_FILENAME))?,
|
||||||
|
b"new remote project instructions".to_vec(),
|
||||||
|
/*sandbox*/ None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
std::fs::write(
|
||||||
|
local_root.path().join(GLOBAL_AGENTS_OVERRIDE_FILENAME),
|
||||||
|
"new local project instructions",
|
||||||
|
)?;
|
||||||
|
submit_thread_turn(&thread.thread, "second multi-environment turn").await?;
|
||||||
|
|
||||||
|
let contents = format!(
|
||||||
|
"{GLOBAL_INSTRUCTIONS}\n\nfor `{REMOTE_ENVIRONMENT_ID}` with root {}\n\nremote project instructions\n\nfor `{LOCAL_ENVIRONMENT_ID}` with root {}\n\nlocal project instructions",
|
||||||
|
test.config.cwd.display(),
|
||||||
|
local_root.path().display(),
|
||||||
|
);
|
||||||
|
let expected =
|
||||||
|
format!("# AGENTS.md instructions\n\n<INSTRUCTIONS>\n{contents}\n</INSTRUCTIONS>");
|
||||||
|
let requests = response_mock.requests();
|
||||||
|
assert_eq!(requests.len(), 2);
|
||||||
|
assert_single_instruction_fragment(&requests[0], &expected);
|
||||||
|
assert_single_instruction_fragment(&requests[1], &expected);
|
||||||
|
assert_eq!(provider.load_count(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
thread.thread.instruction_sources().await,
|
||||||
|
vec![global_source, remote_source, local_source.try_into()?]
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn global_loading_warning_surfaces_during_thread_creation() -> Result<()> {
|
async fn global_loading_warning_surfaces_during_thread_creation() -> Result<()> {
|
||||||
// Set up a malformed global instruction file and one model response.
|
// Set up a malformed global instruction file and one model response.
|
||||||
@@ -604,7 +749,7 @@ async fn global_loading_warning_surfaces_during_thread_creation() -> Result<()>
|
|||||||
"expected warning to contain \"invalid UTF-8\"; observed: {warning}"
|
"expected warning to contain \"invalid UTF-8\"; observed: {warning}"
|
||||||
);
|
);
|
||||||
let expected_fragment =
|
let expected_fragment =
|
||||||
expected_instruction_fragment(&test.config.cwd, "global\u{FFFD}instructions");
|
expected_provider_only_instruction_fragment("global\u{FFFD}instructions");
|
||||||
assert_single_instruction_fragment(&response_mock.single_request(), &expected_fragment);
|
assert_single_instruction_fragment(&response_mock.single_request(), &expected_fragment);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -690,8 +835,7 @@ async fn cold_resume_replays_rendered_instructions_but_reports_current_config_so
|
|||||||
Some(initial_input.as_slice()),
|
Some(initial_input.as_slice()),
|
||||||
"cold resume should replay the original structured input prefix"
|
"cold resume should replay the original structured input prefix"
|
||||||
);
|
);
|
||||||
let expected_fragment =
|
let expected_fragment = expected_provider_only_instruction_fragment(OLD_GLOBAL_INSTRUCTIONS);
|
||||||
expected_instruction_fragment(&initial.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
|
||||||
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
||||||
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
||||||
|
|
||||||
@@ -797,8 +941,7 @@ async fn fork_replays_rendered_instructions_from_shared_history() -> Result<()>
|
|||||||
Some(parent_input.as_slice()),
|
Some(parent_input.as_slice()),
|
||||||
"fork should replay the parent's original structured input prefix"
|
"fork should replay the parent's original structured input prefix"
|
||||||
);
|
);
|
||||||
let expected_fragment =
|
let expected_fragment = expected_provider_only_instruction_fragment(OLD_GLOBAL_INSTRUCTIONS);
|
||||||
expected_instruction_fragment(&parent.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
|
||||||
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
||||||
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
||||||
|
|
||||||
@@ -933,8 +1076,7 @@ async fn run_subagent_global_instruction_case(fork_context: bool) -> Result<()>
|
|||||||
.map_err(|_| anyhow!("timed out waiting for the subagent request"))?;
|
.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.
|
// Assert parent and child report and render the parent's creation-time snapshot exactly once.
|
||||||
let expected_fragment =
|
let expected_fragment = expected_provider_only_instruction_fragment(OLD_GLOBAL_INSTRUCTIONS);
|
||||||
expected_instruction_fragment(&test.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
|
||||||
assert_single_instruction_fragment(&seed_request, &expected_fragment);
|
assert_single_instruction_fragment(&seed_request, &expected_fragment);
|
||||||
assert_single_instruction_fragment(&spawn_request, &expected_fragment);
|
assert_single_instruction_fragment(&spawn_request, &expected_fragment);
|
||||||
assert_single_instruction_fragment(&child_request, &expected_fragment);
|
assert_single_instruction_fragment(&child_request, &expected_fragment);
|
||||||
|
|||||||
@@ -502,7 +502,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
|
|||||||
.position(|(role, text)| {
|
.position(|(role, text)| {
|
||||||
role == "user"
|
role == "user"
|
||||||
&& text.contains("be nice")
|
&& text.contains("be nice")
|
||||||
&& (text.starts_with("# AGENTS.md instructions for "))
|
&& text.starts_with("# AGENTS.md instructions")
|
||||||
})
|
})
|
||||||
.expect("user instructions");
|
.expect("user instructions");
|
||||||
let pos_environment = messages
|
let pos_environment = messages
|
||||||
@@ -1284,7 +1284,7 @@ async fn includes_user_instructions_message_in_request() {
|
|||||||
assert!(
|
assert!(
|
||||||
user_context_texts
|
user_context_texts
|
||||||
.iter()
|
.iter()
|
||||||
.any(|text| text.starts_with("# AGENTS.md instructions for ")),
|
.any(|text| text.starts_with("# AGENTS.md instructions")),
|
||||||
"expected AGENTS text in contextual user message, got {user_context_texts:?}"
|
"expected AGENTS text in contextual user message, got {user_context_texts:?}"
|
||||||
);
|
);
|
||||||
let ui_text = user_context_texts
|
let ui_text = user_context_texts
|
||||||
@@ -2370,7 +2370,7 @@ async fn includes_developer_instructions_message_in_request() {
|
|||||||
assert!(
|
assert!(
|
||||||
user_context_texts
|
user_context_texts
|
||||||
.iter()
|
.iter()
|
||||||
.any(|text| text.starts_with("# AGENTS.md instructions for ")),
|
.any(|text| text.starts_with("# AGENTS.md instructions")),
|
||||||
"expected AGENTS text in contextual user message, got {user_context_texts:?}"
|
"expected AGENTS text in contextual user message, got {user_context_texts:?}"
|
||||||
);
|
);
|
||||||
let ui_text = user_context_texts
|
let ui_text = user_context_texts
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ fn instruction_fragments(request: &responses::ResponsesRequest) -> Vec<String> {
|
|||||||
request
|
request
|
||||||
.message_input_texts("user")
|
.message_input_texts("user")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|text| text.starts_with("# AGENTS.md instructions for "))
|
.filter(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,14 +306,13 @@ fn instruction_fragments_in_items(items: &[Value]) -> Vec<String> {
|
|||||||
.filter_map(|item| item.get("content").and_then(Value::as_array))
|
.filter_map(|item| item.get("content").and_then(Value::as_array))
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(|span| span.get("text").and_then(Value::as_str))
|
.filter_map(|span| span.get("text").and_then(Value::as_str))
|
||||||
.filter(|text| text.starts_with("# AGENTS.md instructions for "))
|
.filter(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expected_instruction_fragment(cwd: &AbsolutePathBuf, contents: &str) -> String {
|
fn expected_instruction_fragment(contents: &str) -> String {
|
||||||
let cwd = cwd.as_path().display();
|
format!("# AGENTS.md instructions\n\n<INSTRUCTIONS>\n{contents}\n</INSTRUCTIONS>")
|
||||||
format!("# AGENTS.md instructions for {cwd}\n\n<INSTRUCTIONS>\n{contents}\n</INSTRUCTIONS>")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_single_instruction_fragment(request: &responses::ResponsesRequest, expected: &str) {
|
fn assert_single_instruction_fragment(request: &responses::ResponsesRequest, expected: &str) {
|
||||||
@@ -1193,7 +1192,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
|
|||||||
!item
|
!item
|
||||||
.get("text")
|
.get("text")
|
||||||
.and_then(|text| text.as_str())
|
.and_then(|text| text.as_str())
|
||||||
.is_some_and(|text| text.starts_with("# AGENTS.md instructions for "))
|
.is_some_and(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -4629,8 +4628,7 @@ async fn manual_compaction_keeps_the_creation_time_global_instructions() -> Resu
|
|||||||
// path now contains new text.
|
// path now contains new text.
|
||||||
let requests = response_mock.requests();
|
let requests = response_mock.requests();
|
||||||
assert_eq!(requests.len(), 3);
|
assert_eq!(requests.len(), 3);
|
||||||
let expected_fragment =
|
let expected_fragment = expected_instruction_fragment(OLD_GLOBAL_INSTRUCTIONS);
|
||||||
expected_instruction_fragment(&test.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
|
||||||
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
||||||
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
||||||
assert_single_instruction_fragment(&requests[2], &expected_fragment);
|
assert_single_instruction_fragment(&requests[2], &expected_fragment);
|
||||||
@@ -4702,8 +4700,7 @@ async fn mid_turn_compaction_keeps_the_creation_time_global_instructions() -> Re
|
|||||||
// Assert the initial, compact, and resumed requests all keep the old snapshot and source.
|
// Assert the initial, compact, and resumed requests all keep the old snapshot and source.
|
||||||
let requests = response_mock.requests();
|
let requests = response_mock.requests();
|
||||||
assert_eq!(requests.len(), 3);
|
assert_eq!(requests.len(), 3);
|
||||||
let expected_fragment =
|
let expected_fragment = expected_instruction_fragment(OLD_GLOBAL_INSTRUCTIONS);
|
||||||
expected_instruction_fragment(&test.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
|
||||||
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
assert_single_instruction_fragment(&requests[0], &expected_fragment);
|
||||||
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
assert_single_instruction_fragment(&requests[1], &expected_fragment);
|
||||||
assert_single_instruction_fragment(&requests[2], &expected_fragment);
|
assert_single_instruction_fragment(&requests[2], &expected_fragment);
|
||||||
@@ -4776,7 +4773,7 @@ async fn remote_v2_compaction_keeps_creation_time_instructions_after_same_path_m
|
|||||||
// creation-time item despite the file-backed source now containing new text.
|
// creation-time item despite the file-backed source now containing new text.
|
||||||
let requests = response_mock.requests();
|
let requests = response_mock.requests();
|
||||||
assert_eq!(requests.len(), 3);
|
assert_eq!(requests.len(), 3);
|
||||||
let old_fragment = expected_instruction_fragment(&test.config.cwd, OLD_GLOBAL_INSTRUCTIONS);
|
let old_fragment = expected_instruction_fragment(OLD_GLOBAL_INSTRUCTIONS);
|
||||||
assert_single_instruction_fragment(&requests[0], &old_fragment);
|
assert_single_instruction_fragment(&requests[0], &old_fragment);
|
||||||
assert_single_instruction_fragment(&requests[1], &old_fragment);
|
assert_single_instruction_fragment(&requests[1], &old_fragment);
|
||||||
assert_single_instruction_fragment(&requests[2], &old_fragment);
|
assert_single_instruction_fragment(&requests[2], &old_fragment);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
|
|||||||
let user_messages = request.message_input_texts("user");
|
let user_messages = request.message_input_texts("user");
|
||||||
let instructions = user_messages
|
let instructions = user_messages
|
||||||
.iter()
|
.iter()
|
||||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
.find(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
.expect("instructions message");
|
.expect("instructions message");
|
||||||
assert!(
|
assert!(
|
||||||
instructions.contains("be nice"),
|
instructions.contains("be nice"),
|
||||||
@@ -88,7 +88,7 @@ async fn hierarchical_agents_emits_when_no_project_doc() {
|
|||||||
let user_messages = request.message_input_texts("user");
|
let user_messages = request.message_input_texts("user");
|
||||||
let instructions = user_messages
|
let instructions = user_messages
|
||||||
.iter()
|
.iter()
|
||||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
.find(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
.expect("instructions message");
|
.expect("instructions message");
|
||||||
assert!(
|
assert!(
|
||||||
instructions.contains(HIERARCHICAL_AGENTS_SNIPPET),
|
instructions.contains(HIERARCHICAL_AGENTS_SNIPPET),
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ fn user_instructions_wrapper_count(request: &ResponsesRequest) -> usize {
|
|||||||
request
|
request
|
||||||
.message_input_texts("user")
|
.message_input_texts("user")
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|text| text.starts_with("# AGENTS.md instructions for "))
|
.filter(|text| text.starts_with("# AGENTS.md instructions"))
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ mod job {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
matches_marked_fragment(text, "# AGENTS.md instructions for ", "</INSTRUCTIONS>")
|
matches_marked_fragment(text, "# AGENTS.md instructions", "</INSTRUCTIONS>")
|
||||||
|| matches_marked_fragment(text, "<skill>", "</skill>")
|
|| matches_marked_fragment(text, "<skill>", "</skill>")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +487,10 @@ mod job {
|
|||||||
"# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>",
|
"# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>",
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"# AGENTS.md instructions\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>",
|
||||||
|
true,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
|
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
|
||||||
true,
|
true,
|
||||||
@@ -665,6 +669,10 @@ mod tests {
|
|||||||
"# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
|
"# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
},
|
},
|
||||||
|
ContentItem::InputText {
|
||||||
|
text: "# AGENTS.md instructions\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
ContentItem::InputText {
|
ContentItem::InputText {
|
||||||
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>"
|
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|||||||
Reference in new issue
Block a user