Download codex-rs/core/src/agents_md.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 20.1 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/src/agents_md.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core/src/agents_md.rs
-
curl -L -o agents_md.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/src/agents_md.rs
20.1 kB
| //! AGENTS.md discovery and user instruction assembly. | |
| //! | |
| //! Project-level documentation is primarily stored in files named `AGENTS.md`. | |
| //! Additional fallback filenames can be configured via `project_doc_fallback_filenames`. | |
| //! Fallback entries containing path syntax for the executor's OS are ignored | |
| //! before any filesystem probes use them. | |
| //! We include the concatenation of all files found along the path from the | |
| //! project root to the current working directory as follows: | |
| //! | |
| //! 1. Determine the project root by walking upwards from the current working | |
| //! directory until a configured `project_root_markers` entry is found. | |
| //! When `project_root_markers` is unset, the default marker list is used | |
| //! (`.git`). If no marker is found, only the current working directory is | |
| //! considered. An empty marker list disables parent traversal. | |
| //! 2. Collect every `AGENTS.md` found from the project root down to the | |
| //! current working directory (inclusive) and concatenate their contents in | |
| //! that order. | |
| //! 3. We do **not** walk past the project root. | |
| use crate::config::Config; | |
| use crate::context::UserInstructions as ContextUserInstructions; | |
| use crate::environment_selection::TurnEnvironmentSnapshot; | |
| use codex_config::ConfigLayerSource; | |
| use codex_config::default_project_root_markers; | |
| use codex_config::merge_toml_values; | |
| use codex_config::project_root_markers_from_config; | |
| use codex_exec_server::ExecutorFileSystem; | |
| use codex_exec_server::GetMetadataOptions; | |
| use codex_exec_server::ReadFileOptions; | |
| use codex_extension_api::Instructions; | |
| use codex_file_system::FileSystemSandboxContext; | |
| use codex_file_system::FindUpErrorPolicy; | |
| use codex_file_system::find_nearest_ancestor_with_markers; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use codex_utils_path_uri::PathConvention; | |
| use codex_utils_path_uri::PathUri; | |
| use futures::StreamExt; | |
| use std::io; | |
| use toml::Value as TomlValue; | |
| use tracing::error; | |
| /// Default filename scanned for AGENTS.md instructions. | |
| 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 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"; | |
| // Metadata probes are cheap and the exec-server transport already bounds total in-flight calls. | |
| // This covers typical project hierarchies in one remote round trip without monopolizing that | |
| // transport when independent startup discovery runs concurrently. | |
| const MAX_CONCURRENT_ANCESTOR_PROBES: usize = 256; | |
| /// Loads project AGENTS.md content and combines it with host-provided user | |
| /// instructions. | |
| pub(crate) async fn load_project_instructions( | |
| config: &Config, | |
| user_instructions: Option<Instructions>, | |
| environments: &TurnEnvironmentSnapshot, | |
| ) -> io::Result<Option<LoadedAgentsMd>> { | |
| let mut loaded = LoadedAgentsMd::from_user_instructions(user_instructions); | |
| if config.active_project.is_untrusted() { | |
| return Ok((!loaded.is_empty()).then_some(loaded)); | |
| } | |
| let mut remaining = config.project_doc_max_bytes; | |
| for turn_environment in environments.turn_environments() { | |
| if remaining == 0 { | |
| break; | |
| } | |
| let filesystem = turn_environment.environment.get_filesystem(); | |
| let sandbox = (!turn_environment | |
| .permission_profile() | |
| .file_system_sandbox_policy() | |
| .has_full_disk_read_access()) | |
| .then(|| turn_environment.sandbox_context(/*additional_permissions*/ None)); | |
| match read_agents_md( | |
| config, | |
| filesystem.as_ref(), | |
| &turn_environment.selection.environment_id, | |
| turn_environment.cwd(), | |
| remaining, | |
| sandbox.as_ref(), | |
| ) | |
| .await | |
| { | |
| Ok(Some(docs)) => { | |
| for entry in docs.entries { | |
| remaining = remaining.saturating_sub(entry.contents.len()); | |
| loaded.entries.push(entry); | |
| } | |
| } | |
| Ok(None) => {} | |
| Err(error) if sandbox.is_none() => { | |
| error!( | |
| environment_id = turn_environment.selection.environment_id, | |
| "error trying to find AGENTS.md docs: {error:#}" | |
| ); | |
| } | |
| Err(error) => { | |
| return Err(io::Error::new( | |
| error.kind(), | |
| format!( | |
| "failed to load AGENTS.md instructions for environment `{}`: {error}", | |
| turn_environment.selection.environment_id | |
| ), | |
| )); | |
| } | |
| } | |
| } | |
| Ok((!loaded.is_empty()).then_some(loaded)) | |
| } | |
| /// Attempt to locate and load AGENTS.md documentation. | |
| /// | |
| /// 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( | |
| config: &Config, | |
| fs: &dyn ExecutorFileSystem, | |
| environment_id: &str, | |
| cwd: &PathUri, | |
| max_total: usize, | |
| sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<Option<LoadedAgentsMd>> { | |
| if max_total == 0 { | |
| return Ok(None); | |
| } | |
| let paths = agents_md_paths(config, cwd, fs, sandbox).await?; | |
| if paths.is_empty() { | |
| return Ok(None); | |
| } | |
| let mut remaining: u64 = max_total as u64; | |
| let mut loaded = LoadedAgentsMd::default(); | |
| for p in paths { | |
| if remaining == 0 { | |
| break; | |
| } | |
| let mut data = match fs.read_file(&p, ReadFileOptions::default(), sandbox).await { | |
| Ok(data) => data, | |
| Err(err) if err.kind() == io::ErrorKind::NotFound => continue, | |
| Err(err) => return Err(err), | |
| }; | |
| let size = data.len() as u64; | |
| if size > remaining { | |
| data.truncate(remaining as usize); | |
| } | |
| if size > remaining { | |
| tracing::warn!( | |
| path = %p, | |
| remaining_bytes = remaining, | |
| "project doc exceeds remaining budget; truncating" | |
| ); | |
| } | |
| let text = String::from_utf8_lossy(&data).to_string(); | |
| if !text.trim().is_empty() { | |
| loaded.entries.push(InstructionEntry { | |
| contents: text, | |
| provenance: InstructionProvenance::Project { | |
| source_path: p, | |
| environment_id: environment_id.to_string(), | |
| cwd: cwd.clone(), | |
| }, | |
| }); | |
| remaining = remaining.saturating_sub(data.len() as u64); | |
| } | |
| } | |
| if loaded.is_empty() { | |
| Ok(None) | |
| } else { | |
| Ok(Some(loaded)) | |
| } | |
| } | |
| /// Discovers AGENTS.md files from the project root to the current working | |
| /// directory, inclusive. Symlinks are allowed. | |
| async fn agents_md_paths( | |
| config: &Config, | |
| cwd: &PathUri, | |
| fs: &dyn ExecutorFileSystem, | |
| sandbox: Option<&FileSystemSandboxContext>, | |
| ) -> io::Result<Vec<PathUri>> { | |
| let dir = cwd.clone(); | |
| let mut merged = TomlValue::Table(toml::map::Map::new()); | |
| for layer in config.config_layer_stack.layers_low_to_high() { | |
| if matches!(layer.name, ConfigLayerSource::Project { .. }) { | |
| continue; | |
| } | |
| merge_toml_values(&mut merged, &layer.config); | |
| } | |
| let project_root_markers = match project_root_markers_from_config(&merged) { | |
| Ok(Some(markers)) => markers, | |
| Ok(None) => default_project_root_markers(), | |
| Err(err) => { | |
| tracing::warn!("invalid project_root_markers: {err}"); | |
| default_project_root_markers() | |
| } | |
| }; | |
| let project_root = find_nearest_ancestor_with_markers( | |
| fs, | |
| &dir, | |
| project_root_markers, | |
| FindUpErrorPolicy::Ignore, | |
| sandbox, | |
| ) | |
| .await?; | |
| let search_dirs = if let Some(root) = project_root { | |
| let mut dirs = Vec::new(); | |
| let mut cursor = dir.clone(); | |
| loop { | |
| dirs.push(cursor.clone()); | |
| if cursor == root { | |
| break; | |
| } | |
| let Some(parent) = cursor.parent() else { | |
| break; | |
| }; | |
| cursor = parent; | |
| } | |
| dirs.reverse(); | |
| dirs | |
| } else { | |
| vec![dir] | |
| }; | |
| let candidate_filenames = candidate_filenames(config, cwd); | |
| let candidate_filenames = &candidate_filenames; | |
| let mut results = futures::stream::iter(search_dirs) | |
| .map(|directory| async move { | |
| for name in candidate_filenames { | |
| let candidate = directory | |
| .join(name) | |
| .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; | |
| match fs | |
| .get_metadata(&candidate, GetMetadataOptions::default(), sandbox) | |
| .await | |
| { | |
| Ok(metadata) if metadata.is_file => return Ok(Some(candidate)), | |
| Ok(_) => {} | |
| Err(err) if err.kind() == io::ErrorKind::NotFound => {} | |
| Err(err) => return Err(err), | |
| } | |
| } | |
| Ok(None) | |
| }) | |
| .buffered(MAX_CONCURRENT_ANCESTOR_PROBES); | |
| let mut found = Vec::new(); | |
| while let Some(result) = results.next().await { | |
| if let Some(candidate) = result? { | |
| found.push(candidate); | |
| } | |
| } | |
| Ok(found) | |
| } | |
| fn candidate_filenames<'a>(config: &'a Config, cwd: &PathUri) -> Vec<&'a str> { | |
| let mut names: Vec<&str> = Vec::with_capacity(2 + config.project_doc_fallback_filenames.len()); | |
| names.push(LOCAL_AGENTS_MD_FILENAME); | |
| names.push(DEFAULT_AGENTS_MD_FILENAME); | |
| for candidate in &config.project_doc_fallback_filenames { | |
| let candidate = candidate.as_str(); | |
| if candidate.is_empty() { | |
| continue; | |
| } | |
| // Use the executor's path convention, not the host's: resolving a Windows | |
| // network path can send ambient credentials even during metadata probes. | |
| if matches!(candidate, "." | "..") | |
| || candidate.contains(['/', '\0']) | |
| || cwd.infer_path_convention() == Some(PathConvention::Windows) | |
| && candidate.contains(['\\', ':']) | |
| { | |
| tracing::warn!("ignoring project_doc_fallback_filenames entry that is not a filename"); | |
| continue; | |
| } | |
| if !names.contains(&candidate) { | |
| names.push(candidate); | |
| } | |
| } | |
| names | |
| } | |
| /// Model-visible instructions loaded from AGENTS.md files and internal | |
| /// guidance. | |
| pub struct LoadedAgentsMd { | |
| /// Account- or home-scoped instructions supplied by the host. | |
| user_instructions: Option<Instructions>, | |
| /// Thread-scoped instructions supplied by the host. | |
| thread_instructions: Option<Instructions>, | |
| /// 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 { | |
| user_instructions: Some(Instructions { | |
| text: contents, | |
| source: Some(path), | |
| }), | |
| thread_instructions: None, | |
| entries: Vec::new(), | |
| } | |
| } | |
| fn from_user_instructions(user_instructions: Option<Instructions>) -> Self { | |
| Self { | |
| user_instructions: user_instructions | |
| .filter(|instructions| !instructions.text.trim().is_empty()), | |
| thread_instructions: None, | |
| entries: Vec::new(), | |
| } | |
| } | |
| pub(crate) fn with_instructions( | |
| mut self, | |
| user_instructions: Option<Instructions>, | |
| thread_instructions: Option<Instructions>, | |
| ) -> Option<Self> { | |
| self.user_instructions = | |
| user_instructions.filter(|instructions| !instructions.text.trim().is_empty()); | |
| self.thread_instructions = | |
| thread_instructions.filter(|instructions| !instructions.text.trim().is_empty()); | |
| (!self.is_empty()).then_some(self) | |
| } | |
| /// 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 { | |
| user_instructions: None, | |
| thread_instructions: None, | |
| entries: vec![InstructionEntry { | |
| contents, | |
| provenance: InstructionProvenance::Internal, | |
| }], | |
| } | |
| } | |
| fn is_empty(&self) -> bool { | |
| self.user_instructions.is_none() | |
| && self.thread_instructions.is_none() | |
| && self | |
| .entries | |
| .iter() | |
| .all(|entry| entry.contents.trim().is_empty()) | |
| } | |
| /// Returns the concatenated model-visible instruction text. | |
| 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 has_previous = false; | |
| let mut previous_was_project = false; | |
| for instructions in self | |
| .user_instructions | |
| .iter() | |
| .chain(self.thread_instructions.iter()) | |
| { | |
| if has_previous { | |
| output.push_str("\n\n"); | |
| } | |
| output.push_str(&instructions.text); | |
| has_previous = true; | |
| } | |
| for entry in &self.entries { | |
| let is_project = matches!(&entry.provenance, InstructionProvenance::Project { .. }); | |
| if has_previous { | |
| // 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 = if is_project && !previous_was_project { | |
| AGENTS_MD_SEPARATOR | |
| } else { | |
| "\n\n" | |
| }; | |
| output.push_str(separator); | |
| } | |
| output.push_str(&entry.contents); | |
| has_previous = true; | |
| previous_was_project = is_project; | |
| } | |
| output | |
| } | |
| fn environment_labeled_text(&self) -> String { | |
| let mut output = String::new(); | |
| let mut has_previous = false; | |
| let mut previous_environment: Option<(&str, &PathUri)> = None; | |
| for instructions in self | |
| .user_instructions | |
| .iter() | |
| .chain(self.thread_instructions.iter()) | |
| { | |
| if has_previous { | |
| output.push_str("\n\n"); | |
| } | |
| 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.inferred_native_path_string() | |
| )); | |
| } | |
| 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 | |
| } | |
| pub(crate) fn contextual_user_fragment(&self) -> ContextUserInstructions { | |
| // 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(PathUri::inferred_native_path_string) | |
| }; | |
| ContextUserInstructions { | |
| directory, | |
| text: self.text(), | |
| } | |
| } | |
| /// Returns the AGENTS.md files that supplied instruction entries. | |
| pub fn sources(&self) -> impl Iterator<Item = PathUri> + '_ { | |
| self.user_instructions | |
| .iter() | |
| .chain(self.thread_instructions.iter()) | |
| .filter_map(|instructions| instructions.source.as_ref().map(PathUri::from_abs_path)) | |
| .chain( | |
| self.entries | |
| .iter() | |
| .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<&PathUri> { | |
| self.entries | |
| .iter() | |
| .find_map(|entry| match &entry.provenance { | |
| InstructionProvenance::Project { cwd, .. } => Some(cwd), | |
| InstructionProvenance::Internal => None, | |
| }) | |
| } | |
| } | |
| /// One model-visible instruction and its provenance. | |
| struct InstructionEntry { | |
| /// Model-visible instruction text. | |
| contents: String, | |
| /// Origin of the instruction. | |
| provenance: InstructionProvenance, | |
| } | |
| enum InstructionProvenance { | |
| /// Workspace instructions discovered from project AGENTS.md files. | |
| Project { | |
| /// Exact AGENTS.md file, distinct from the environment's selected cwd. | |
| source_path: PathUri, | |
| environment_id: String, | |
| cwd: PathUri, | |
| }, | |
| /// Instructions without a file source, including internally defined guidance. | |
| Internal, | |
| } | |
| impl InstructionProvenance { | |
| fn path(&self) -> Option<PathUri> { | |
| match self { | |
| Self::Project { source_path, .. } => Some(source_path.clone()), | |
| Self::Internal => None, | |
| } | |
| } | |
| } | |
| mod tests; | |