Skip to content

AGENTS文件发现与作用域

追踪 AGENTS.md 的根目录定位、候选文件选择、层级拼接、字节预算和多环境作用域。

基于rust-v0.150.0
CodexRustContext

AGENTS文件发现与作用域 ​

AGENTS.md 不是启动时随便读取的一份全局文本。Codex 先根据项目根标记确定搜索上界,再从根目录到当前工作目录逐层选择文件;同一目录的 AGENTS.override.md、AGENTS.md 和 fallback 文件只保留一个,多个目录的内容按路径顺序拼接。本文追踪这条真实源码链,并解释文件不存在、目录同名、非 UTF-8、预算耗尽和多 environment 的结果。

阅读前建议先看 指令优先级与拼装顺序 和 Context变更语义。本文不讨论文件变化后的刷新时机,也不展开 project_root_markers 的配置解析语法;重点是一次 step 捕获时,哪些目录有资格贡献文本、谁拥有 provenance,以及文本如何成为模型可见的 contextual user fragment。

1. 入口与边界 ​

一次 turn 的 StepContext 需要一个与当前 environment snapshot 对齐的 LoadedAgentsMd。Session::capture_step_context_with_required_mcp_servers 先刷新 manager,再把缓存结果放进 step;因此 AGENTS 文件发现不是 WorldState 自己访问文件系统,而是 step 捕获前完成的输入准备。

相关源码:

  • codex-rs/core/src/session/mod.rs :: capture_step_context_with_required_mcp_servers
  • codex-rs/core/src/session/step_context.rs :: StepContext
rust
let environments = turn_context.environments.refresh_readiness();
self.services
    .agents_md_manager
    .refresh(&turn_context.config, &environments)
    .await;
let loaded_agents_md = self.services.agents_md_manager.get_loaded().await;

let step_context = StepContext {
    turn: Arc::clone(&turn_context),
    environments,
    // ... mcp, tool_router and capability snapshots
    loaded_agents_md,
};

StepContext.loaded_agents_md 是本 step 的快照,不是对文件的懒引用。后续 build_world_state_for_step 只读取它并创建 AgentsMdState;如果本 step 没有 loaded value,WorldState 会得到空的 AGENTS section。这个边界保证模型请求使用的 cwd、environment 和指令来自同一批捕获状态。

2. 根目录定位 ​

源码位置:codex-rs/core/src/agents_md.rs :: agents_md_paths。

rust
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::Propagate,
    /*sandbox*/ None,
)
.await?;

项目层 config 不直接改变 root marker:源码先合并非 project config layers,明确跳过 ConfigLayerSource::Project。默认 marker 是 .git;自定义 marker 找到最近祖先后,搜索不会继续越过它。找不到 marker 时只检查 cwd;空 marker 列表则禁用 parent traversal,同样只检查 cwd。

3. 目录候选 ​

找到 root 后,代码先把 cwd 到 root 的目录链反转为 root→cwd,再为每个目录按候选文件名顺序探测。候选顺序固定以 AGENTS.override.md、AGENTS.md 开始,之后追加配置中的 fallback,并删除重复和空字符串。

源码位置:codex-rs/core/src/agents_md.rs :: candidate_filenames、agents_md_paths。

rust
fn candidate_filenames(config: &Config) -> Vec<&str> {
    let mut names = 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 {
        if candidate.is_empty() {
            continue;
        }
        if !names.contains(&candidate.as_str()) {
            names.push(candidate);
        }
    }
    names
}

每个目录的探测一旦找到一个 regular file 就返回,后续候选不会再读。因此 AGENTS.override.md 是同目录选择优先级,不是把 AGENTS.md 内容覆盖后再追加;如果 override 是目录,metadata 的 is_file 为 false,代码会继续尝试 AGENTS.md。符号链接 cwd 会保留其选定路径,测试证明不会先把 cwd canonicalize 成目标目录。

目录 metadata 探测是并发的,最多 MAX_CONCURRENT_ANCESTOR_PROBES = 256 个 probe;但结果通过 buffered stream 按输入目录顺序收集,所以并发不会改变 root→cwd 的拼接顺序。metadata 的权限错误会向上传播,文件在发现后被删除则 read 阶段把 NotFound 当作跳过。

4. 内容预算 ​

源码位置:codex-rs/core/src/agents_md.rs :: read_agents_md。

rust
let max_total = config.project_doc_max_bytes;
if max_total == 0 {
    return Ok(None);
}

let mut remaining = max_total as u64;
for path in paths {
    if remaining == 0 {
        break;
    }
    let mut data = match fs.read_file(&path, /*sandbox*/ None).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);
    }
    let text = String::from_utf8_lossy(&data).to_string();
    if !text.trim().is_empty() {
        loaded.entries.push(InstructionEntry { contents: text, provenance });
        remaining = remaining.saturating_sub(data.len() as u64);
    }
}

