假设你在 demo/backend 目录里打开 Codex,输入:
使用 $test-skill,按照当前仓库规则检查这个模块。
这里用 $test-skill 代表一个被用户明确点名的 skill。
这条任务发给模型之前,Codex 已经做了不少准备。仓库根目录和 backend 目录里的开发规则要合并,skill 的完整说明要展开,此前的消息和工具结果也要进入 history。这里的 history 是一份结构化执行记录。任务跑得足够久,Codex 还会把 history 压成更短的 summary,再接着执行。
Codex 把上下文维护成一份可以重建的运行状态:规则有明确来源,能力按任务展开,执行记录可以压缩,压缩后再恢复当前规则。
第 6 篇就沿着这次任务走一遍。
◆项目规则会从仓库根一路收集到工作目录
示例仓库里有两份规则:
demo/├── AGENTS.md└── backend/├── AGENTS.override.md└── src/
codex-rs/core/src/agents_md.rs 负责寻找它们。
Codex 从当前工作目录向上寻找项目根,默认把 .git 当作项目根标记。找到 demo 后,它再按相反方向逐层读取:
demo/AGENTS.md-> demo/backend/AGENTS.override.md
同一层目录里,AGENTS.override.md 的优先级高于普通 AGENTS.md。因此,根目录可以放整个仓库都要遵守的规则,backend 目录再补充当前模块的约束。
项目文档还有一个总字节预算 project_doc_max_bytes。上层文件先占预算,剩余空间不足时,靠近工作目录的后续文件会被截断。把大段背景材料塞进根目录 AGENTS.md,可能让更具体的模块规则进不了完整上下文。
会话创建时,Session::spawn_internal 调用 load_project_instructions,把沿途发现的规则装进 LoadedAgentsMd,再存入 SessionConfiguration。这一步只发生在会话初始化阶段。会话运行期间改了磁盘上的 AGENTS.md,当前 Session 不会因为一次 compact 自动重新读取。

小黑沿项目目录逐层收集 AGENTS.md
◆项目规则先进入会话,skill 按任务展开
规则收集完成后,Session::build_initial_context 开始组装模型第一次看到的上下文。
把与本文无关的内容省掉,请求里的层次大致是:
base instructionsdeveloper message- 权限和运行方式- 可用 skills / plugins / apps 目录contextual user message- AGENTS.md instructions- cwd、shell 和环境信息real user message- 使用 $test-skill 检查模块
AGENTS.md 被包装成 # AGENTS.md instructions,作为 contextual user message 写入 history。可用 skills 和 plugins 则先以轻量目录进入 developer message。
这个目录只告诉模型“当前有哪些能力”。AvailableSkillsInstructions 渲染 skill 名称、说明和路径等 metadata,不会在会话开始时读取每一份 SKILL.md 全文。
用户点名 $test-skill 后,build_skills_and_plugins 才会识别这次调用。build_skill_injections 找到对应路径,读取完整 SKILL.md,再生成一条 SkillInstructions:
<skill><name>test-skill</name><path>.../SKILL.md</path>完整的 skill 指令</skill>
plugin 也采用按需展开。用户显式选择某个 plugin 后,build_plugin_injections 才补充它提供的 skill 前缀、MCP servers 和 apps,具体任务随后落到对应的 skill、MCP tool 或 app 上。
这套两阶段加载控制了上下文成本。能力目录保持轻量,当前任务需要哪份说明,Codex 再把哪份说明写进这个 turn 的 history。
◆history 把消息和工具执行记在一起
假设 Codex 为了检查模块执行了一条测试命令,这次交互留下的记录大致是:
用户消息-> assistant 发出 tool call-> shell 返回 tool output-> assistant 根据结果继续判断
这些记录连同项目规则和按需展开的能力,都会进入 ContextManager。base instructions 单独传给模型,不存放在这里。
ContextManager 内部保存一组按时间排列的 ResponseItem。用户和助手消息只是其中一部分,function call、tool output、shell call、reasoning item 和 compact 结果也属于这份执行记录。
第 3 篇讲过,工具结果要回灌给下一轮模型。到了这里,回灌的载体已经很明确:tool call 和 tool output 一起进入 history。下一次模型采样时,Codex 才能知道刚才调用了什么、返回了什么,还要不要继续执行。
工具输出太长时,process_item 会按 truncation policy 截断。发送给模型前,for_prompt 还会修补 call 和 output 的配对关系,避免残缺记录进入下一次采样。
每次模型采样前,run_turn 都会取出归一化后的 history:
sess.clone_history().await.for_prompt(&turn_context.model_info.input_modalities)
build_prompt 再把它和 base instructions、本轮可见 tools、输出 schema 放进同一份模型请求。
初始上下文也不会在每个 turn 原样重复。第一次进入真实用户 turn 时,Codex 注入完整上下文,并保存一份 reference_context_item。后续工作目录、权限或模型配置发生变化,只追加对应的 context diff。

小黑把消息和工具执行缝成一条 history
◆compact 改写历史后,当前规则还要装回来
随着工具调用不断增加,history 会逼近模型的上下文窗口。达到 auto compact 阈值后,Codex 会把旧 history 改写成更小的 replacement_history。
以任务执行到一半触发 compact 为例。模型刚刚返回了工具结果,当前 turn 还没有结束,压缩完成后必须接着做。Codex 会保留必要的用户消息和 summary,丢掉旧 history 里可能重复或已经过期的上下文包装。
随后,InitialContextInjection::BeforeLastUserMessage 调用 build_initial_context,把当前 Session 保存的项目规则、能力目录、权限和环境信息插回最后一条真实用户消息之前。
旧 history-> compact-> 用户消息 + summary-> 重新插入当前初始上下文-> 继续本轮任务
compact 发生在 turn 中途时,当前上下文会立即插回去,任务继续执行。若它发生在两个 turn 之间,Codex 会重置上下文基线,等下一条真实用户消息到来时再完整注入。
这里的“当前”指 Session 已经加载的 LoadedAgentsMd 和运行配置。compact 不负责刷新磁盘文件。想让修改后的 AGENTS.md 生效,仍要走新的会话初始化过程。

小黑折叠长 history 并装回当前规则
◆上下文管理决定 Agent 能不能持续按规则工作
回到开头的任务,模型最终看到的执行材料来自四个步骤:
沿目录收集 AGENTS.md-> 按当前任务展开 skill / plugin-> 把消息和工具结果写进 history-> compact 后重建当前上下文
层级规则让 Agent 知道当前目录该遵守什么;按需展开避免所有能力说明一起占满窗口;结构化 history 保留执行现场;compact 为长任务腾出空间,同时把当前规则重新装回去。
长任务里,执行记录可以缩短,项目规则和运行约束仍要从 Session 保存的当前状态重新建立。只让 summary 接管这些约束,任务越长,Agent 越容易偏离用户要求。
下一篇是这个系列的总结。入口、事件、Agent Loop、工具、权限和上下文已经连成一条运行链,我们再从中提炼一组可以迁移到其他 Agent 系统的工程设计。
夜雨聆风