From 09e1ec811ec096f6cb00bef3a55cf5356ab296ae Mon Sep 17 00:00:00 2001 From: pap Date: Wed, 6 Aug 2025 22:38:38 +0100 Subject: [PATCH] agents doc path passed as sessionconfigured instead --- codex-rs/common/src/config_summary.rs | 12 +- codex-rs/core/src/codex.rs | 1 + codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/project_doc.rs | 108 ++++++++++++++++++ codex-rs/core/src/protocol.rs | 5 + .../src/event_processor_with_human_output.rs | 1 + codex-rs/mcp-server/src/mcp_protocol.rs | 1 + codex-rs/mcp-server/src/outgoing_message.rs | 2 + codex-rs/tui/src/history_cell.rs | 1 + 9 files changed, 129 insertions(+), 3 deletions(-) diff --git a/codex-rs/common/src/config_summary.rs b/codex-rs/common/src/config_summary.rs index 39d524731f..0d86db330a 100644 --- a/codex-rs/common/src/config_summary.rs +++ b/codex-rs/common/src/config_summary.rs @@ -1,17 +1,23 @@ use codex_core::WireApi; +use codex_core::agents_doc_path_string; use codex_core::config::Config; use crate::sandbox_summary::summarize_sandbox_policy; /// Build a list of key/value pairs summarizing the effective configuration. pub fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, String)> { - let mut entries = vec![ - ("workdir", config.cwd.display().to_string()), + let mut entries = vec![("workdir", config.cwd.display().to_string())]; + + entries.extend([ + ( + "agents.md", + agents_doc_path_string(config).unwrap_or_else(|| "none".to_string()), + ), ("model", config.model.clone()), ("provider", config.model_provider_id.clone()), ("approval", config.approval_policy.to_string()), ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), - ]; + ]); if config.model_provider.wire_api == WireApi::Responses && config.model_family.supports_reasoning_summaries { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 98d13b4cd6..64f0cb2bab 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -857,6 +857,7 @@ async fn submission_loop( msg: EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, model, + agents_doc_path: crate::project_doc::agents_doc_path_string(&config), history_log_id, history_entry_count, }), diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index e7dd06e375..037476be0c 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -39,6 +39,7 @@ mod openai_model_info; mod openai_tools; pub mod plan_tool; mod project_doc; +pub use project_doc::agents_doc_path_string; pub use project_doc::discover_project_doc_path; pub mod protocol; mod rollout; diff --git a/codex-rs/core/src/project_doc.rs b/codex-rs/core/src/project_doc.rs index fefdd1d1aa..090ec6ac26 100644 --- a/codex-rs/core/src/project_doc.rs +++ b/codex-rs/core/src/project_doc.rs @@ -13,6 +13,7 @@ use crate::config::Config; use std::path::Path; +use std::path::PathBuf; use tokio::io::AsyncReadExt; use tracing::error; @@ -128,6 +129,113 @@ pub(crate) async fn get_user_instructions(config: &Config) -> Option { } } +/// Return a human‑readable description of the AGENTS.md path(s) that will be +/// loaded for this session, or `None` if neither global nor project docs are +/// present. +/// +/// This mirrors the discovery logic used by `get_user_instructions()`: +/// - If `~/.codex/AGENTS.md` (global) is non‑empty, it is included. +/// - If a project AGENTS.md is found (respecting the byte limit and git root +/// stop), it is included. +/// - When the project_doc_max_bytes is set to 0, project docs are disabled. +pub fn agents_doc_path_string(config: &Config) -> Option { + let mut parts: Vec = Vec::new(); + + // Global AGENTS.md in CODEX_HOME. + if config.user_instructions.is_some() { + let global = config.codex_home.join("AGENTS.md"); + parts.push(global.display().to_string()); + } + + // Project AGENTS.md, unless disabled via byte‑limit == 0. + if config.project_doc_max_bytes > 0 { + if let Some(p) = find_project_doc_path_sync(config) { + parts.push(p.display().to_string()); + } + } + + if parts.is_empty() { + None + } else { + Some(parts.join(" + ")) + } +} + +/// Synchronous counterpart to `find_project_doc()` that returns the discovered +/// path rather than the contents. Skips empty files (after trimming) to stay +/// consistent with `load_first_candidate()`. +fn find_project_doc_path_sync(config: &Config) -> Option { + // Attempt to locate in cwd first. + if let Some(p) = first_nonempty_candidate_in_dir(&config.cwd, CANDIDATE_FILENAMES) { + return Some(p); + } + + // Walk up to git root. + let mut dir = config.cwd.clone(); + if let Ok(canon) = dir.canonicalize() { + dir = canon; + } + + while let Some(parent) = dir.parent() { + let git_marker = dir.join(".git"); + let git_exists = std::fs::metadata(&git_marker).is_ok(); + if git_exists { + if let Some(p) = first_nonempty_candidate_in_dir(&dir, CANDIDATE_FILENAMES) { + return Some(p); + } + break; // do not walk past git root + } + dir = parent.to_path_buf(); + } + + None +} + +/// Return the first path in `dir` that matches any of `names` and is non‑empty +/// (after trimming). Returns `None` if no such file exists. +fn first_nonempty_candidate_in_dir(dir: &Path, names: &[&str]) -> Option { + for name in names { + let candidate = dir.join(name); + // Fast path: must exist and be a file. + let md = match std::fs::metadata(&candidate) { + Ok(m) if m.is_file() => m, + _ => continue, + }; + + // If the file is zero bytes, skip without reading. + if md.len() == 0 { + continue; + } + + // Read up to a modest limit to determine if the contents are + // effectively empty after trimming. Use the same limit as + // `project_doc_max_bytes` would permit by default: however, the goal is + // to avoid reading arbitrarily large files here just for display. Read + // up to 8 KiB which should be sufficient to determine non‑emptiness + // after trimming whitespace. + const MAX_PEEK_BYTES: usize = 8 * 1024; + let mut file = match std::fs::File::open(&candidate) { + Ok(f) => f, + Err(_) => continue, + }; + use std::io::Read as _; + let mut buf = Vec::with_capacity(std::cmp::min(md.len() as usize, MAX_PEEK_BYTES)); + if std::io::Read::by_ref(&mut file) + .take(MAX_PEEK_BYTES as u64) + .read_to_end(&mut buf) + .is_err() + { + continue; + } + let s = String::from_utf8_lossy(&buf); + if s.trim().is_empty() { + continue; + } + return Some(candidate); + } + None +} + /// Attempt to locate and load the project documentation. Currently, the search /// starts from `Config::cwd`, but if we may want to consider other directories /// in the future, e.g., additional writable directories in the `SandboxPolicy`. diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 55000fb6d7..441aca2a67 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -642,6 +642,10 @@ pub struct SessionConfiguredEvent { /// Tell the client what model is being queried. pub model: String, + /// The path(s) to AGENTS.md that were loaded for this session, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub agents_doc_path: Option, + /// Identifier of the history log file (inode on Unix, 0 otherwise). pub history_log_id: u64, @@ -707,6 +711,7 @@ mod tests { msg: EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, model: "codex-mini-latest".to_string(), + agents_doc_path: None, history_log_id: 0, history_entry_count: 0, }), diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 6b03ed7882..2e4a206357 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -494,6 +494,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { model, history_log_id: _, history_entry_count: _, + .. } = session_configured_event; ts_println!( diff --git a/codex-rs/mcp-server/src/mcp_protocol.rs b/codex-rs/mcp-server/src/mcp_protocol.rs index 2f8858a37b..8104d4ac3e 100644 --- a/codex-rs/mcp-server/src/mcp_protocol.rs +++ b/codex-rs/mcp-server/src/mcp_protocol.rs @@ -906,6 +906,7 @@ mod tests { msg: EventMsg::SessionConfigured(codex_core::protocol::SessionConfiguredEvent { session_id: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"), model: "codex-mini-latest".into(), + agents_doc_path: None, history_log_id: 42, history_entry_count: 3, }), diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index e7b0b9b63c..74079d3169 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -242,6 +242,7 @@ mod tests { msg: EventMsg::SessionConfigured(SessionConfiguredEvent { session_id: Uuid::new_v4(), model: "gpt-4o".to_string(), + agents_doc_path: None, history_log_id: 1, history_entry_count: 1000, }), @@ -282,6 +283,7 @@ mod tests { let session_configured_event = SessionConfiguredEvent { session_id: Uuid::new_v4(), model: "gpt-4o".to_string(), + agents_doc_path: None, history_log_id: 1, history_entry_count: 1000, }; diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index facb0e0a8f..1e4002d559 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -169,6 +169,7 @@ impl HistoryCell { session_id, history_log_id: _, history_entry_count: _, + .. } = event; if is_first_event { const VERSION: &str = env!("CARGO_PKG_VERSION");