单个 environment 内,预算从 root 文档开始消耗;根文件过大时后面的子目录只能得到剩余字节。截断发生在 byte buffer 上,随后使用 from_utf8_lossy,所以截断可能落在 UTF-8 字符中间并产生 �。空白文件不进入 entries,也不会消耗预算;预算为 0 则整个 environment 返回 None。

load_project_instructions 在遍历 turn environment 之前创建一次 remaining,每成功加入一个 entry 都扣减 其字节数。因此预算是所有 ready environment 共享的全局上限,且按 environment selection 顺序消费;前面的 environment 可以耗尽额度,使后面的 environment 不再读取项目文档。

图中的预算按 selection 顺序连续扣减。测试 project_doc_byte_limit_is_shared_across_environments 明确断言 前一个 environment 的大文档可以阻止后一个文档进入结果。

5. Provenance与文本 ​

LoadedAgentsMd 不只保存拼接后的字符串,还保存每个 entry 的来源路径、environment ID 和 cwd。单 environment 使用 legacy layout;多个 project environment 时,文本会在每个 environment 组前加入 for <id> with root <cwd> 标签,避免模型把两个工作区的同名规则混为一谈。

源码位置:codex-rs/core/src/agents_md.rs :: LoadedAgentsMd::text、legacy_text、environment_labeled_text。

rust
pub struct LoadedAgentsMd {
    user_instructions: Option<UserInstructions>,
    entries: Vec<InstructionEntry>,
}

fn legacy_text(&self) -> String {
    let mut output = String::new();
    if let Some(instructions) = &self.user_instructions {
        output.push_str(&instructions.text);
    }
    for entry in &self.entries {
        if !output.is_empty() {
            output.push_str("\n\n");
        }
        output.push_str(&entry.contents);
    }
    output
}

当已有 host/user instructions 后首次进入 project entry,实际实现插入 --- project-doc --- 分隔符;连续的同一类 project entries 只用普通空行连接。最终 contextual_user_fragment 再把文本包装成 # AGENTS.md instructions ... 与 <INSTRUCTIONS> 标记,随后由 指令优先级与拼装顺序 介绍的 WorldState section 负责初始注入或 replacement/removal diff。

6. 失败与恢复 ​

发现阶段的 NotFound 是正常分支:候选缺失、文件在 metadata 后被删除、同名路径是目录,都会继续尝试或跳过。权限错误和其他 read/metadata 错误则由 read_agents_md 返回,load_project_instructions 在 session 层记录 error 并继续处理其他 environment;这意味着一个 environment 失败不会自动抹掉已经加载的另一个 environment。

加载成功后,AgentsMdManager 以 environment selections 作为缓存键。selection 不变时不重复发现;selection 变化时重新加载并替换 Arc<LoadedAgentsMd>。本文不把这解释成文件热更新:文件内容本身改变但 selection 不变的可见性属于 AGENTS缓存与更新边界 的 manager 刷新专题。

7. 发现测试 ​

concatenates_root_and_cwd_docs 创建 Git 根和嵌套 cwd,各放一个 AGENTS.md,断言 sources 顺序为 root、cwd,文本为 root doc 后接 crate doc;它证明的是层级拼接,不证明“子目录覆盖父目录”。

agents_local_md_preferred 同目录同时创建 override 和普通文件,断言只返回 local;uses_configured_fallback_when_agents_missing 则只提供 fallback 文件,证明 fallback 是候选链的最后一环。override_directory_falls_back_to_agents_md_file 证明 override 目录不算可读文档。

total_byte_limit_truncates_later_project_docs 用 7 字节预算验证 root 消耗 4 字节后 nested 文档只保留 3 字节; project_doc_byte_limit_is_shared_across_environments 验证多个 environment 共享上限。 project_doc_invalid_utf8_uses_lossy_text 证明非法字节转换为 replacement character,而不会让其他 environment 失败。

这些测试证明候选选择、路径顺序、预算和容错边界;它们不证明文件系统 watcher 的实时通知、模型对规则冲突的服从程度,也不证明跨进程 remote filesystem 的延迟上界。

8. 复现路径 ​

读者可以用以下搜索复述主线:

bash
rg -n "agents_md_manager\.refresh|loaded_agents_md" \
  codex-rs/core/src/session/mod.rs
rg -n "fn agents_md_paths|fn read_agents_md|fn candidate_filenames" \
  codex-rs/core/src/agents_md.rs
rg -n "agents_local_md_preferred|total_byte_limit|project_doc_byte_limit" \
  codex-rs/core/src/agents_md_tests.rs

给定一个“子目录规则没有生效”的现象,先检查 cwd 是否位于项目 root marker 之下,再检查该目录是否同时存在 AGENTS.override.md 或同名目录,最后检查父级文件是否已经耗尽本 environment 的 byte budget。不要先把问题归因于模型忽略指令:文件可能根本没有进入 LoadedAgentsMd。