nanocodex-agent 0.3.0

Owned OpenAI agent lifecycle for Nanocodex
Documentation
use std::{
    fs::{self, File},
    io::{self, Read},
    path::{Path, PathBuf},
    sync::Arc,
};

use tracing::warn;

const MAX_PROJECT_INSTRUCTIONS_BYTES: usize = 32 * 1024;
const CANDIDATE_FILENAMES: [&str; 2] = ["AGENTS.override.md", "AGENTS.md"];
const PROJECT_DOC_SEPARATOR: &str = "\n\n--- project-doc ---\n\n";

struct ProjectInstructionsError {
    path: PathBuf,
    source: io::Error,
}

pub(crate) fn load_global_instructions(codex_home: Option<&Path>) -> Option<Arc<str>> {
    let codex_home = codex_home?;
    for filename in CANDIDATE_FILENAMES {
        let path = codex_home.join(filename);
        match fs::metadata(&path) {
            Ok(metadata) if !metadata.is_file() => continue,
            Ok(_) => {}
            Err(source) if source.kind() == io::ErrorKind::NotFound => continue,
            Err(source) => {
                warn!(
                    path = %path.display(),
                    error = %source,
                    "failed to read global AGENTS.md instructions"
                );
                continue;
            }
        }
        let data = match fs::read(&path) {
            Ok(data) => data,
            Err(source) if source.kind() == io::ErrorKind::NotFound => continue,
            Err(source) => {
                warn!(
                    path = %path.display(),
                    error = %source,
                    "failed to read global AGENTS.md instructions"
                );
                continue;
            }
        };
        let text = String::from_utf8_lossy(&data);
        let text = text.trim();
        if !text.is_empty() {
            return Some(Arc::from(text));
        }
    }
    None
}

pub(super) fn load_instructions(
    workspace: &Path,
    global_instructions: Option<&str>,
) -> Option<String> {
    let project_instructions = match load_project_instructions(workspace) {
        Ok(instructions) => instructions,
        Err(error) => {
            warn!(
                path = %error.path.display(),
                error = %error.source,
                "failed to read project AGENTS.md instructions"
            );
            None
        }
    };
    match (global_instructions, project_instructions) {
        (Some(global), Some(project)) => Some(format!("{global}{PROJECT_DOC_SEPARATOR}{project}")),
        (Some(global), None) => Some(global.to_owned()),
        (None, project) => project,
    }
}

fn load_project_instructions(workspace: &Path) -> Result<Option<String>, ProjectInstructionsError> {
    let root = find_project_root(workspace)?;
    let mut directories = workspace
        .ancestors()
        .take_while(|directory| *directory != root)
        .map(Path::to_path_buf)
        .collect::<Vec<_>>();
    directories.push(root);
    directories.reverse();

    let mut remaining = MAX_PROJECT_INSTRUCTIONS_BYTES;
    let mut documents = Vec::new();
    for directory in directories {
        if remaining == 0 {
            break;
        }
        let Some(path) = instruction_file(&directory)? else {
            continue;
        };
        let (data, truncated) =
            read_bounded(&path, remaining).map_err(|source| ProjectInstructionsError {
                path: path.clone(),
                source,
            })?;
        if truncated {
            warn!(
                path = %path.display(),
                remaining_bytes = remaining,
                "project doc exceeds remaining budget; truncating"
            );
        }
        let text = String::from_utf8_lossy(&data).into_owned();
        if !text.trim().is_empty() {
            remaining -= data.len();
            documents.push(text);
        }
    }

    Ok((!documents.is_empty()).then(|| documents.join("\n\n")))
}

fn find_project_root(workspace: &Path) -> Result<PathBuf, ProjectInstructionsError> {
    for directory in workspace.ancestors() {
        let marker = directory.join(".git");
        match fs::metadata(&marker) {
            Ok(_) => return Ok(directory.to_path_buf()),
            Err(source) if source.kind() == io::ErrorKind::NotFound => {}
            Err(source) => {
                return Err(ProjectInstructionsError {
                    path: marker,
                    source,
                });
            }
        }
    }
    Ok(workspace.to_path_buf())
}

fn instruction_file(directory: &Path) -> Result<Option<PathBuf>, ProjectInstructionsError> {
    for filename in CANDIDATE_FILENAMES {
        let path = directory.join(filename);
        match fs::metadata(&path) {
            Ok(metadata) if metadata.is_file() => return Ok(Some(path)),
            Ok(_) => {}
            Err(source) if source.kind() == io::ErrorKind::NotFound => {}
            Err(source) => {
                return Err(ProjectInstructionsError { path, source });
            }
        }
    }
    Ok(None)
}

