feat: add support for AGENTS.md (formerly CODEX.md)

This commit is contained in:
Michael Bolin
2025-05-10 10:24:57 -07:00
parent 27198bfe11
commit 4aacd80fa6
3 changed files with 129 additions and 1 deletions
+21 -1
View File
@@ -52,6 +52,7 @@ use crate::models::FunctionCallOutputPayload;
use crate::models::ResponseInputItem;
use crate::models::ResponseItem;
use crate::models::ShellToolCallParams;
use crate::project_doc::find_project_doc;
use crate::protocol::AskForApproval;
use crate::protocol::Event;
use crate::protocol::EventMsg;
@@ -83,10 +84,12 @@ impl Codex {
pub async fn spawn(config: Config, ctrl_c: Arc<Notify>) -> CodexResult<(Codex, String)> {
let (tx_sub, rx_sub) = async_channel::bounded(64);
let (tx_event, rx_event) = async_channel::bounded(64);
let instructions = create_full_instructions(&config).await;
let configure_session = Op::ConfigureSession {
provider: config.model_provider.clone(),
model: config.model.clone(),
instructions: config.instructions.clone(),
instructions,
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy.clone(),
disable_response_storage: config.disable_response_storage,
@@ -136,6 +139,23 @@ impl Codex {
}
}
async fn create_full_instructions(config: &Config) -> Option<String> {
match find_project_doc(config).await {
Ok(Some(project_doc)) => {
let original_instructions = config.instructions.clone();
match original_instructions {
Some(instructions) => Some(format!("{instructions}{project_doc}")),
None => Some(project_doc),
}
}
Ok(None) => config.instructions.clone(),
Err(e) => {
error!("error trying to find project doc: {e:#}");
config.instructions.clone()
}
}
}
/// Context for an initialized model agent
///
/// A session has at most 1 running task at a time, and can be interrupted by user input.
+1
View File
@@ -27,6 +27,7 @@ mod model_provider_info;
pub use model_provider_info::ModelProviderInfo;
pub use model_provider_info::WireApi;
mod models;
mod project_doc;
pub mod protocol;
mod rollout;
mod safety;
+107
View File
@@ -0,0 +1,107 @@
//! Project-level documentation discovery.
//!
//! Project-level documentation can be stored in a file named `AGENTS.md`.
//! Currently, we include only the contents of the first file found as follows:
//!
//! 1. Look for the doc file in the current working directory.
//! 2. If not found, walk *upwards* until the Git repository root is reached
//! (detected by the presence of a `.git` directory/file).
//! 3. If/when the Git root is encountered, look for the doc file there. If it
//! exists, the search stops – we do **not** walk past the Git root.
use crate::config::Config;
use std::path::Path;
/// Maximum number of bytes of the documentation that will be embedded. Larger
/// files are *silently truncated* to this size so we never blow the context
/// window.
pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
/// Currently, we only match `AGENTS.md` exactly.
const CANDIDATE_FILENAMES: &[&str] = &["AGENTS.md"];
/// Attempt to locate and load the project documentation.
///
/// On success returns `Ok(Some(contents))`. 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.
pub(crate) async fn find_project_doc(config: &Config) -> std::io::Result<Option<String>> {
// Attempt to load from the working directory first.
if let Some(doc) = load_first_candidate(&config.cwd, CANDIDATE_FILENAMES).await? {
return Ok(Some(doc));
}
// Walk up towards the filesystem root, stopping once we encounter the Git
// repository root. The presence of **either** a `.git` *file* or
// *directory* counts.
let mut dir = config.cwd.clone();
// Canonicalize the path so that we do not end up in an infinite loop when
// `cwd` contains `..` components.
if let Ok(canon) = dir.canonicalize() {
dir = canon;
}
while let Some(parent) = dir.parent() {
// `.git` can be a *file* (for worktrees or submodules) or a *dir*.
let git_marker = dir.join(".git");
let git_exists = match tokio::fs::metadata(&git_marker).await {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
Err(e) => return Err(e),
};
if git_exists {
// We are at the repo root – attempt one final load.
if let Some(doc) = load_first_candidate(&dir, CANDIDATE_FILENAMES).await? {
return Ok(Some(doc));
}
break;
}
dir = parent.to_path_buf();
}
Ok(None)
}
/// Attempt to load the first candidate file found in `dir`. Returns the file
/// contents (truncated) when successful.
async fn load_first_candidate(dir: &Path, names: &[&str]) -> std::io::Result<Option<String>> {
use tokio::io::AsyncReadExt;
for name in names {
let candidate = dir.join(name);
let file = match tokio::fs::File::open(&candidate).await {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(e),
Ok(f) => f,
};
let size = file.metadata().await?.len();
let reader = tokio::io::BufReader::new(file);
let mut data = Vec::with_capacity(std::cmp::min(size as usize, PROJECT_DOC_MAX_BYTES));
let mut limited = reader.take(PROJECT_DOC_MAX_BYTES as u64);
limited.read_to_end(&mut data).await?;
if size as usize > PROJECT_DOC_MAX_BYTES {
tracing::warn!(
"Project doc `{}` exceeds {PROJECT_DOC_MAX_BYTES} bytes - truncating.",
candidate.display(),
);
}
let contents = String::from_utf8_lossy(&data).to_string();
if contents.trim().is_empty() {
// Empty file – treat as not found.
continue;
}
return Ok(Some(contents));
}
Ok(None)
}