pub(crate) mod subdir;
use anyhow::Context;
use std::path::{Path, PathBuf};
const MAX_STARTUP_INSTRUCTION_BYTES: u64 = 256 * 1024;
const MAX_STARTUP_INSTRUCTION_TOTAL_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstructionSourceKind {
User,
Repository,
Configured,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstructionFile {
pub kind: InstructionSourceKind,
pub path: PathBuf,
pub content: String,
}
fn discover_agents(
user_agents: &Path,
project_root: &Path,
) -> anyhow::Result<Vec<InstructionFile>> {
let mut files = Vec::new();
load_optional(user_agents, InstructionSourceKind::User, &mut files)?;
load_optional(
&project_root.join("AGENTS.md"),
InstructionSourceKind::Repository,
&mut files,
)?;
Ok(files)
}
fn load_optional(
path: &Path,
kind: InstructionSourceKind,
files: &mut Vec<InstructionFile>,
) -> anyhow::Result<()> {
match crate::prompt_file::read_prompt_file(path, MAX_STARTUP_INSTRUCTION_BYTES, false) {
Ok(file) => append_with_budget(files, kind, path.to_path_buf(), file.text, file.bytes),
Err(error)
if error
.downcast_ref::<std::io::Error>()
.is_some_and(|e| e.kind() == std::io::ErrorKind::NotFound) =>
{
Ok(())
}
Err(error) => Err(error)
.with_context(|| format!("failed to read instruction file: {}", path.display())),
}
}
fn append_with_budget(
files: &mut Vec<InstructionFile>,
kind: InstructionSourceKind,
path: PathBuf,
content: String,
bytes: usize,
) -> anyhow::Result<()> {
let current = files.iter().map(|file| file.content.len()).sum::<usize>();
let total = current
.checked_add(bytes)
.ok_or_else(|| anyhow::anyhow!("startup instruction byte count overflow"))?;
if total > MAX_STARTUP_INSTRUCTION_TOTAL_BYTES {
anyhow::bail!(
"startup instruction budget exceeded at {}: current total {current} bytes, source {bytes} bytes, ceiling {MAX_STARTUP_INSTRUCTION_TOTAL_BYTES} bytes",
path.display()
);
}
files.push(InstructionFile {
kind,
path,
content,
});
Ok(())
}
pub fn discover_agents_with_additional_markdown(
user_agents: &Path,
project_root: &Path,
additional_paths: &[PathBuf],
) -> anyhow::Result<Vec<InstructionFile>> {
let mut files = discover_agents(user_agents, project_root)?;
for (index, path) in additional_paths.iter().enumerate() {
let file = load_configured_markdown(index, path)?;
let bytes = file.content.len();
append_with_budget(&mut files, file.kind, file.path, file.content, bytes)?;
}
Ok(files)
}
fn load_configured_markdown(index: usize, path: &Path) -> anyhow::Result<InstructionFile> {
let key = format!("instructions.additional_markdown_paths[{index}]");
if !path.is_absolute() {
anyhow::bail!("{key} error: path must be absolute: {}", path.display());
}
if !path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
{
anyhow::bail!("{key} error: expected .md file: {}", path.display());
}
let content = crate::prompt_file::read_prompt_file(path, MAX_STARTUP_INSTRUCTION_BYTES, false)
.with_context(|| {
format!(
"{key} error: failed to read UTF-8 markdown file: {}",
path.display()
)
})?;
Ok(InstructionFile {
kind: InstructionSourceKind::Configured,
path: path.to_path_buf(),
content: content.text,
})
}