fn read_bounded(path: &Path, limit: usize) -> io::Result<(Vec<u8>, bool)> {
    let file = File::open(path)?;
    let mut data = Vec::with_capacity(limit.saturating_add(1).min(8 * 1024));
    file.take(limit.saturating_add(1) as u64)
        .read_to_end(&mut data)?;
    let truncated = data.len() > limit;
    data.truncate(limit);
    Ok((data, truncated))
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::*;

    #[test]
    fn missing_global_files_return_no_instructions() {
        let home = tempdir().unwrap();

        assert!(load_global_instructions(Some(home.path())).is_none());
    }

    #[test]
    fn global_override_precedes_default() {
        let home = tempdir().unwrap();
        fs::write(home.path().join("AGENTS.md"), "default").unwrap();
        fs::write(home.path().join("AGENTS.override.md"), " override \n").unwrap();

        assert_eq!(
            load_global_instructions(Some(home.path())).as_deref(),
            Some("override")
        );
    }

    #[test]
    fn empty_global_override_falls_back_to_default() {
        let home = tempdir().unwrap();
        fs::write(home.path().join("AGENTS.override.md"), " \n\t").unwrap();
        fs::write(home.path().join("AGENTS.md"), " default \n").unwrap();

        assert_eq!(
            load_global_instructions(Some(home.path())).as_deref(),
            Some("default")
        );
    }

    #[test]
    fn global_directory_falls_back_to_default() {
        let home = tempdir().unwrap();
        fs::create_dir(home.path().join("AGENTS.override.md")).unwrap();
        fs::write(home.path().join("AGENTS.md"), "default").unwrap();

        assert_eq!(
            load_global_instructions(Some(home.path())).as_deref(),
            Some("default")
        );
    }

    #[cfg(unix)]
    #[test]
    fn unreadable_global_override_falls_back_to_default() {
        use std::os::unix::fs::symlink;

        let home = tempdir().unwrap();
        symlink("AGENTS.override.md", home.path().join("AGENTS.override.md")).unwrap();
        fs::write(home.path().join("AGENTS.md"), "default").unwrap();

        assert_eq!(
            load_global_instructions(Some(home.path())).as_deref(),
            Some("default")
        );
    }

    #[test]
    fn global_instructions_decode_invalid_utf8_lossily() {
        let home = tempdir().unwrap();
        fs::write(home.path().join("AGENTS.md"), b"global\xff doc").unwrap();

        assert_eq!(
            load_global_instructions(Some(home.path())).as_deref(),
            Some("global\u{fffd} doc")
        );
    }

    #[test]
    fn global_instructions_precede_project_hierarchy() {
        let repo = tempdir().unwrap();
        fs::create_dir(repo.path().join(".git")).unwrap();
        fs::write(repo.path().join("AGENTS.md"), "root").unwrap();
        let workspace = repo.path().join("crate/src");
        fs::create_dir_all(&workspace).unwrap();
        fs::write(repo.path().join("crate/AGENTS.md"), "crate").unwrap();
        fs::write(workspace.join("AGENTS.override.md"), "local").unwrap();

        assert_eq!(
            load_instructions(&workspace, Some("global")),
            Some("global\n\n--- project-doc ---\n\nroot\n\ncrate\n\nlocal".to_owned())
        );
    }

    #[test]
    fn empty_project_docs_do_not_consume_the_shared_budget() {
        let repo = tempdir().unwrap();
        fs::create_dir(repo.path().join(".git")).unwrap();
        fs::write(
            repo.path().join("AGENTS.md"),
            " ".repeat(MAX_PROJECT_INSTRUCTIONS_BYTES),
        )
        .unwrap();
        let workspace = repo.path().join("crate");
        fs::create_dir(&workspace).unwrap();
        fs::write(workspace.join("AGENTS.md"), "use the crate instructions").unwrap();

        assert_eq!(
            load_instructions(&workspace, None),
            Some("use the crate instructions".to_owned())
        );
    }

    #[cfg(unix)]
    #[test]
    fn project_read_errors_preserve_global_instructions() {
        use std::os::unix::fs::symlink;

        let repo = tempdir().unwrap();
        fs::create_dir(repo.path().join(".git")).unwrap();
        symlink("AGENTS.md", repo.path().join("AGENTS.md")).unwrap();

        assert_eq!(
            load_instructions(repo.path(), Some("global")),
            Some("global".to_owned())
        );
    }
}