mirror of
https://github.com/openai/codex.git
synced 2026-04-07 14:24:47 +00:00
Compare commits
16 Commits
dev/window
...
dev/honor-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9212c96815 | ||
|
|
a23c12a7fd | ||
|
|
585450b16b | ||
|
|
3047ae3fb2 | ||
|
|
f148caa9a1 | ||
|
|
891566471f | ||
|
|
60bf8ff815 | ||
|
|
eedec118b7 | ||
|
|
aafcfec146 | ||
|
|
8ecfca3186 | ||
|
|
81252aa0c5 | ||
|
|
e7e8ae8450 | ||
|
|
ae434054e4 | ||
|
|
7a9927a73c | ||
|
|
f50a52b895 | ||
|
|
11b179bdee |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -2794,7 +2794,6 @@ dependencies = [
|
||||
"codex-cloud-requirements",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
|
||||
@@ -84,6 +84,7 @@ pub fn create_fake_rollout_with_source(
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
@@ -167,6 +168,7 @@ pub fn create_fake_rollout_with_text_elements(
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
|
||||
@@ -200,6 +200,10 @@ async fn thread_fork_honors_explicit_null_thread_instructions() -> Result<()> {
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let mut config_toml = std::fs::read_to_string(&config_path)?;
|
||||
config_toml.push_str("\ndeveloper_instructions = \"Config developer instructions sentinel\"\n");
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let conversation_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
@@ -323,6 +327,13 @@ async fn thread_fork_honors_explicit_null_thread_instructions() -> Result<()> {
|
||||
"unexpected instructions field in payload: {payload:?}"
|
||||
);
|
||||
let developer_texts = request.message_input_texts("developer");
|
||||
assert_eq!(
|
||||
developer_texts
|
||||
.iter()
|
||||
.any(|text| { text.contains("Config developer instructions sentinel") }),
|
||||
expect_instructions,
|
||||
"unexpected config developer instruction presence: {developer_texts:?}"
|
||||
);
|
||||
assert!(
|
||||
developer_texts.iter().all(|text| !text.is_empty()),
|
||||
"did not expect empty developer instruction messages: {developer_texts:?}"
|
||||
|
||||
@@ -377,6 +377,7 @@ stream_max_retries = 0
|
||||
agent_role: None,
|
||||
model_provider: Some("mock_provider".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
|
||||
@@ -166,6 +166,10 @@ async fn turn_start_honors_explicit_null_thread_instructions() -> Result<()> {
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never", &BTreeMap::new())?;
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let mut config_toml = std::fs::read_to_string(&config_path)?;
|
||||
config_toml.push_str("\ndeveloper_instructions = \"Config developer instructions sentinel\"\n");
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -240,6 +244,13 @@ async fn turn_start_honors_explicit_null_thread_instructions() -> Result<()> {
|
||||
"unexpected instructions field in payload: {payload:?}"
|
||||
);
|
||||
let developer_texts = request.message_input_texts("developer");
|
||||
assert_eq!(
|
||||
developer_texts
|
||||
.iter()
|
||||
.any(|text| { text.contains("Config developer instructions sentinel") }),
|
||||
expect_instructions,
|
||||
"unexpected config developer instruction presence: {developer_texts:?}"
|
||||
);
|
||||
assert!(
|
||||
developer_texts.iter().all(|text| !text.is_empty()),
|
||||
"did not expect empty developer instruction messages: {developer_texts:?}"
|
||||
|
||||
@@ -536,11 +536,7 @@ impl Codex {
|
||||
config.startup_warnings.push(message);
|
||||
}
|
||||
|
||||
let environment = environment_manager
|
||||
.current()
|
||||
.await
|
||||
.map_err(|err| CodexErr::Fatal(format!("failed to create environment: {err}")))?;
|
||||
let user_instructions = get_user_instructions(&config, environment.as_deref()).await;
|
||||
let user_instructions = get_user_instructions(&config).await;
|
||||
|
||||
let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) {
|
||||
// Guardian review should rely on the built-in shell safety checks,
|
||||
@@ -618,6 +614,14 @@ impl Codex {
|
||||
dynamic_tools
|
||||
};
|
||||
|
||||
let developer_instructions_override = config
|
||||
.developer_instructions_override
|
||||
.clone()
|
||||
.or_else(|| conversation_history.get_developer_instructions());
|
||||
let developer_instructions = developer_instructions_override
|
||||
.clone()
|
||||
.unwrap_or_else(|| config.developer_instructions.clone());
|
||||
|
||||
// TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode
|
||||
// to avoid extracting these fields separately and constructing CollaborationMode here.
|
||||
let collaboration_mode = CollaborationMode {
|
||||
@@ -633,7 +637,8 @@ impl Codex {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
service_tier: config.service_tier,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions,
|
||||
developer_instructions_override,
|
||||
user_instructions,
|
||||
personality: config.personality,
|
||||
base_instructions,
|
||||
@@ -672,12 +677,12 @@ impl Codex {
|
||||
agent_status_tx.clone(),
|
||||
conversation_history,
|
||||
session_source_clone,
|
||||
environment_manager,
|
||||
skills_manager,
|
||||
plugins_manager,
|
||||
mcp_manager.clone(),
|
||||
skills_watcher,
|
||||
agent_control,
|
||||
environment,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -1103,6 +1108,10 @@ pub(crate) struct SessionConfiguration {
|
||||
/// Developer instructions that supplement the base instructions.
|
||||
developer_instructions: Option<String>,
|
||||
|
||||
/// Explicit developer instructions override, preserving `null` as distinct
|
||||
/// from a missing override.
|
||||
developer_instructions_override: Option<Option<String>>,
|
||||
|
||||
/// Model instructions that are appended to the base instructions.
|
||||
user_instructions: Option<String>,
|
||||
|
||||
@@ -1526,12 +1535,12 @@ impl Session {
|
||||
agent_status: watch::Sender<AgentStatus>,
|
||||
initial_history: InitialHistory,
|
||||
session_source: SessionSource,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
skills_manager: Arc<SkillsManager>,
|
||||
plugins_manager: Arc<PluginsManager>,
|
||||
mcp_manager: Arc<McpManager>,
|
||||
skills_watcher: Arc<SkillsWatcher>,
|
||||
agent_control: AgentControl,
|
||||
environment: Option<Arc<Environment>>,
|
||||
) -> anyhow::Result<Arc<Self>> {
|
||||
debug!(
|
||||
"Configuring session: model={}; provider={:?}",
|
||||
@@ -1553,6 +1562,9 @@ impl Session {
|
||||
.base_instructions
|
||||
.clone()
|
||||
.map(|text| BaseInstructions { text }),
|
||||
session_configuration
|
||||
.developer_instructions_override
|
||||
.clone(),
|
||||
session_configuration.dynamic_tools.clone(),
|
||||
if session_configuration.persist_extended_history {
|
||||
EventPersistenceMode::Extended
|
||||
@@ -1972,7 +1984,7 @@ impl Session {
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(
|
||||
config.js_repl_node_path.clone(),
|
||||
),
|
||||
environment,
|
||||
environment: environment_manager.current().await?,
|
||||
};
|
||||
services
|
||||
.model_client
|
||||
|
||||
@@ -1853,6 +1853,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
@@ -1955,6 +1956,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
@@ -2227,6 +2229,7 @@ async fn attach_rollout_recorder(session: &Arc<Session>) -> PathBuf {
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
@@ -2304,6 +2307,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
@@ -2570,6 +2574,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
@@ -2616,16 +2621,14 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
agent_status_tx,
|
||||
InitialHistory::New,
|
||||
SessionSource::Exec,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::new(
|
||||
/*exec_server_url*/ None,
|
||||
)),
|
||||
skills_manager,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
Arc::new(SkillsWatcher::noop()),
|
||||
AgentControl::default(),
|
||||
Some(Arc::new(
|
||||
codex_exec_server::Environment::create(/*exec_server_url*/ None)
|
||||
.await
|
||||
.expect("create environment"),
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -2673,6 +2676,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
@@ -3513,6 +3517,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
@@ -4089,13 +4094,18 @@ async fn handle_output_item_done_records_image_save_history_message() {
|
||||
let image_output_dir = image_output_path
|
||||
.parent()
|
||||
.expect("generated image path should have a parent");
|
||||
let image_message: ResponseItem = DeveloperInstructions::new(format!(
|
||||
"Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.",
|
||||
let save_message: ResponseItem = DeveloperInstructions::new(format!(
|
||||
"Generated images are saved to {} as {} by default.",
|
||||
image_output_dir.display(),
|
||||
image_output_path.display(),
|
||||
))
|
||||
.into();
|
||||
assert_eq!(history.raw_items(), &[image_message, item]);
|
||||
let copy_message: ResponseItem = DeveloperInstructions::new(
|
||||
"If you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it."
|
||||
.to_string(),
|
||||
)
|
||||
.into();
|
||||
assert_eq!(history.raw_items(), &[save_message, copy_message, item]);
|
||||
assert_eq!(
|
||||
std::fs::read(&expected_saved_path).expect("saved file"),
|
||||
b"foo"
|
||||
@@ -4264,6 +4274,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
@@ -4361,6 +4372,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
|
||||
@@ -4506,6 +4506,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
developer_instructions_override: None,
|
||||
guardian_developer_instructions: None,
|
||||
include_permissions_instructions: true,
|
||||
include_apps_instructions: true,
|
||||
@@ -4651,6 +4652,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
developer_instructions_override: None,
|
||||
guardian_developer_instructions: None,
|
||||
include_permissions_instructions: true,
|
||||
include_apps_instructions: true,
|
||||
@@ -4794,6 +4796,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
developer_instructions_override: None,
|
||||
guardian_developer_instructions: None,
|
||||
include_permissions_instructions: true,
|
||||
include_apps_instructions: true,
|
||||
@@ -4923,6 +4926,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
developer_instructions_override: None,
|
||||
guardian_developer_instructions: None,
|
||||
include_permissions_instructions: true,
|
||||
include_apps_instructions: true,
|
||||
|
||||
@@ -248,6 +248,10 @@ pub struct Config {
|
||||
/// Developer instructions override injected as a separate message.
|
||||
pub developer_instructions: Option<String>,
|
||||
|
||||
/// Explicit developer instructions override, preserving `null` as distinct
|
||||
/// from a missing override.
|
||||
pub developer_instructions_override: Option<Option<String>>,
|
||||
|
||||
/// Guardian-specific developer instructions override from requirements.toml.
|
||||
pub guardian_developer_instructions: Option<String>,
|
||||
|
||||
@@ -1761,6 +1765,7 @@ impl Config {
|
||||
let file_base_instructions =
|
||||
Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?;
|
||||
let base_instructions = base_instructions.or_else(|| file_base_instructions.map(Some));
|
||||
let developer_instructions_override = developer_instructions.clone();
|
||||
let developer_instructions =
|
||||
developer_instructions.unwrap_or_else(|| cfg.developer_instructions.clone());
|
||||
let include_permissions_instructions = config_profile
|
||||
@@ -1945,6 +1950,7 @@ impl Config {
|
||||
base_instructions,
|
||||
personality,
|
||||
developer_instructions,
|
||||
developer_instructions_override,
|
||||
compact_prompt,
|
||||
commit_attribution,
|
||||
include_permissions_instructions,
|
||||
|
||||
@@ -43,6 +43,7 @@ async fn write_session_with_user_event(codex_home: &Path) -> io::Result<()> {
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -21,12 +21,10 @@ use crate::config_loader::default_project_root_markers;
|
||||
use crate::config_loader::merge_toml_values;
|
||||
use crate::config_loader::project_root_markers_from_config;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_features::Feature;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use dunce::canonicalize as normalize_path;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use toml::Value as TomlValue;
|
||||
use tracing::error;
|
||||
|
||||
@@ -78,19 +76,8 @@ fn render_js_repl_instructions(config: &Config) -> Option<String> {
|
||||
|
||||
/// Combines `Config::instructions` and `AGENTS.md` (if present) into a single
|
||||
/// string of instructions.
|
||||
pub(crate) async fn get_user_instructions(
|
||||
config: &Config,
|
||||
environment: Option<&Environment>,
|
||||
) -> Option<String> {
|
||||
let fs = environment?.get_filesystem();
|
||||
get_user_instructions_with_fs(config, fs.as_ref()).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_user_instructions_with_fs(
|
||||
config: &Config,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
) -> Option<String> {
|
||||
let project_docs = read_project_docs_with_fs(config, fs).await;
|
||||
pub(crate) async fn get_user_instructions(config: &Config) -> Option<String> {
|
||||
let project_docs = read_project_docs(config).await;
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
@@ -138,25 +125,14 @@ pub(crate) async fn get_user_instructions_with_fs(
|
||||
/// concatenation of all discovered docs. If no documentation file is found the
|
||||
/// function returns `Ok(None)`. Unexpected I/O failures bubble up as `Err` so
|
||||
/// callers can decide how to handle them.
|
||||
pub async fn read_project_docs(
|
||||
config: &Config,
|
||||
environment: &Environment,
|
||||
) -> io::Result<Option<String>> {
|
||||
let fs = environment.get_filesystem();
|
||||
read_project_docs_with_fs(config, fs.as_ref()).await
|
||||
}
|
||||
|
||||
async fn read_project_docs_with_fs(
|
||||
config: &Config,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
) -> io::Result<Option<String>> {
|
||||
pub async fn read_project_docs(config: &Config) -> std::io::Result<Option<String>> {
|
||||
let max_total = config.project_doc_max_bytes;
|
||||
|
||||
if max_total == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let paths = discover_project_doc_paths(config, fs).await?;
|
||||
let paths = discover_project_doc_paths(config)?;
|
||||
if paths.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -169,22 +145,16 @@ async fn read_project_docs_with_fs(
|
||||
break;
|
||||
}
|
||||
|
||||
match fs.get_metadata(&p).await {
|
||||
Ok(metadata) if !metadata.is_file => continue,
|
||||
Ok(_) => {}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
let mut data = match fs.read_file(&p).await {
|
||||
Ok(data) => data,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(err),
|
||||
let file = match tokio::fs::File::open(&p).await {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let size = data.len() as u64;
|
||||
if size > remaining {
|
||||
data.truncate(remaining as usize);
|
||||
}
|
||||
|
||||
let size = file.metadata().await?.len();
|
||||
let mut reader = tokio::io::BufReader::new(file).take(remaining);
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
reader.read_to_end(&mut data).await?;
|
||||
|
||||
if size > remaining {
|
||||
tracing::warn!(
|
||||
@@ -213,17 +183,10 @@ async fn read_project_docs_with_fs(
|
||||
/// contents. The list is ordered from project root to the current working
|
||||
/// directory (inclusive). Symlinks are allowed. When `project_doc_max_bytes`
|
||||
/// is zero, returns an empty list.
|
||||
pub async fn discover_project_doc_paths(
|
||||
config: &Config,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
) -> io::Result<Vec<AbsolutePathBuf>> {
|
||||
if config.project_doc_max_bytes == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut dir = config.cwd.clone();
|
||||
pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBuf>> {
|
||||
let mut dir = config.cwd.to_path_buf();
|
||||
if let Ok(canon) = normalize_path(&dir) {
|
||||
dir = AbsolutePathBuf::try_from(canon)?;
|
||||
dir = canon;
|
||||
}
|
||||
|
||||
let mut merged = TomlValue::Table(toml::map::Map::new());
|
||||
@@ -248,14 +211,14 @@ pub async fn discover_project_doc_paths(
|
||||
if !project_root_markers.is_empty() {
|
||||
for ancestor in dir.ancestors() {
|
||||
for marker in &project_root_markers {
|
||||
let marker_path = AbsolutePathBuf::try_from(ancestor.join(marker))?;
|
||||
let marker_exists = match fs.get_metadata(&marker_path).await {
|
||||
let marker_path = ancestor.join(marker);
|
||||
let marker_exists = match std::fs::metadata(&marker_path) {
|
||||
Ok(_) => true,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => false,
|
||||
Err(err) => return Err(err),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
if marker_exists {
|
||||
project_root = Some(AbsolutePathBuf::try_from(ancestor.to_path_buf())?);
|
||||
project_root = Some(ancestor.to_path_buf());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -265,11 +228,11 @@ pub async fn discover_project_doc_paths(
|
||||
}
|
||||
}
|
||||
|
||||
let search_dirs: Vec<AbsolutePathBuf> = if let Some(root) = project_root {
|
||||
let search_dirs: Vec<PathBuf> = if let Some(root) = project_root {
|
||||
let mut dirs = Vec::new();
|
||||
let mut cursor = dir.clone();
|
||||
let mut cursor = dir.as_path();
|
||||
loop {
|
||||
dirs.push(cursor.clone());
|
||||
dirs.push(cursor.to_path_buf());
|
||||
if cursor == root {
|
||||
break;
|
||||
}
|
||||
@@ -284,25 +247,29 @@ pub async fn discover_project_doc_paths(
|
||||
vec![dir]
|
||||
};
|
||||
|
||||
let mut found: Vec<AbsolutePathBuf> = Vec::new();
|
||||
let mut found: Vec<PathBuf> = Vec::new();
|
||||
let candidate_filenames = candidate_filenames(config);
|
||||
for d in search_dirs {
|
||||
for name in &candidate_filenames {
|
||||
let candidate = d.join(name)?;
|
||||
match fs.get_metadata(&candidate).await {
|
||||
Ok(md) if md.is_file => {
|
||||
found.push(candidate);
|
||||
break;
|
||||
let candidate = d.join(name);
|
||||
match std::fs::symlink_metadata(&candidate) {
|
||||
Ok(md) => {
|
||||
let ft = md.file_type();
|
||||
// Allow regular files and symlinks; opening will later fail for dangling links.
|
||||
if ft.is_file() || ft.is_symlink() {
|
||||
found.push(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(err),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
fn candidate_filenames<'a>(config: &'a Config) -> Vec<&'a str> {
|
||||
let mut names: Vec<&'a str> =
|
||||
Vec::with_capacity(2 + config.project_doc_fallback_filenames.len());
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
use super::*;
|
||||
use crate::config::ConfigBuilder;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::Feature;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::TempDirExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn get_user_instructions(config: &Config) -> Option<String> {
|
||||
super::get_user_instructions_with_fs(config, LOCAL_FS.as_ref()).await
|
||||
}
|
||||
|
||||
async fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<AbsolutePathBuf>> {
|
||||
super::discover_project_doc_paths(config, LOCAL_FS.as_ref()).await
|
||||
}
|
||||
|
||||
/// Helper that returns a `Config` pointing at `root` and using `limit` as
|
||||
/// the maximum number of bytes to embed from AGENTS.md. The caller can
|
||||
/// optionally specify a custom `instructions` string – when `None` the
|
||||
@@ -96,16 +85,6 @@ async fn no_doc_file_returns_none() {
|
||||
assert!(res.is_none(), "Expected None when AGENTS.md is absent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_environment_returns_none() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let config = make_config(&tmp, /*limit*/ 4096, Some("user instructions")).await;
|
||||
|
||||
let res = super::get_user_instructions(&config, /*environment*/ None).await;
|
||||
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
/// Small file within the byte-limit is returned unmodified.
|
||||
#[tokio::test]
|
||||
async fn doc_smaller_than_limit_is_returned() {
|
||||
@@ -182,18 +161,6 @@ async fn zero_byte_limit_disables_docs() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn zero_byte_limit_disables_discovery() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "something").unwrap();
|
||||
|
||||
let discovery =
|
||||
discover_project_doc_paths(&make_config(&tmp, /*limit*/ 0, /*instructions*/ None).await)
|
||||
.await
|
||||
.expect("discover paths");
|
||||
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn js_repl_instructions_are_appended_when_enabled() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
@@ -326,18 +293,11 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
|
||||
.await;
|
||||
cfg.cwd = nested.abs();
|
||||
|
||||
let discovery = discover_project_doc_paths(&cfg)
|
||||
.await
|
||||
.expect("discover paths");
|
||||
let expected_parent = AbsolutePathBuf::try_from(
|
||||
dunce::canonicalize(root.path().join("AGENTS.md")).expect("canonical parent doc path"),
|
||||
)
|
||||
.expect("absolute parent doc path");
|
||||
let expected_child = AbsolutePathBuf::try_from(
|
||||
dunce::canonicalize(cfg.cwd.join("AGENTS.md").expect("absolute child doc path"))
|
||||
.expect("canonical child doc path"),
|
||||
)
|
||||
.expect("absolute child doc path");
|
||||
let discovery = discover_project_doc_paths(&cfg).expect("discover paths");
|
||||
let expected_parent =
|
||||
dunce::canonicalize(root.path().join("AGENTS.md")).expect("canonical parent doc path");
|
||||
let expected_child =
|
||||
dunce::canonicalize(cfg.cwd.as_path().join("AGENTS.md")).expect("canonical child doc path");
|
||||
assert_eq!(discovery.len(), 2);
|
||||
assert_eq!(discovery[0], expected_parent);
|
||||
assert_eq!(discovery[1], expected_child);
|
||||
@@ -361,9 +321,7 @@ async fn agents_local_md_preferred() {
|
||||
|
||||
assert_eq!(res, "local");
|
||||
|
||||
let discovery = discover_project_doc_paths(&cfg)
|
||||
.await
|
||||
.expect("discover paths");
|
||||
let discovery = discover_project_doc_paths(&cfg).expect("discover paths");
|
||||
assert_eq!(discovery.len(), 1);
|
||||
assert_eq!(
|
||||
discovery[0].file_name().unwrap().to_string_lossy(),
|
||||
@@ -413,9 +371,7 @@ async fn agents_md_preferred_over_fallbacks() {
|
||||
|
||||
assert_eq!(res, "primary");
|
||||
|
||||
let discovery = discover_project_doc_paths(&cfg)
|
||||
.await
|
||||
.expect("discover paths");
|
||||
let discovery = discover_project_doc_paths(&cfg).expect("discover paths");
|
||||
assert_eq!(discovery.len(), 1);
|
||||
assert!(
|
||||
discovery[0]
|
||||
@@ -426,73 +382,6 @@ async fn agents_md_preferred_over_fallbacks() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agents_md_directory_is_ignored() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::create_dir(tmp.path().join("AGENTS.md")).unwrap();
|
||||
|
||||
let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
|
||||
|
||||
let res = get_user_instructions(&cfg).await;
|
||||
assert_eq!(res, None);
|
||||
|
||||
let discovery = discover_project_doc_paths(&cfg)
|
||||
.await
|
||||
.expect("discover paths");
|
||||
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn agents_md_special_file_is_ignored() {
|
||||
use std::ffi::CString;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let path = tmp.path().join("AGENTS.md");
|
||||
let c_path = CString::new(path.as_os_str().as_bytes()).expect("path without nul");
|
||||
// SAFETY: `c_path` is a valid, nul-terminated path and `mkfifo` does not
|
||||
// retain the pointer after the call.
|
||||
let rc = unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) };
|
||||
assert_eq!(rc, 0);
|
||||
|
||||
let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
|
||||
|
||||
let res = get_user_instructions(&cfg).await;
|
||||
assert_eq!(res, None);
|
||||
|
||||
let discovery = discover_project_doc_paths(&cfg)
|
||||
.await
|
||||
.expect("discover paths");
|
||||
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn override_directory_falls_back_to_agents_md_file() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::create_dir(tmp.path().join(LOCAL_PROJECT_DOC_FILENAME)).unwrap();
|
||||
fs::write(tmp.path().join(DEFAULT_PROJECT_DOC_FILENAME), "primary").unwrap();
|
||||
|
||||
let cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
|
||||
|
||||
let res = get_user_instructions(&cfg)
|
||||
.await
|
||||
.expect("AGENTS.md should be used when override is a directory");
|
||||
assert_eq!(res, "primary");
|
||||
|
||||
let discovery = discover_project_doc_paths(&cfg)
|
||||
.await
|
||||
.expect("discover paths");
|
||||
assert_eq!(discovery.len(), 1);
|
||||
assert_eq!(
|
||||
discovery[0]
|
||||
.file_name()
|
||||
.expect("file name")
|
||||
.to_string_lossy(),
|
||||
DEFAULT_PROJECT_DOC_FILENAME
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_are_not_appended_to_project_doc() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -379,12 +379,17 @@ pub(crate) async fn handle_non_tool_response_item(
|
||||
.parent()
|
||||
.unwrap_or(turn_context.config.codex_home.as_path());
|
||||
let message: ResponseItem = DeveloperInstructions::new(format!(
|
||||
"Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.",
|
||||
"Generated images are saved to {} as {} by default.",
|
||||
image_output_dir.display(),
|
||||
image_output_path.display(),
|
||||
))
|
||||
.into();
|
||||
sess.record_conversation_items(turn_context, &[message])
|
||||
let copy_message: ResponseItem = DeveloperInstructions::new(
|
||||
"If you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it."
|
||||
.to_string(),
|
||||
)
|
||||
.into();
|
||||
sess.record_conversation_items(turn_context, &[message, copy_message])
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::future::Future;
|
||||
use std::mem::swap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -35,12 +34,12 @@ use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use futures::future::BoxFuture;
|
||||
use serde_json::Value;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::MockServer;
|
||||
|
||||
use crate::PathBufExt;
|
||||
use crate::PathExt;
|
||||
use crate::RemoteEnvConfig;
|
||||
use crate::TempDirExt;
|
||||
use crate::get_remote_test_env;
|
||||
@@ -49,15 +48,13 @@ use crate::responses::WebSocketTestServer;
|
||||
use crate::responses::output_value_to_text;
|
||||
use crate::responses::start_mock_server;
|
||||
use crate::streaming_sse::StreamingSseServer;
|
||||
use crate::wait_for_event;
|
||||
use crate::wait_for_event_match;
|
||||
use crate::wait_for_event_with_timeout;
|
||||
use wiremock::Match;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
type ConfigMutator = dyn FnOnce(&mut Config) + Send;
|
||||
type PreBuildHook = dyn FnOnce(&Path) + Send + 'static;
|
||||
type WorkspaceSetup = dyn FnOnce(AbsolutePathBuf, Arc<dyn ExecutorFileSystem>) -> BoxFuture<'static, Result<()>>
|
||||
+ Send;
|
||||
const TEST_MODEL_WITH_EXPERIMENTAL_TOOLS: &str = "test-gpt-5.1-codex";
|
||||
const REMOTE_EXEC_SERVER_START_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const REMOTE_EXEC_SERVER_POLL_INTERVAL: Duration = Duration::from_millis(25);
|
||||
@@ -100,28 +97,24 @@ impl RemoteExecServerProcess {
|
||||
#[derive(Debug)]
|
||||
pub struct TestEnv {
|
||||
environment: codex_exec_server::Environment,
|
||||
cwd: AbsolutePathBuf,
|
||||
local_cwd_temp_dir: Option<Arc<TempDir>>,
|
||||
cwd: PathBuf,
|
||||
_local_cwd_temp_dir: Option<TempDir>,
|
||||
_remote_exec_server_process: Option<RemoteExecServerProcess>,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
pub async fn local() -> Result<Self> {
|
||||
let local_cwd_temp_dir = Arc::new(TempDir::new()?);
|
||||
let cwd = local_cwd_temp_dir.abs();
|
||||
let local_cwd_temp_dir = TempDir::new()?;
|
||||
let cwd = local_cwd_temp_dir.path().to_path_buf();
|
||||
let environment = codex_exec_server::Environment::create(/*exec_server_url*/ None).await?;
|
||||
Ok(Self {
|
||||
environment,
|
||||
cwd,
|
||||
local_cwd_temp_dir: Some(local_cwd_temp_dir),
|
||||
_local_cwd_temp_dir: Some(local_cwd_temp_dir),
|
||||
_remote_exec_server_process: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cwd(&self) -> &AbsolutePathBuf {
|
||||
&self.cwd
|
||||
}
|
||||
|
||||
pub fn environment(&self) -> &codex_exec_server::Environment {
|
||||
&self.environment
|
||||
}
|
||||
@@ -129,10 +122,6 @@ impl TestEnv {
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
self.environment.exec_server_url()
|
||||
}
|
||||
|
||||
fn local_cwd_temp_dir(&self) -> Option<Arc<TempDir>> {
|
||||
self.local_cwd_temp_dir.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_env() -> Result<TestEnv> {
|
||||
@@ -145,13 +134,16 @@ pub async fn test_env() -> Result<TestEnv> {
|
||||
let cwd = remote_aware_cwd_path();
|
||||
environment
|
||||
.get_filesystem()
|
||||
.create_directory(&cwd, CreateDirectoryOptions { recursive: true })
|
||||
.create_directory(
|
||||
&absolute_path(&cwd)?,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
)
|
||||
.await?;
|
||||
remote_process.process.register_cleanup_path(cwd.as_path());
|
||||
remote_process.process.register_cleanup_path(&cwd);
|
||||
Ok(TestEnv {
|
||||
environment,
|
||||
cwd,
|
||||
local_cwd_temp_dir: None,
|
||||
_local_cwd_temp_dir: None,
|
||||
_remote_exec_server_process: Some(remote_process.process),
|
||||
})
|
||||
}
|
||||
@@ -209,12 +201,11 @@ echo $!"
|
||||
})
|
||||
}
|
||||
|
||||
fn remote_aware_cwd_path() -> AbsolutePathBuf {
|
||||
fn remote_aware_cwd_path() -> PathBuf {
|
||||
PathBuf::from(format!(
|
||||
"/tmp/codex-core-test-cwd-{}",
|
||||
remote_exec_server_instance_id()
|
||||
))
|
||||
.abs()
|
||||
}
|
||||
|
||||
fn wait_for_remote_listen_url(container_name: &str, stdout_path: &str) -> Result<String> {
|
||||
@@ -308,6 +299,10 @@ fn docker_command_capture_stdout<const N: usize>(args: [&str; N]) -> Result<Stri
|
||||
String::from_utf8(output.stdout).context("docker stdout must be utf-8")
|
||||
}
|
||||
|
||||
fn absolute_path(path: &Path) -> Result<AbsolutePathBuf> {
|
||||
Ok(path.abs())
|
||||
}
|
||||
|
||||
/// A collection of different ways the model can output an apply_patch call
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ApplyPatchModelOutput {
|
||||
@@ -331,7 +326,6 @@ pub struct TestCodexBuilder {
|
||||
config_mutators: Vec<Box<ConfigMutator>>,
|
||||
auth: CodexAuth,
|
||||
pre_build_hooks: Vec<Box<PreBuildHook>>,
|
||||
workspace_setups: Vec<Box<WorkspaceSetup>>,
|
||||
home: Option<Arc<TempDir>>,
|
||||
user_shell_override: Option<Shell>,
|
||||
}
|
||||
@@ -365,16 +359,6 @@ impl TestCodexBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_workspace_setup<F, Fut>(mut self, setup: F) -> Self
|
||||
where
|
||||
F: FnOnce(AbsolutePathBuf, Arc<dyn ExecutorFileSystem>) -> Fut + Send + 'static,
|
||||
Fut: Future<Output = Result<()>> + Send + 'static,
|
||||
{
|
||||
self.workspace_setups
|
||||
.push(Box::new(move |cwd, fs| Box::pin(setup(cwd, fs))));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_home(mut self, home: Arc<TempDir>) -> Self {
|
||||
self.home = Some(home);
|
||||
self
|
||||
@@ -398,24 +382,25 @@ impl TestCodexBuilder {
|
||||
Some(home) => home,
|
||||
None => Arc::new(TempDir::new()?),
|
||||
};
|
||||
let base_url = format!("{}/v1", server.uri());
|
||||
let test_env = TestEnv::local().await?;
|
||||
Box::pin(self.build_with_home_and_base_url(base_url, home, /*resume_from*/ None, test_env))
|
||||
.await
|
||||
Box::pin(self.build_with_home(server, home, /*resume_from*/ None)).await
|
||||
}
|
||||
|
||||
pub async fn build_remote_aware(
|
||||
&mut self,
|
||||
server: &wiremock::MockServer,
|
||||
) -> anyhow::Result<TestCodex> {
|
||||
let test_env = test_env().await?;
|
||||
let home = match self.home.clone() {
|
||||
Some(home) => home,
|
||||
None => Arc::new(TempDir::new()?),
|
||||
};
|
||||
let base_url = format!("{}/v1", server.uri());
|
||||
let test_env = test_env().await?;
|
||||
Box::pin(self.build_with_home_and_base_url(base_url, home, /*resume_from*/ None, test_env))
|
||||
.await
|
||||
let cwd = test_env.cwd.to_path_buf();
|
||||
self.config_mutators.push(Box::new(move |config| {
|
||||
config.cwd = cwd.abs();
|
||||
}));
|
||||
let (config, cwd) = self.prepare_config(base_url, &home).await?;
|
||||
Box::pin(self.build_from_config(config, cwd, home, /*resume_from*/ None, test_env)).await
|
||||
}
|
||||
|
||||
pub async fn build_with_streaming_server(
|
||||
@@ -427,12 +412,10 @@ impl TestCodexBuilder {
|
||||
Some(home) => home,
|
||||
None => Arc::new(TempDir::new()?),
|
||||
};
|
||||
let test_env = TestEnv::local().await?;
|
||||
Box::pin(self.build_with_home_and_base_url(
|
||||
format!("{base_url}/v1"),
|
||||
home,
|
||||
/*resume_from*/ None,
|
||||
test_env,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -452,9 +435,7 @@ impl TestCodexBuilder {
|
||||
config.model_provider.supports_websockets = true;
|
||||
config.experimental_realtime_ws_model = Some("realtime-test-model".to_string());
|
||||
}));
|
||||
let test_env = TestEnv::local().await?;
|
||||
Box::pin(self.build_with_home_and_base_url(base_url, home, /*resume_from*/ None, test_env))
|
||||
.await
|
||||
Box::pin(self.build_with_home_and_base_url(base_url, home, /*resume_from*/ None)).await
|
||||
}
|
||||
|
||||
pub async fn resume(
|
||||
@@ -462,10 +443,19 @@ impl TestCodexBuilder {
|
||||
server: &wiremock::MockServer,
|
||||
home: Arc<TempDir>,
|
||||
rollout_path: PathBuf,
|
||||
) -> anyhow::Result<TestCodex> {
|
||||
Box::pin(self.build_with_home(server, home, Some(rollout_path))).await
|
||||
}
|
||||
|
||||
async fn build_with_home(
|
||||
&mut self,
|
||||
server: &wiremock::MockServer,
|
||||
home: Arc<TempDir>,
|
||||
resume_from: Option<PathBuf>,
|
||||
) -> anyhow::Result<TestCodex> {
|
||||
let base_url = format!("{}/v1", server.uri());
|
||||
let test_env = TestEnv::local().await?;
|
||||
Box::pin(self.build_with_home_and_base_url(base_url, home, Some(rollout_path), test_env))
|
||||
let (config, cwd) = self.prepare_config(base_url, &home).await?;
|
||||
Box::pin(self.build_from_config(config, cwd, home, resume_from, TestEnv::local().await?))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -474,30 +464,10 @@ impl TestCodexBuilder {
|
||||
base_url: String,
|
||||
home: Arc<TempDir>,
|
||||
resume_from: Option<PathBuf>,
|
||||
test_env: TestEnv,
|
||||
) -> anyhow::Result<TestCodex> {
|
||||
let (config, fallback_cwd) = self
|
||||
.prepare_config(base_url, &home, test_env.cwd().clone())
|
||||
.await?;
|
||||
let environment_manager = Arc::new(codex_exec_server::EnvironmentManager::new(
|
||||
test_env.exec_server_url().map(str::to_owned),
|
||||
));
|
||||
let file_system = test_env.environment().get_filesystem();
|
||||
let mut workspace_setups = vec![];
|
||||
swap(&mut self.workspace_setups, &mut workspace_setups);
|
||||
for setup in workspace_setups {
|
||||
setup(config.cwd.clone(), Arc::clone(&file_system)).await?;
|
||||
}
|
||||
let cwd = test_env.local_cwd_temp_dir().unwrap_or(fallback_cwd);
|
||||
Box::pin(self.build_from_config(
|
||||
config,
|
||||
cwd,
|
||||
home,
|
||||
resume_from,
|
||||
test_env,
|
||||
environment_manager,
|
||||
))
|
||||
.await
|
||||
let (config, cwd) = self.prepare_config(base_url, &home).await?;
|
||||
Box::pin(self.build_from_config(config, cwd, home, resume_from, TestEnv::local().await?))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn build_from_config(
|
||||
@@ -507,9 +477,11 @@ impl TestCodexBuilder {
|
||||
home: Arc<TempDir>,
|
||||
resume_from: Option<PathBuf>,
|
||||
test_env: TestEnv,
|
||||
environment_manager: Arc<codex_exec_server::EnvironmentManager>,
|
||||
) -> anyhow::Result<TestCodex> {
|
||||
let auth = self.auth.clone();
|
||||
let environment_manager = Arc::new(codex_exec_server::EnvironmentManager::new(
|
||||
test_env.exec_server_url().map(str::to_owned),
|
||||
));
|
||||
let thread_manager = if config.model_catalog.is_some() {
|
||||
ThreadManager::new(
|
||||
&config,
|
||||
@@ -581,7 +553,6 @@ impl TestCodexBuilder {
|
||||
&mut self,
|
||||
base_url: String,
|
||||
home: &TempDir,
|
||||
cwd_override: AbsolutePathBuf,
|
||||
) -> anyhow::Result<(Config, Arc<TempDir>)> {
|
||||
let model_provider = ModelProviderInfo {
|
||||
base_url: Some(base_url),
|
||||
@@ -592,7 +563,7 @@ impl TestCodexBuilder {
|
||||
};
|
||||
let cwd = Arc::new(TempDir::new()?);
|
||||
let mut config = load_default_config_for_test(home).await;
|
||||
config.cwd = cwd_override;
|
||||
config.cwd = cwd.abs();
|
||||
config.model_provider = model_provider;
|
||||
for hook in self.pre_build_hooks.drain(..) {
|
||||
hook(home.path());
|
||||
@@ -767,14 +738,10 @@ impl TestCodex {
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
wait_for_event_with_timeout(
|
||||
&self.codex,
|
||||
|event| match event {
|
||||
EventMsg::TurnComplete(event) => event.turn_id == turn_id,
|
||||
_ => false,
|
||||
},
|
||||
Duration::from_secs(60),
|
||||
)
|
||||
wait_for_event(&self.codex, |event| match event {
|
||||
EventMsg::TurnComplete(event) => event.turn_id == turn_id,
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -930,7 +897,6 @@ pub fn test_codex() -> TestCodexBuilder {
|
||||
config_mutators: vec![],
|
||||
auth: CodexAuth::from_api_key("dummy"),
|
||||
pre_build_hooks: vec![],
|
||||
workspace_setups: vec![],
|
||||
home: None,
|
||||
user_shell_override: None,
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ impl TestCodexExecBuilder {
|
||||
);
|
||||
cmd.current_dir(self.cwd.path())
|
||||
.env("CODEX_HOME", self.home.path())
|
||||
.env("CODEX_SQLITE_HOME", self.home.path())
|
||||
.env(CODEX_API_KEY_ENV_VAR, "dummy");
|
||||
cmd
|
||||
}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
|
||||
async fn agents_instructions(mut builder: TestCodexBuilder) -> Result<String> {
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let test = builder.build_remote_aware(&server).await?;
|
||||
test.submit_turn("hello").await?;
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
request
|
||||
.message_input_texts("user")
|
||||
.into_iter()
|
||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
||||
.ok_or_else(|| anyhow::anyhow!("instructions message not found"))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agents_override_is_preferred_over_agents_md() -> Result<()> {
|
||||
let instructions =
|
||||
agents_instructions(test_codex().with_workspace_setup(|cwd, fs| async move {
|
||||
let agents_md = cwd.join("AGENTS.md").expect("absolute AGENTS.md path");
|
||||
let override_md = cwd
|
||||
.join("AGENTS.override.md")
|
||||
.expect("absolute AGENTS.override.md path");
|
||||
fs.write_file(&agents_md, b"base doc".to_vec()).await?;
|
||||
fs.write_file(&override_md, b"override doc".to_vec())
|
||||
.await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}))
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
instructions.contains("override doc"),
|
||||
"expected AGENTS.override.md contents: {instructions}"
|
||||
);
|
||||
assert!(
|
||||
!instructions.contains("base doc"),
|
||||
"expected AGENTS.md to be ignored when override exists: {instructions}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn configured_fallback_is_used_when_agents_candidate_is_directory() -> Result<()> {
|
||||
let instructions = agents_instructions(
|
||||
test_codex()
|
||||
.with_config(|config| {
|
||||
config.project_doc_fallback_filenames = vec!["WORKFLOW.md".to_string()];
|
||||
})
|
||||
.with_workspace_setup(|cwd, fs| async move {
|
||||
let agents_dir = cwd.join("AGENTS.md").expect("absolute AGENTS.md path");
|
||||
let fallback = cwd.join("WORKFLOW.md").expect("absolute WORKFLOW.md path");
|
||||
fs.create_directory(&agents_dir, CreateDirectoryOptions { recursive: true })
|
||||
.await?;
|
||||
fs.write_file(&fallback, b"fallback doc".to_vec()).await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
instructions.contains("fallback doc"),
|
||||
"expected fallback doc contents: {instructions}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agents_docs_are_concatenated_from_project_root_to_cwd() -> Result<()> {
|
||||
let instructions = agents_instructions(
|
||||
test_codex()
|
||||
.with_config(|config| {
|
||||
config.cwd = config
|
||||
.cwd
|
||||
.join("nested/workspace")
|
||||
.expect("absolute nested workspace path");
|
||||
})
|
||||
.with_workspace_setup(|cwd, fs| async move {
|
||||
let nested = cwd.clone();
|
||||
let root = nested
|
||||
.parent()
|
||||
.and_then(|parent| parent.parent())
|
||||
.expect("nested workspace should have a project root ancestor");
|
||||
let root_agents = root
|
||||
.join("AGENTS.md")
|
||||
.expect("absolute root AGENTS.md path");
|
||||
let git_marker = root.join(".git").expect("absolute .git path");
|
||||
let nested_agents = nested
|
||||
.join("AGENTS.md")
|
||||
.expect("absolute nested AGENTS.md path");
|
||||
|
||||
fs.create_directory(&nested, CreateDirectoryOptions { recursive: true })
|
||||
.await?;
|
||||
fs.write_file(&root_agents, b"root doc".to_vec()).await?;
|
||||
fs.write_file(&git_marker, b"gitdir: /tmp/mock-git-dir\n".to_vec())
|
||||
.await?;
|
||||
fs.write_file(&nested_agents, b"child doc".to_vec()).await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let root_pos = instructions
|
||||
.find("root doc")
|
||||
.expect("expected root doc in AGENTS instructions");
|
||||
let child_pos = instructions
|
||||
.find("child doc")
|
||||
.expect("expected child doc in AGENTS instructions");
|
||||
assert!(
|
||||
root_pos < child_pos,
|
||||
"expected root doc before child doc: {instructions}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -18,22 +18,21 @@ async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_config(|config| {
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ChildAgentsMd)
|
||||
.expect("test config should allow feature update");
|
||||
std::fs::write(
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ChildAgentsMd)
|
||||
.expect("test config should allow feature update");
|
||||
})
|
||||
.with_workspace_setup(|cwd, fs| async move {
|
||||
let agents_md = cwd.join("AGENTS.md").expect("absolute AGENTS.md path");
|
||||
fs.write_file(&agents_md, b"be nice".to_vec()).await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
let test = builder
|
||||
.build_remote_aware(&server)
|
||||
.await
|
||||
.expect("build test codex");
|
||||
.cwd
|
||||
.join("AGENTS.md")
|
||||
.expect("absolute AGENTS.md path"),
|
||||
"be nice",
|
||||
)
|
||||
.expect("write AGENTS.md");
|
||||
});
|
||||
let test = builder.build(&server).await.expect("build test codex");
|
||||
|
||||
test.submit_turn("hello").await.expect("submit turn");
|
||||
|
||||
@@ -74,10 +73,7 @@ async fn hierarchical_agents_emits_when_no_project_doc() {
|
||||
.enable(Feature::ChildAgentsMd)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder
|
||||
.build_remote_aware(&server)
|
||||
.await
|
||||
.expect("build test codex");
|
||||
let test = builder.build(&server).await.expect("build test codex");
|
||||
|
||||
test.submit_turn("hello").await.expect("submit turn");
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ pub static CODEX_ALIASES_TEMP_DIR: Option<TestCodexAliasesGuard> = {
|
||||
mod abort_tasks;
|
||||
mod agent_jobs;
|
||||
mod agent_websocket;
|
||||
mod agents_md;
|
||||
mod apply_patch_cli;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod approvals;
|
||||
|
||||
@@ -71,6 +71,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
@@ -116,6 +117,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use codex_exec_server::RemoveOptions;
|
||||
use core_test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::get_remote_test_env;
|
||||
use core_test_support::test_codex::test_env;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -17,7 +17,8 @@ async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> {
|
||||
let test_env = test_env().await?;
|
||||
let file_system = test_env.environment().get_filesystem();
|
||||
|
||||
let file_path_abs = remote_test_file_path().abs();
|
||||
let file_path = remote_test_file_path();
|
||||
let file_path_abs = absolute_path(file_path.clone())?;
|
||||
let payload = b"remote-test-env-ok".to_vec();
|
||||
|
||||
file_system
|
||||
@@ -38,6 +39,12 @@ async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn absolute_path(path: PathBuf) -> Result<AbsolutePathBuf> {
|
||||
AbsolutePathBuf::try_from(path.clone())
|
||||
.map_err(|err| anyhow::anyhow!("invalid absolute path {}: {err}", path.display()))
|
||||
}
|
||||
|
||||
fn remote_test_file_path() -> PathBuf {
|
||||
let nanos = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(duration) => duration.as_nanos(),
|
||||
|
||||
@@ -174,6 +174,7 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()>
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
|
||||
@@ -146,6 +146,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: Some(dynamic_tools_for_hook),
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -41,7 +41,6 @@ pub use file_system::FileMetadata;
|
||||
pub use file_system::FileSystemResult;
|
||||
pub use file_system::ReadDirectoryEntry;
|
||||
pub use file_system::RemoveOptions;
|
||||
pub use local_file_system::LOCAL_FS;
|
||||
pub use process::ExecBackend;
|
||||
pub use process::ExecProcess;
|
||||
pub use process::StartedExecProcess;
|
||||
|
||||
@@ -3,8 +3,6 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use tokio::io;
|
||||
@@ -19,9 +17,6 @@ use crate::RemoveOptions;
|
||||
|
||||
const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024;
|
||||
|
||||
pub static LOCAL_FS: LazyLock<Arc<dyn ExecutorFileSystem>> =
|
||||
LazyLock::new(|| -> Arc<dyn ExecutorFileSystem> { Arc::new(LocalFileSystem) });
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct LocalFileSystem;
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ use crate::ReadDirectoryEntry;
|
||||
use crate::RemoveOptions;
|
||||
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
const NOT_FOUND_ERROR_CODE: i64 = -32004;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RemoteFileSystem {
|
||||
@@ -152,9 +151,6 @@ impl ExecutorFileSystem for RemoteFileSystem {
|
||||
|
||||
fn map_remote_error(error: ExecServerError) -> io::Error {
|
||||
match error {
|
||||
ExecServerError::Server { code, message } if code == NOT_FOUND_ERROR_CODE => {
|
||||
io::Error::new(io::ErrorKind::NotFound, message)
|
||||
}
|
||||
ExecServerError::Server { code, message } if code == INVALID_REQUEST_ERROR_CODE => {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, message)
|
||||
}
|
||||
|
||||
@@ -356,14 +356,6 @@ pub(crate) fn invalid_params(message: String) -> JSONRPCErrorError {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn not_found(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: -32004,
|
||||
data: None,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn internal_error(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: -32603,
|
||||
|
||||
@@ -26,7 +26,6 @@ use crate::RemoveOptions;
|
||||
use crate::local_file_system::LocalFileSystem;
|
||||
use crate::rpc::internal_error;
|
||||
use crate::rpc::invalid_request;
|
||||
use crate::rpc::not_found;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct FileSystemHandler {
|
||||
@@ -154,9 +153,7 @@ impl FileSystemHandler {
|
||||
}
|
||||
|
||||
fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
|
||||
if err.kind() == io::ErrorKind::NotFound {
|
||||
not_found(err.to_string())
|
||||
} else if err.kind() == io::ErrorKind::InvalidInput {
|
||||
if err.kind() == io::ErrorKind::InvalidInput {
|
||||
invalid_request(err.to_string())
|
||||
} else {
|
||||
internal_error(err.to_string())
|
||||
|
||||
@@ -2351,6 +2351,26 @@ impl InitialHistory {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_developer_instructions(&self) -> Option<Option<String>> {
|
||||
match self {
|
||||
InitialHistory::New => None,
|
||||
InitialHistory::Resumed(resumed) => {
|
||||
resumed.history.iter().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) => {
|
||||
meta_line.meta.developer_instructions.clone()
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) => {
|
||||
meta_line.meta.developer_instructions.clone()
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_dynamic_tools(&self) -> Option<Vec<DynamicToolSpec>> {
|
||||
match self {
|
||||
InitialHistory::New => None,
|
||||
@@ -2549,6 +2569,13 @@ pub struct SessionMeta {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub base_instructions: Option<Option<BaseInstructions>>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_double_option",
|
||||
serialize_with = "serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub developer_instructions: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -2570,6 +2597,7 @@ impl Default for SessionMeta {
|
||||
agent_path: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ async fn extract_metadata_from_rollout_uses_session_meta() {
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
@@ -107,6 +108,7 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() {
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
@@ -369,6 +371,7 @@ fn write_rollout_in_sessions_with_cwd(
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
|
||||
@@ -82,6 +82,7 @@ pub enum RolloutRecorderParams {
|
||||
forked_from_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
base_instructions: Option<BaseInstructions>,
|
||||
developer_instructions: Option<Option<String>>,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
event_persistence_mode: EventPersistenceMode,
|
||||
},
|
||||
@@ -111,6 +112,7 @@ impl RolloutRecorderParams {
|
||||
forked_from_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
base_instructions: Option<BaseInstructions>,
|
||||
developer_instructions: Option<Option<String>>,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
event_persistence_mode: EventPersistenceMode,
|
||||
) -> Self {
|
||||
@@ -119,6 +121,7 @@ impl RolloutRecorderParams {
|
||||
forked_from_id,
|
||||
source,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
dynamic_tools,
|
||||
event_persistence_mode,
|
||||
}
|
||||
@@ -380,6 +383,7 @@ impl RolloutRecorder {
|
||||
forked_from_id,
|
||||
source,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
dynamic_tools,
|
||||
event_persistence_mode,
|
||||
} => {
|
||||
@@ -409,6 +413,7 @@ impl RolloutRecorder {
|
||||
source,
|
||||
model_provider: Some(config.model_provider_id().to_string()),
|
||||
base_instructions: Some(base_instructions),
|
||||
developer_instructions,
|
||||
dynamic_tools: if dynamic_tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -74,6 +74,7 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
@@ -167,6 +168,7 @@ async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Resu
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Cli,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
|
||||
@@ -1140,6 +1140,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".into()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -257,6 +257,7 @@ mod tests {
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
@@ -384,6 +385,7 @@ mod tests {
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -1030,6 +1030,7 @@ mod tests {
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: Some("polluted".to_string()),
|
||||
},
|
||||
@@ -1088,6 +1089,7 @@ mod tests {
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -33,7 +33,6 @@ codex-chatgpt = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
|
||||
@@ -7303,8 +7303,6 @@ impl ChatWidget {
|
||||
.values()
|
||||
.cloned()
|
||||
.collect();
|
||||
let config = self.config.clone();
|
||||
let frame_requester = self.frame_requester.clone();
|
||||
let (cell, handle) = crate::status::new_status_output_with_rate_limits_handle(
|
||||
&self.config,
|
||||
self.status_account_display.as_ref(),
|
||||
@@ -7319,21 +7317,8 @@ impl ChatWidget {
|
||||
self.model_display_name(),
|
||||
collaboration_mode,
|
||||
reasoning_effort_override,
|
||||
"<none>".to_string(),
|
||||
refreshing_rate_limits,
|
||||
);
|
||||
let agents_summary_handle = handle.clone();
|
||||
tokio::spawn(async move {
|
||||
let agents_summary = match crate::status::discover_agents_summary(&config).await {
|
||||
Ok(summary) => summary,
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "failed to discover project docs for /status");
|
||||
"<none>".to_string()
|
||||
}
|
||||
};
|
||||
agents_summary_handle.finish_agents_summary_discovery(agents_summary);
|
||||
frame_requester.schedule_frame();
|
||||
});
|
||||
if let Some(request_id) = request_id {
|
||||
self.refreshing_status_outputs.push((request_id, handle));
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ use super::format::line_display_width;
|
||||
use super::format::push_label;
|
||||
use super::format::truncate_line_to_width;
|
||||
use super::helpers::compose_account_display;
|
||||
use super::helpers::compose_agents_summary;
|
||||
use super::helpers::compose_model_display;
|
||||
use super::helpers::format_directory_display;
|
||||
use super::helpers::format_tokens_compact;
|
||||
@@ -67,20 +68,10 @@ struct StatusRateLimitState {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct StatusHistoryHandle {
|
||||
agents_summary: Arc<RwLock<String>>,
|
||||
rate_limit_state: Arc<RwLock<StatusRateLimitState>>,
|
||||
}
|
||||
|
||||
impl StatusHistoryHandle {
|
||||
pub(crate) fn finish_agents_summary_discovery(&self, agents_summary: String) {
|
||||
#[expect(clippy::expect_used)]
|
||||
let mut current = self
|
||||
.agents_summary
|
||||
.write()
|
||||
.expect("status history agents summary state poisoned");
|
||||
*current = agents_summary;
|
||||
}
|
||||
|
||||
pub(crate) fn finish_rate_limit_refresh(
|
||||
&self,
|
||||
rate_limits: &[RateLimitSnapshotDisplay],
|
||||
@@ -107,7 +98,7 @@ struct StatusHistoryCell {
|
||||
model_details: Vec<String>,
|
||||
directory: PathBuf,
|
||||
permissions: String,
|
||||
agents_summary: Arc<RwLock<String>>,
|
||||
agents_summary: String,
|
||||
collaboration_mode: Option<String>,
|
||||
model_provider: Option<String>,
|
||||
account: Option<StatusAccountDisplay>,
|
||||
@@ -186,7 +177,6 @@ pub(crate) fn new_status_output_with_rate_limits(
|
||||
model_name,
|
||||
collaboration_mode,
|
||||
reasoning_effort_override,
|
||||
"<none>".to_string(),
|
||||
refreshing_rate_limits,
|
||||
)
|
||||
.0
|
||||
@@ -207,7 +197,6 @@ pub(crate) fn new_status_output_with_rate_limits_handle(
|
||||
model_name: &str,
|
||||
collaboration_mode: Option<&str>,
|
||||
reasoning_effort_override: Option<Option<ReasoningEffort>>,
|
||||
agents_summary: String,
|
||||
refreshing_rate_limits: bool,
|
||||
) -> (CompositeHistoryCell, StatusHistoryHandle) {
|
||||
let command = PlainHistoryCell::new(vec!["/status".magenta().into()]);
|
||||
@@ -225,7 +214,6 @@ pub(crate) fn new_status_output_with_rate_limits_handle(
|
||||
model_name,
|
||||
collaboration_mode,
|
||||
reasoning_effort_override,
|
||||
agents_summary,
|
||||
refreshing_rate_limits,
|
||||
);
|
||||
|
||||
@@ -251,7 +239,6 @@ impl StatusHistoryCell {
|
||||
model_name: &str,
|
||||
collaboration_mode: Option<&str>,
|
||||
reasoning_effort_override: Option<Option<ReasoningEffort>>,
|
||||
agents_summary: String,
|
||||
refreshing_rate_limits: bool,
|
||||
) -> (Self, StatusHistoryHandle) {
|
||||
let mut config_entries = vec![
|
||||
@@ -315,6 +302,7 @@ impl StatusHistoryCell {
|
||||
} else {
|
||||
format!("Custom ({sandbox}, {approval})")
|
||||
};
|
||||
let agents_summary = compose_agents_summary(config);
|
||||
let model_provider = format_model_provider(config);
|
||||
let account = compose_account_display(account_display);
|
||||
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
|
||||
@@ -345,7 +333,6 @@ impl StatusHistoryCell {
|
||||
rate_limits,
|
||||
refreshing_rate_limits,
|
||||
}));
|
||||
let agents_summary = Arc::new(RwLock::new(agents_summary));
|
||||
|
||||
(
|
||||
Self {
|
||||
@@ -353,6 +340,7 @@ impl StatusHistoryCell {
|
||||
model_details,
|
||||
directory: config.cwd.to_path_buf(),
|
||||
permissions,
|
||||
agents_summary,
|
||||
collaboration_mode: collaboration_mode.map(ToString::to_string),
|
||||
model_provider,
|
||||
account,
|
||||
@@ -360,13 +348,9 @@ impl StatusHistoryCell {
|
||||
session_id,
|
||||
forked_from,
|
||||
token_usage,
|
||||
agents_summary: agents_summary.clone(),
|
||||
rate_limit_state: rate_limit_state.clone(),
|
||||
},
|
||||
StatusHistoryHandle {
|
||||
agents_summary,
|
||||
rate_limit_state,
|
||||
},
|
||||
StatusHistoryHandle { rate_limit_state },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -580,12 +564,6 @@ impl HistoryCell for StatusHistoryCell {
|
||||
.rate_limit_state
|
||||
.read()
|
||||
.expect("status history rate-limit state poisoned");
|
||||
#[expect(clippy::expect_used)]
|
||||
let agents_summary = self
|
||||
.agents_summary
|
||||
.read()
|
||||
.expect("status history agents summary state poisoned")
|
||||
.clone();
|
||||
|
||||
if self.model_provider.is_some() {
|
||||
push_label(&mut labels, &mut seen, "Model provider");
|
||||
@@ -647,7 +625,7 @@ impl HistoryCell for StatusHistoryCell {
|
||||
}
|
||||
lines.push(formatter.line("Directory", vec![Span::from(directory_value)]));
|
||||
lines.push(formatter.line("Permissions", vec![Span::from(self.permissions.clone())]));
|
||||
lines.push(formatter.line("Agents.md", vec![Span::from(agents_summary)]));
|
||||
lines.push(formatter.line("Agents.md", vec![Span::from(self.agents_summary.clone())]));
|
||||
|
||||
if let Some(account_value) = account_value {
|
||||
lines.push(formatter.line("Account", vec![Span::from(account_value)]));
|
||||
|
||||
@@ -5,10 +5,7 @@ use chrono::DateTime;
|
||||
use chrono::Local;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::project_doc::discover_project_doc_paths;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -36,52 +33,51 @@ pub(crate) fn compose_model_display(
|
||||
(model_name.to_string(), details)
|
||||
}
|
||||
|
||||
pub(crate) async fn discover_agents_summary(config: &Config) -> io::Result<String> {
|
||||
let paths = discover_project_doc_paths(config, LOCAL_FS.as_ref()).await?;
|
||||
Ok(compose_agents_summary(config, &paths))
|
||||
}
|
||||
|
||||
pub(crate) fn compose_agents_summary(config: &Config, paths: &[AbsolutePathBuf]) -> String {
|
||||
let mut rels: Vec<String> = Vec::new();
|
||||
for p in paths {
|
||||
let file_name = p
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
let display = if let Some(parent) = p.parent() {
|
||||
if parent.as_path() == config.cwd.as_path() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
let mut cur = config.cwd.as_path();
|
||||
let mut ups = 0usize;
|
||||
let mut reached = false;
|
||||
while let Some(c) = cur.parent() {
|
||||
if cur == parent.as_path() {
|
||||
reached = true;
|
||||
break;
|
||||
pub(crate) fn compose_agents_summary(config: &Config) -> String {
|
||||
match discover_project_doc_paths(config) {
|
||||
Ok(paths) => {
|
||||
let mut rels: Vec<String> = Vec::new();
|
||||
for p in paths {
|
||||
let file_name = p
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
let display = if let Some(parent) = p.parent() {
|
||||
if parent == config.cwd.as_path() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
let mut cur = config.cwd.as_path();
|
||||
let mut ups = 0usize;
|
||||
let mut reached = false;
|
||||
while let Some(c) = cur.parent() {
|
||||
if cur == parent {
|
||||
reached = true;
|
||||
break;
|
||||
}
|
||||
cur = c;
|
||||
ups += 1;
|
||||
}
|
||||
if reached {
|
||||
let up = format!("..{}", std::path::MAIN_SEPARATOR);
|
||||
format!("{}{}", up.repeat(ups), file_name)
|
||||
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
|
||||
normalize_agents_display_path(stripped)
|
||||
} else {
|
||||
normalize_agents_display_path(&p)
|
||||
}
|
||||
}
|
||||
cur = c;
|
||||
ups += 1;
|
||||
}
|
||||
if reached {
|
||||
let up = format!("..{}", std::path::MAIN_SEPARATOR);
|
||||
format!("{}{}", up.repeat(ups), file_name)
|
||||
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
|
||||
normalize_agents_display_path(stripped)
|
||||
} else {
|
||||
normalize_agents_display_path(p)
|
||||
}
|
||||
normalize_agents_display_path(&p)
|
||||
};
|
||||
rels.push(display);
|
||||
}
|
||||
} else {
|
||||
normalize_agents_display_path(p)
|
||||
};
|
||||
rels.push(display);
|
||||
}
|
||||
|
||||
if rels.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
rels.join(", ")
|
||||
if rels.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
rels.join(", ")
|
||||
}
|
||||
}
|
||||
Err(_) => "<none>".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ pub(crate) use card::new_status_output;
|
||||
#[cfg(test)]
|
||||
pub(crate) use card::new_status_output_with_rate_limits;
|
||||
pub(crate) use card::new_status_output_with_rate_limits_handle;
|
||||
pub(crate) use helpers::discover_agents_summary;
|
||||
pub(crate) use helpers::format_directory_display;
|
||||
pub(crate) use helpers::format_tokens_compact;
|
||||
pub(crate) use helpers::plan_type_display_name;
|
||||
|
||||
Reference in New Issue
Block a user