Add AGENTS.md loader crate

This commit is contained in:
Adam Perry
2026-06-04 23:18:54 -07:00
parent d89c654d22
commit d642618398
20 changed files with 146 additions and 89 deletions
+14 -2
View File
@@ -1832,6 +1832,15 @@ dependencies = [
"sha2 0.10.9",
]
[[package]]
name = "codex-agents-md"
version = "0.0.0"
dependencies = [
"codex-core",
"codex-exec-server",
"codex-protocol",
]
[[package]]
name = "codex-analytics"
version = "0.0.0"
@@ -1907,6 +1916,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"clap",
"codex-agents-md",
"codex-analytics",
"codex-app-server-protocol",
"codex-app-server-transport",
@@ -1916,7 +1926,6 @@ dependencies = [
"codex-cloud-config",
"codex-config",
"codex-core",
"codex-core-api",
"codex-core-plugins",
"codex-exec-server",
"codex-extension-api",
@@ -2250,6 +2259,7 @@ dependencies = [
"assert_matches",
"clap",
"clap_complete",
"codex-agents-md",
"codex-api",
"codex-app-server",
"codex-app-server-daemon",
@@ -2260,7 +2270,6 @@ dependencies = [
"codex-cloud-tasks",
"codex-config",
"codex-core",
"codex-core-api",
"codex-core-plugins",
"codex-exec",
"codex-exec-server",
@@ -2642,6 +2651,7 @@ dependencies = [
name = "codex-core-api"
version = "0.0.0"
dependencies = [
"codex-agents-md",
"codex-analytics",
"codex-app-server-protocol",
"codex-arg0",
@@ -3198,6 +3208,7 @@ name = "codex-mcp-server"
version = "0.0.0"
dependencies = [
"anyhow",
"codex-agents-md",
"codex-arg0",
"codex-config",
"codex-core",
@@ -3263,6 +3274,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"chrono",
"codex-agents-md",
"codex-backend-client",
"codex-config",
"codex-core",
+2
View File
@@ -4,6 +4,7 @@ members = [
"analytics",
"agent-graph-store",
"agent-identity",
"agents-md",
"backend-client",
"bwrap",
"ansi-escape",
@@ -134,6 +135,7 @@ app_test_support = { path = "app-server/tests/common" }
codex-analytics = { path = "analytics" }
codex-agent-graph-store = { path = "agent-graph-store" }
codex-agent-identity = { path = "agent-identity" }
codex-agents-md = { path = "agents-md" }
codex-ansi-escape = { path = "ansi-escape" }
codex-api = { path = "codex-api" }
codex-aws-auth = { path = "aws-auth" }
+6
View File
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "agents-md",
crate_name = "codex_agents_md",
)
+19
View File
@@ -0,0 +1,19 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-agents-md"
version.workspace = true
[lib]
doctest = false
name = "codex_agents_md"
path = "src/lib.rs"
test = false
[lints]
workspace = true
[dependencies]
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-protocol = { workspace = true }
+46
View File
@@ -0,0 +1,46 @@
//! Loads AGENTS.md instructions for callers constructing Codex threads.
#![deny(private_bounds, private_interfaces, unreachable_pub)]
use codex_core::AgentsMdManager;
use codex_core::config::Config;
use codex_exec_server::EnvironmentManager;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::protocol::TurnEnvironmentSelection;
/// Preloads AGENTS.md instructions for thread construction from the primary
/// selected environment.
///
/// Call this after the selected environments have been materialized and before
/// passing `config` to a thread start, resume, or fork operation. When no
/// environment is selected, this clears [`Config::user_instructions`].
///
/// This helper assumes user-level AGENTS.md instructions were loaded from the
/// configured `CODEX_HOME`. Callers that source user-level instructions from
/// another location should assemble [`Config::user_instructions`] themselves.
pub async fn load_thread_user_instructions(
config: &mut Config,
environment_manager: &EnvironmentManager,
environments: &[TurnEnvironmentSelection],
) -> CodexResult<()> {
let Some(primary_selection) = environments.first() else {
config.user_instructions = None;
return Ok(());
};
let environment = environment_manager
.get_environment(&primary_selection.environment_id)
.ok_or_else(|| {
CodexErr::InvalidRequest(format!(
"unknown turn environment id `{}`",
primary_selection.environment_id
))
})?;
let mut warnings = Vec::new();
let user_instructions = AgentsMdManager::new(config)
.load_user_instructions(environment.as_ref(), &mut warnings)
.await;
config.startup_warnings.extend(warnings);
config.user_instructions = user_instructions;
Ok(())
}
+1 -1
View File
@@ -31,11 +31,11 @@ axum = { workspace = true, default-features = false, features = [
"ws",
] }
codex-analytics = { workspace = true }
codex-agents-md = { workspace = true }
codex-arg0 = { workspace = true }
codex-cloud-config = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-core-api = { workspace = true }
codex-core-plugins = { workspace = true }
codex-exec-server = { workspace = true }
codex-extension-api = { workspace = true }
@@ -11,6 +11,7 @@ use crate::error_code::internal_error;
use crate::error_code::invalid_params;
use crate::outgoing_message::ConnectionRequestId;
use crate::outgoing_message::OutgoingMessageSender;
use codex_agents_md::load_thread_user_instructions;
use codex_app_server_protocol::CommandMigration;
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
use codex_app_server_protocol::ExternalAgentConfigDetectResponse;
@@ -29,7 +30,6 @@ use codex_arg0::Arg0DispatchPaths;
use codex_core::StartThreadOptions;
use codex_core::ThreadManager;
use codex_core::config::ConfigOverrides;
use codex_core_api::load_thread_user_instructions;
use codex_external_agent_sessions::ExternalAgentSessionMigration as CoreSessionMigration;
use codex_external_agent_sessions::ImportedExternalAgentSession;
use codex_external_agent_sessions::PendingSessionImport;
@@ -1,6 +1,6 @@
use super::*;
use crate::error_code::method_not_found;
use codex_core_api::load_thread_user_instructions;
use codex_agents_md::load_thread_user_instructions;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
@@ -1,5 +1,5 @@
use super::*;
use codex_core_api::load_thread_user_instructions;
use codex_agents_md::load_thread_user_instructions;
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
+1 -1
View File
@@ -25,6 +25,7 @@ codex-app-server = { workspace = true }
codex-app-server-daemon = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-app-server-test-client = { workspace = true }
codex-agents-md = { workspace = true }
codex-arg0 = { workspace = true }
codex-api = { workspace = true }
codex-chatgpt = { workspace = true }
@@ -32,7 +33,6 @@ codex-cloud-tasks = { path = "../cloud-tasks" }
codex-utils-cli = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-core-api = { workspace = true }
codex-core-plugins = { workspace = true }
codex-exec = { workspace = true }
codex-exec-server = { workspace = true }
+6 -2
View File
@@ -1871,8 +1871,12 @@ async fn run_debug_prompt_input_command(
)
.into_iter()
.collect::<Vec<_>>();
codex_core_api::load_thread_user_instructions(&mut config, &environment_manager, &environments)
.await?;
codex_agents_md::load_thread_user_instructions(
&mut config,
&environment_manager,
&environments,
)
.await?;
let mut input = shared
.images
+1
View File
@@ -15,6 +15,7 @@ workspace = true
[dependencies]
codex-app-server-protocol = { workspace = true }
codex-agents-md = { workspace = true }
codex-arg0 = { workspace = true }
codex-analytics = { workspace = true }
codex-config = { workspace = true }
+1 -36
View File
@@ -2,6 +2,7 @@
#![deny(private_bounds, private_interfaces, unreachable_pub)]
pub use codex_agents_md::load_thread_user_instructions;
pub use codex_analytics::AnalyticsEventsClient;
pub use codex_app_server_protocol::ServerNotification;
pub use codex_app_server_protocol::item_event_to_server_notification;
@@ -77,40 +78,4 @@ pub use codex_protocol::protocol::SessionSource;
pub use codex_protocol::protocol::TurnEnvironmentSelection;
pub use codex_protocol::protocol::W3cTraceContext;
pub use codex_protocol::user_input::UserInput;
/// Preloads AGENTS.md instructions for thread construction from the primary
/// selected environment.
///
/// Call this after the selected environments have been materialized and before
/// passing `config` to a thread start, resume, or fork operation. When no
/// environment is selected, this clears [`Config::user_instructions`].
///
/// This helper assumes user-level AGENTS.md instructions were loaded from the
/// configured `CODEX_HOME`. Callers that source user-level instructions from
/// another location should assemble [`Config::user_instructions`] themselves.
pub async fn load_thread_user_instructions(
config: &mut Config,
environment_manager: &EnvironmentManager,
environments: &[TurnEnvironmentSelection],
) -> CodexResult<()> {
let Some(primary_selection) = environments.first() else {
config.user_instructions = None;
return Ok(());
};
let environment = environment_manager
.get_environment(&primary_selection.environment_id)
.ok_or_else(|| {
codex_protocol::error::CodexErr::InvalidRequest(format!(
"unknown turn environment id `{}`",
primary_selection.environment_id
))
})?;
let mut warnings = Vec::new();
let user_instructions = codex_core::AgentsMdManager::new(config)
.load_user_instructions(environment.as_ref(), &mut warnings)
.await;
config.startup_warnings.extend(warnings);
config.user_instructions = user_instructions;
Ok(())
}
pub use codex_utils_absolute_path::AbsolutePathBuf;
+2
View File
@@ -71,6 +71,8 @@ async fn apply_role_to_config_inner(
}
let preserve_current_provider = role_layer_toml.get("model_provider").is_none();
let preserve_current_service_tier = role_layer_toml.get("service_tier").is_none();
// Role reload rebuilds Config from config layers, but thread instructions
// are a caller-provided snapshot and must not be reloaded from disk here.
let user_instructions = config.user_instructions.clone();
let mut next_config = reload::build_next_config(
+19 -13
View File
@@ -106,7 +106,7 @@ impl<'a> AgentsMdManager<'a> {
.config
.user_instructions
.as_ref()
.map(LoadedAgentsMd::user_instructions)
.map(LoadedAgentsMd::user_scoped_instructions)
.unwrap_or_default();
match agents_md_docs {
@@ -330,7 +330,7 @@ impl LoadedAgentsMd {
Self {
entries: vec![InstructionEntry {
contents,
provenance: InstructionProvenance::User(Some(path)),
provenance: InstructionProvenance::User(path),
}],
}
}
@@ -347,7 +347,7 @@ impl LoadedAgentsMd {
Self {
entries: vec![InstructionEntry {
contents,
provenance: InstructionProvenance::User(None),
provenance: InstructionProvenance::InlineUser,
}],
}
}
@@ -358,12 +358,17 @@ impl LoadedAgentsMd {
.all(|entry| entry.contents.trim().is_empty())
}
fn user_instructions(&self) -> Self {
fn user_scoped_instructions(&self) -> Self {
Self {
entries: self
.entries
.iter()
.filter(|entry| matches!(entry.provenance, InstructionProvenance::User(_)))
.filter(|entry| {
matches!(
entry.provenance,
InstructionProvenance::User(_) | InstructionProvenance::InlineUser
)
})
.cloned()
.collect(),
}
@@ -374,16 +379,15 @@ impl LoadedAgentsMd {
let mut output = String::new();
let mut previous_provenance: Option<&InstructionProvenance> = None;
for entry in &self.entries {
if entry.contents.trim().is_empty() {
continue;
}
if let Some(previous_provenance) = previous_provenance {
// The project-doc marker tells the model where workspace-scoped
// instructions begin, so it is only needed on the transition
// from user or internal instructions to project instructions.
let separator = match (previous_provenance, &entry.provenance) {
(
InstructionProvenance::User(_) | InstructionProvenance::Internal,
InstructionProvenance::User(_)
| InstructionProvenance::InlineUser
| InstructionProvenance::Internal,
InstructionProvenance::Project(_),
) => AGENTS_MD_SEPARATOR,
_ => "\n\n",
@@ -417,7 +421,10 @@ struct InstructionEntry {
#[derive(Clone, Debug, PartialEq, Eq)]
enum InstructionProvenance {
/// User-level instructions, normally loaded from CODEX_HOME.
User(Option<AbsolutePathBuf>),
User(AbsolutePathBuf),
/// User-scoped instructions supplied directly rather than loaded from a file.
InlineUser,
/// Workspace instructions discovered from project AGENTS.md files.
Project(AbsolutePathBuf),
@@ -429,9 +436,8 @@ enum InstructionProvenance {
impl InstructionProvenance {
fn path(&self) -> Option<&AbsolutePathBuf> {
match self {
Self::User(path) => path.as_ref(),
Self::Project(path) => Some(path),
Self::Internal => None,
Self::User(path) | Self::Project(path) => Some(path),
Self::InlineUser | Self::Internal => None,
}
}
}
+4 -4
View File
@@ -388,7 +388,7 @@ async fn resolving_again_replaces_project_instructions() {
entries: vec![
InstructionEntry {
contents: "user instructions".to_string(),
provenance: InstructionProvenance::User(Some(user_agents)),
provenance: InstructionProvenance::User(user_agents),
},
InstructionEntry {
contents: "second project".to_string(),
@@ -517,7 +517,7 @@ async fn child_agents_message_after_global_instructions_uses_plain_separator() {
entries: vec![
InstructionEntry {
contents: "global doc".to_string(),
provenance: InstructionProvenance::User(Some(global_agents)),
provenance: InstructionProvenance::User(global_agents),
},
InstructionEntry {
contents: HIERARCHICAL_AGENTS_MESSAGE.to_string(),
@@ -554,7 +554,7 @@ async fn instruction_sources_include_global_before_agents_md_docs() {
entries: vec![
InstructionEntry {
contents: "global doc".to_string(),
provenance: InstructionProvenance::User(Some(global_agents.clone())),
provenance: InstructionProvenance::User(global_agents.clone()),
},
InstructionEntry {
contents: "project doc".to_string(),
@@ -595,7 +595,7 @@ async fn child_agents_message_after_project_docs_is_not_an_instruction_source()
entries: vec![
InstructionEntry {
contents: "global doc".to_string(),
provenance: InstructionProvenance::User(Some(global_agents.clone())),
provenance: InstructionProvenance::User(global_agents.clone()),
},
InstructionEntry {
contents: "project doc".to_string(),
+1
View File
@@ -18,6 +18,7 @@ workspace = true
[dependencies]
anyhow = { workspace = true }
codex-agents-md = { workspace = true }
codex-arg0 = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
+11 -11
View File
@@ -9,7 +9,7 @@ use crate::exec_approval::handle_exec_approval_request;
use crate::outgoing_message::OutgoingMessageSender;
use crate::outgoing_message::OutgoingNotificationMeta;
use crate::patch_approval::handle_patch_approval_request;
use codex_core::AgentsMdManager;
use codex_agents_md::load_thread_user_instructions;
use codex_core::CodexThread;
use codex_core::NewThread;
use codex_core::ThreadManager;
@@ -63,16 +63,16 @@ pub async fn run_codex_tool_session(
thread_manager: Arc<ThreadManager>,
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, ThreadId>>>,
) {
let mut warnings = Vec::new();
config.user_instructions = match thread_manager.environment_manager().default_environment() {
Some(environment) => {
AgentsMdManager::new(&config)
.load_user_instructions(environment.as_ref(), &mut warnings)
.await
}
None => None,
};
config.startup_warnings.extend(warnings);
let environments = thread_manager.default_environment_selections(&config.cwd);
if let Err(err) = load_thread_user_instructions(
&mut config,
thread_manager.environment_manager().as_ref(),
&environments,
)
.await
{
tracing::error!("failed to load thread user instructions: {err}");
}
let NewThread {
thread_id,
thread,
+2 -1
View File
@@ -15,9 +15,10 @@ workspace = true
[dependencies]
anyhow = { workspace = true }
chrono = { workspace = true }
codex-agents-md = { workspace = true }
codex-backend-client = { workspace = true }
codex-core = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-features = { workspace = true }
codex-git-utils = { workspace = true }
codex-login = { workspace = true }
+7 -15
View File
@@ -1,4 +1,4 @@
use codex_core::AgentsMdManager;
use codex_agents_md::load_thread_user_instructions;
use codex_core::CodexThread;
use codex_core::ModelClient;
use codex_core::NewThread;
@@ -237,20 +237,12 @@ impl MemoryStartupContext {
let environments = self
.thread_manager
.default_environment_selections(&config.cwd);
let mut warnings = Vec::new();
config.user_instructions = match environments.first().and_then(|selection| {
self.thread_manager
.environment_manager()
.get_environment(&selection.environment_id)
}) {
Some(environment) => {
AgentsMdManager::new(&config)
.load_user_instructions(environment.as_ref(), &mut warnings)
.await
}
None => None,
};
config.startup_warnings.extend(warnings);
load_thread_user_instructions(
&mut config,
self.thread_manager.environment_manager().as_ref(),
&environments,
)
.await?;
let NewThread {
thread_id, thread, ..
} = self