mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Merge branch 'main' into jif/wal-config
This commit is contained in:
@@ -109,7 +109,8 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
|
||||
_ => false,
|
||||
},
|
||||
RolloutItem::ResponseItem(
|
||||
ResponseItem::Reasoning { .. }
|
||||
ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
@@ -715,7 +716,12 @@ impl AgentControl {
|
||||
agent_id: ThreadId,
|
||||
initial_operation: Op,
|
||||
) -> CodexResult<String> {
|
||||
let last_task_message = render_input_preview(&initial_operation);
|
||||
let last_task_message = match &initial_operation {
|
||||
Op::InterAgentCommunication { communication } => {
|
||||
last_task_message_from_communication(communication)
|
||||
}
|
||||
_ => non_empty_task_message(render_input_preview(&initial_operation)),
|
||||
};
|
||||
let state = self.upgrade()?;
|
||||
let result = self
|
||||
.handle_thread_request_result(
|
||||
@@ -725,8 +731,12 @@ impl AgentControl {
|
||||
)
|
||||
.await;
|
||||
if result.is_ok() {
|
||||
self.state
|
||||
.update_last_task_message(agent_id, last_task_message);
|
||||
match last_task_message {
|
||||
Some(last_task_message) => self
|
||||
.state
|
||||
.update_last_task_message(agent_id, last_task_message),
|
||||
None => self.state.clear_last_task_message(agent_id),
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -736,7 +746,7 @@ impl AgentControl {
|
||||
agent_id: ThreadId,
|
||||
communication: InterAgentCommunication,
|
||||
) -> CodexResult<String> {
|
||||
let last_task_message = communication.content.clone();
|
||||
let last_task_message = last_task_message_from_communication(&communication);
|
||||
let state = self.upgrade()?;
|
||||
let result = self
|
||||
.handle_thread_request_result(
|
||||
@@ -748,8 +758,12 @@ impl AgentControl {
|
||||
)
|
||||
.await;
|
||||
if result.is_ok() {
|
||||
self.state
|
||||
.update_last_task_message(agent_id, last_task_message);
|
||||
match last_task_message {
|
||||
Some(last_task_message) => self
|
||||
.state
|
||||
.update_last_task_message(agent_id, last_task_message),
|
||||
None => self.state.clear_last_task_message(agent_id),
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -1329,6 +1343,17 @@ pub(crate) fn render_input_preview(initial_operation: &Op) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn last_task_message_from_communication(communication: &InterAgentCommunication) -> Option<String> {
|
||||
if communication.encrypted_content.is_some() {
|
||||
return None;
|
||||
}
|
||||
non_empty_task_message(communication.content.clone())
|
||||
}
|
||||
|
||||
fn non_empty_task_message(message: String) -> Option<String> {
|
||||
(!message.is_empty()).then_some(message)
|
||||
}
|
||||
|
||||
fn thread_spawn_depth(session_source: &SessionSource) -> Option<i32> {
|
||||
match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => Some(*depth),
|
||||
|
||||
@@ -527,6 +527,62 @@ async fn send_inter_agent_communication_without_turn_queues_message_without_trig
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn encrypted_inter_agent_communication_clears_existing_last_task_message() {
|
||||
let harness = AgentControlHarness::new().await;
|
||||
let (parent_thread_id, _) = harness.start_thread().await;
|
||||
let agent_path = AgentPath::try_from("/root/worker").expect("agent path");
|
||||
let spawned_agent = harness
|
||||
.control
|
||||
.spawn_agent_with_metadata(
|
||||
harness.config.clone(),
|
||||
text_input("old plaintext task"),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: Some(agent_path.clone()),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
})),
|
||||
SpawnAgentOptions {
|
||||
parent_thread_id: Some(parent_thread_id),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
assert_eq!(
|
||||
harness
|
||||
.control
|
||||
.state
|
||||
.agent_metadata_for_thread(spawned_agent.thread_id)
|
||||
.and_then(|metadata| metadata.last_task_message),
|
||||
Some("old plaintext task".to_string())
|
||||
);
|
||||
|
||||
let communication = InterAgentCommunication::new_encrypted(
|
||||
AgentPath::root(),
|
||||
agent_path,
|
||||
Vec::new(),
|
||||
"encrypted-task".to_string(),
|
||||
/*trigger_turn*/ true,
|
||||
);
|
||||
harness
|
||||
.control
|
||||
.send_inter_agent_communication(spawned_agent.thread_id, communication)
|
||||
.await
|
||||
.expect("send_inter_agent_communication should succeed");
|
||||
|
||||
assert_eq!(
|
||||
harness
|
||||
.control
|
||||
.state
|
||||
.agent_metadata_for_thread(spawned_agent.thread_id)
|
||||
.and_then(|metadata| metadata.last_task_message),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_agent_creates_thread_and_sends_prompt() {
|
||||
let harness = AgentControlHarness::new().await;
|
||||
|
||||
@@ -180,6 +180,20 @@ impl AgentRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_last_task_message(&self, thread_id: ThreadId) {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(metadata) = active_agents
|
||||
.agent_tree
|
||||
.values_mut()
|
||||
.find(|metadata| metadata.agent_id == Some(thread_id))
|
||||
{
|
||||
metadata.last_task_message = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn register_spawned_thread(&self, agent_metadata: AgentMetadata) {
|
||||
let Some(thread_id) = agent_metadata.agent_id else {
|
||||
return;
|
||||
|
||||
+135
-70
@@ -23,11 +23,9 @@ use codex_config::merge_toml_values;
|
||||
use codex_config::project_root_markers_from_config;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::Feature;
|
||||
use codex_prompts::HIERARCHICAL_AGENTS_MESSAGE;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use dunce::canonicalize as normalize_path;
|
||||
use std::io;
|
||||
use toml::Value as TomlValue;
|
||||
use tracing::error;
|
||||
@@ -37,8 +35,8 @@ pub const DEFAULT_AGENTS_MD_FILENAME: &str = "AGENTS.md";
|
||||
/// Preferred local override for AGENTS.md instructions.
|
||||
pub const LOCAL_AGENTS_MD_FILENAME: &str = "AGENTS.override.md";
|
||||
|
||||
/// When both `Config::instructions` and AGENTS.md docs are present, they will
|
||||
/// be concatenated with the following separator.
|
||||
/// When both user and project AGENTS.md docs are present, they will be
|
||||
/// concatenated with the following separator.
|
||||
const AGENTS_MD_SEPARATOR: &str = "\n\n--- project-doc ---\n\n";
|
||||
|
||||
/// Resolves AGENTS.md files into model-visible user instructions and source
|
||||
@@ -47,11 +45,6 @@ pub struct AgentsMdManager<'a> {
|
||||
config: &'a Config,
|
||||
}
|
||||
|
||||
pub(crate) struct LoadedAgentsMd {
|
||||
pub(crate) contents: String,
|
||||
pub(crate) path: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
impl<'a> AgentsMdManager<'a> {
|
||||
pub fn new(config: &'a Config) -> Self {
|
||||
Self { config }
|
||||
@@ -81,10 +74,7 @@ impl<'a> AgentsMdManager<'a> {
|
||||
let contents = String::from_utf8_lossy(&data);
|
||||
let trimmed = contents.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(LoadedAgentsMd {
|
||||
contents: trimmed.to_string(),
|
||||
path,
|
||||
});
|
||||
return Some(LoadedAgentsMd::new_user(trimmed.to_string(), path));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -94,10 +84,10 @@ impl<'a> AgentsMdManager<'a> {
|
||||
/// single model-visible instruction string.
|
||||
pub(crate) async fn user_instructions(
|
||||
&self,
|
||||
environment: Option<&Environment>,
|
||||
environment: &Environment,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> Option<String> {
|
||||
let fs = environment?.get_filesystem();
|
||||
) -> Option<LoadedAgentsMd> {
|
||||
let fs = environment.get_filesystem();
|
||||
self.user_instructions_with_fs(fs.as_ref(), startup_warnings)
|
||||
.await
|
||||
}
|
||||
@@ -106,22 +96,13 @@ impl<'a> AgentsMdManager<'a> {
|
||||
&self,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> Option<String> {
|
||||
) -> Option<LoadedAgentsMd> {
|
||||
let agents_md_docs = self.read_agents_md(fs, startup_warnings).await;
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
if let Some(instructions) = self.config.user_instructions.clone() {
|
||||
output.push_str(&instructions);
|
||||
}
|
||||
let mut loaded = self.config.user_instructions.clone().unwrap_or_default();
|
||||
|
||||
match agents_md_docs {
|
||||
Ok(Some(docs)) => {
|
||||
if !output.is_empty() {
|
||||
output.push_str(AGENTS_MD_SEPARATOR);
|
||||
}
|
||||
output.push_str(&docs);
|
||||
}
|
||||
Ok(Some(docs)) => loaded.entries.extend(docs.entries),
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
error!("error trying to find AGENTS.md docs: {e:#}");
|
||||
@@ -129,50 +110,26 @@ impl<'a> AgentsMdManager<'a> {
|
||||
};
|
||||
|
||||
if self.config.features.enabled(Feature::ChildAgentsMd) {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
output.push_str(HIERARCHICAL_AGENTS_MESSAGE);
|
||||
loaded.entries.push(InstructionEntry {
|
||||
contents: HIERARCHICAL_AGENTS_MESSAGE.to_string(),
|
||||
provenance: InstructionProvenance::Internal,
|
||||
});
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(output)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all instruction source files included in the current config.
|
||||
pub async fn instruction_sources(&self, fs: &dyn ExecutorFileSystem) -> Vec<AbsolutePathBuf> {
|
||||
let mut global_instruction_warnings = Vec::new();
|
||||
let mut paths = Self::load_global_instructions(
|
||||
LOCAL_FS.as_ref(),
|
||||
Some(&self.config.codex_home),
|
||||
&mut global_instruction_warnings,
|
||||
)
|
||||
.await
|
||||
.map(|loaded| vec![loaded.path])
|
||||
.unwrap_or_default();
|
||||
match self.agents_md_paths(fs).await {
|
||||
Ok(agents_md_paths) => paths.extend(agents_md_paths),
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "failed to discover AGENTS.md docs for instruction sources");
|
||||
}
|
||||
}
|
||||
paths
|
||||
(!loaded.is_empty()).then_some(loaded)
|
||||
}
|
||||
|
||||
/// Attempt to locate and load AGENTS.md documentation.
|
||||
///
|
||||
/// On success returns `Ok(Some(contents))` where `contents` is the
|
||||
/// 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.
|
||||
/// On success returns `Ok(Some(loaded))` where `loaded` contains every
|
||||
/// discovered doc. 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.
|
||||
async fn read_agents_md(
|
||||
&self,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> io::Result<Option<String>> {
|
||||
) -> io::Result<Option<LoadedAgentsMd>> {
|
||||
let max_total = self.config.project_doc_max_bytes;
|
||||
|
||||
if max_total == 0 {
|
||||
@@ -185,7 +142,7 @@ impl<'a> AgentsMdManager<'a> {
|
||||
}
|
||||
|
||||
let mut remaining: u64 = max_total as u64;
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
let mut loaded = LoadedAgentsMd::default();
|
||||
|
||||
for p in paths {
|
||||
if remaining == 0 {
|
||||
@@ -221,15 +178,18 @@ impl<'a> AgentsMdManager<'a> {
|
||||
|
||||
let text = String::from_utf8_lossy(&data).to_string();
|
||||
if !text.trim().is_empty() {
|
||||
parts.push(text);
|
||||
loaded.entries.push(InstructionEntry {
|
||||
contents: text,
|
||||
provenance: InstructionProvenance::Project(p),
|
||||
});
|
||||
remaining = remaining.saturating_sub(data.len() as u64);
|
||||
}
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
if loaded.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(parts.join("\n\n")))
|
||||
Ok(Some(loaded))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +206,7 @@ impl<'a> AgentsMdManager<'a> {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut dir = self.config.cwd.clone();
|
||||
if let Ok(canon) = normalize_path(&dir) {
|
||||
dir = AbsolutePathBuf::try_from(canon)?;
|
||||
}
|
||||
let dir = self.config.cwd.clone();
|
||||
|
||||
let mut merged = TomlValue::Table(toml::map::Map::new());
|
||||
for layer in self.config.config_layer_stack.get_layers(
|
||||
@@ -348,6 +305,114 @@ impl<'a> AgentsMdManager<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Model-visible instructions loaded from AGENTS.md files and internal
|
||||
/// guidance.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LoadedAgentsMd {
|
||||
/// Ordered instructions and their provenance.
|
||||
entries: Vec<InstructionEntry>,
|
||||
}
|
||||
|
||||
impl LoadedAgentsMd {
|
||||
/// Creates loaded instructions containing one user-level AGENTS.md entry.
|
||||
pub fn new_user(contents: String, path: AbsolutePathBuf) -> Self {
|
||||
if contents.trim().is_empty() {
|
||||
return Self::default();
|
||||
}
|
||||
Self {
|
||||
entries: vec![InstructionEntry {
|
||||
contents,
|
||||
provenance: InstructionProvenance::User(path),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates source-less user instructions for tests.
|
||||
///
|
||||
/// This cannot be gated with `#[cfg(test)]` because integration tests
|
||||
/// compile `codex-core` as a normal dependency without that configuration.
|
||||
pub fn from_text_for_testing(contents: impl Into<String>) -> Self {
|
||||
let contents = contents.into();
|
||||
if contents.trim().is_empty() {
|
||||
return Self::default();
|
||||
}
|
||||
Self {
|
||||
entries: vec![InstructionEntry {
|
||||
contents,
|
||||
provenance: InstructionProvenance::Internal,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.entries
|
||||
.iter()
|
||||
.all(|entry| entry.contents.trim().is_empty())
|
||||
}
|
||||
|
||||
/// Returns the concatenated model-visible instruction text.
|
||||
pub fn text(&self) -> String {
|
||||
let mut output = String::new();
|
||||
let mut previous_provenance: Option<&InstructionProvenance> = None;
|
||||
for entry in &self.entries {
|
||||
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::Project(_),
|
||||
) => AGENTS_MD_SEPARATOR,
|
||||
_ => "\n\n",
|
||||
};
|
||||
output.push_str(separator);
|
||||
}
|
||||
output.push_str(&entry.contents);
|
||||
previous_provenance = Some(&entry.provenance);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
/// Returns the AGENTS.md files that supplied instruction entries.
|
||||
pub fn sources(&self) -> impl Iterator<Item = &AbsolutePathBuf> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.provenance.path())
|
||||
}
|
||||
}
|
||||
|
||||
/// One model-visible instruction and its provenance.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct InstructionEntry {
|
||||
/// Model-visible instruction text.
|
||||
contents: String,
|
||||
|
||||
/// Origin of the instruction.
|
||||
provenance: InstructionProvenance,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum InstructionProvenance {
|
||||
/// User-level instructions, normally loaded from CODEX_HOME.
|
||||
User(AbsolutePathBuf),
|
||||
|
||||
/// Workspace instructions discovered from project AGENTS.md files.
|
||||
Project(AbsolutePathBuf),
|
||||
|
||||
/// Instructions without a file source, including internally defined guidance.
|
||||
Internal,
|
||||
}
|
||||
|
||||
impl InstructionProvenance {
|
||||
fn path(&self) -> Option<&AbsolutePathBuf> {
|
||||
match self {
|
||||
Self::User(path) | Self::Project(path) => Some(path),
|
||||
Self::Internal => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_invalid_utf8(
|
||||
path: &AbsolutePathBuf,
|
||||
data: &[u8],
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_features::Feature;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::TempDirExt;
|
||||
use core_test_support::create_directory_symlink;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
@@ -16,6 +17,7 @@ async fn get_user_instructions(config: &Config) -> Option<String> {
|
||||
AgentsMdManager::new(config)
|
||||
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
|
||||
.await
|
||||
.map(|loaded| loaded.text())
|
||||
}
|
||||
|
||||
async fn agents_md_paths(config: &Config) -> std::io::Result<Vec<AbsolutePathBuf>> {
|
||||
@@ -53,7 +55,12 @@ async fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -
|
||||
config.cwd = root.abs();
|
||||
config.project_doc_max_bytes = limit;
|
||||
|
||||
config.user_instructions = instructions.map(ToOwned::to_owned);
|
||||
config.user_instructions = instructions.map(|text| {
|
||||
LoadedAgentsMd::new_user(
|
||||
text.to_owned(),
|
||||
config.codex_home.join(DEFAULT_AGENTS_MD_FILENAME),
|
||||
)
|
||||
});
|
||||
config
|
||||
}
|
||||
|
||||
@@ -96,7 +103,12 @@ async fn make_config_with_project_root_markers(
|
||||
|
||||
config.cwd = root.abs();
|
||||
config.project_doc_max_bytes = limit;
|
||||
config.user_instructions = instructions.map(ToOwned::to_owned);
|
||||
config.user_instructions = instructions.map(|text| {
|
||||
LoadedAgentsMd::new_user(
|
||||
text.to_owned(),
|
||||
config.codex_home.join(DEFAULT_AGENTS_MD_FILENAME),
|
||||
)
|
||||
});
|
||||
config
|
||||
}
|
||||
|
||||
@@ -115,17 +127,46 @@ 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;
|
||||
#[test]
|
||||
fn empty_loaded_instructions_are_empty() {
|
||||
let source =
|
||||
AbsolutePathBuf::from_absolute_path("/tmp/AGENTS.md").expect("absolute source path");
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
let res = AgentsMdManager::new(&config)
|
||||
.user_instructions(/*environment*/ None, &mut warnings)
|
||||
.await;
|
||||
assert_eq!(
|
||||
LoadedAgentsMd::new_user(String::new(), source.clone()),
|
||||
LoadedAgentsMd::default()
|
||||
);
|
||||
assert_eq!(
|
||||
LoadedAgentsMd::new_user(" \n\t".to_string(), source),
|
||||
LoadedAgentsMd::default()
|
||||
);
|
||||
assert_eq!(
|
||||
LoadedAgentsMd::from_text_for_testing(String::new()),
|
||||
LoadedAgentsMd::default()
|
||||
);
|
||||
assert_eq!(
|
||||
LoadedAgentsMd::from_text_for_testing(" \n\t"),
|
||||
LoadedAgentsMd::default()
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(res, None);
|
||||
#[test]
|
||||
fn loaded_instructions_with_only_empty_or_whitespace_entries_are_empty() {
|
||||
let empty = LoadedAgentsMd {
|
||||
entries: vec![InstructionEntry {
|
||||
contents: String::new(),
|
||||
provenance: InstructionProvenance::Internal,
|
||||
}],
|
||||
};
|
||||
let whitespace = LoadedAgentsMd {
|
||||
entries: vec![InstructionEntry {
|
||||
contents: " \n\t".to_string(),
|
||||
provenance: InstructionProvenance::Internal,
|
||||
}],
|
||||
};
|
||||
|
||||
assert!(empty.is_empty());
|
||||
assert!(whitespace.is_empty());
|
||||
}
|
||||
|
||||
/// Small file within the byte-limit is returned unmodified.
|
||||
@@ -161,8 +202,10 @@ async fn global_doc_invalid_utf8_warns_and_uses_lossy_text() {
|
||||
.await
|
||||
.expect("global doc expected");
|
||||
|
||||
assert_eq!(loaded.contents, "global\u{FFFD} doc");
|
||||
assert_eq!(loaded.path, path);
|
||||
assert_eq!(
|
||||
loaded,
|
||||
LoadedAgentsMd::new_user("global\u{FFFD} doc".to_string(), path.clone())
|
||||
);
|
||||
assert_invalid_utf8_warning(&warnings, "Global", path.as_path());
|
||||
}
|
||||
|
||||
@@ -177,11 +220,11 @@ async fn project_doc_invalid_utf8_warns_and_uses_lossy_text() {
|
||||
let res = AgentsMdManager::new(&config)
|
||||
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
.expect("doc expected")
|
||||
.text();
|
||||
|
||||
assert_eq!(res, "project\u{FFFD} doc");
|
||||
let canonical_path = dunce::canonicalize(&path).expect("canonical doc path");
|
||||
assert_invalid_utf8_warning(&warnings, "Project", &canonical_path);
|
||||
assert_invalid_utf8_warning(&warnings, "Project", config.cwd.join("AGENTS.md").as_path());
|
||||
}
|
||||
|
||||
/// Oversize file is truncated to `project_doc_max_bytes`.
|
||||
@@ -272,6 +315,30 @@ async fn merges_existing_instructions_with_agents_md() {
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sourceless_user_instructions_preserve_separator_without_reporting_a_source() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap();
|
||||
|
||||
let mut cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
|
||||
cfg.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
|
||||
"user instructions".to_string(),
|
||||
));
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
let loaded = AgentsMdManager::new(&cfg)
|
||||
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
let project_agents = cfg.cwd.join("AGENTS.md");
|
||||
|
||||
assert_eq!(
|
||||
loaded.text(),
|
||||
format!("user instructions{AGENTS_MD_SEPARATOR}project doc")
|
||||
);
|
||||
assert_eq!(loaded.sources().collect::<Vec<_>>(), vec![&project_agents]);
|
||||
}
|
||||
|
||||
/// If there are existing system instructions but AGENTS.md docs are
|
||||
/// missing we expect the original instructions to be returned unchanged.
|
||||
#[tokio::test]
|
||||
@@ -310,8 +377,32 @@ async fn concatenates_root_and_cwd_docs() {
|
||||
let mut cfg = make_config(&repo, /*limit*/ 4096, /*instructions*/ None).await;
|
||||
cfg.cwd = nested.abs();
|
||||
|
||||
let res = get_user_instructions(&cfg).await.expect("doc expected");
|
||||
assert_eq!(res, "root doc\n\ncrate doc");
|
||||
let mut warnings = Vec::new();
|
||||
let loaded = AgentsMdManager::new(&cfg)
|
||||
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
let root_agents = repo.path().join("AGENTS.md").abs();
|
||||
let crate_agents = cfg.cwd.join("AGENTS.md");
|
||||
let expected = LoadedAgentsMd {
|
||||
entries: vec![
|
||||
InstructionEntry {
|
||||
contents: "root doc".to_string(),
|
||||
provenance: InstructionProvenance::Project(root_agents.clone()),
|
||||
},
|
||||
InstructionEntry {
|
||||
contents: "crate doc".to_string(),
|
||||
provenance: InstructionProvenance::Project(crate_agents.clone()),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(loaded, expected);
|
||||
assert_eq!(loaded.text(), "root doc\n\ncrate doc");
|
||||
assert_eq!(
|
||||
loaded.sources().collect::<Vec<_>>(),
|
||||
vec![&root_agents, &crate_agents]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -334,14 +425,8 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
|
||||
cfg.cwd = nested.abs();
|
||||
|
||||
let discovery = agents_md_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("canonical child doc path"),
|
||||
)
|
||||
.expect("absolute child doc path");
|
||||
let expected_parent = root.path().join("AGENTS.md").abs();
|
||||
let expected_child = cfg.cwd.join("AGENTS.md");
|
||||
assert_eq!(discovery.len(), 2);
|
||||
assert_eq!(discovery[0], expected_parent);
|
||||
assert_eq!(discovery[1], expected_child);
|
||||
@@ -350,6 +435,58 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
|
||||
assert_eq!(res, "parent doc\n\nchild doc");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agents_md_paths_preserve_symlinked_cwd() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let target = tmp.path().join("target");
|
||||
fs::create_dir(&target).unwrap();
|
||||
fs::write(target.join("AGENTS.md"), "project doc").unwrap();
|
||||
|
||||
let linked_cwd = tmp.path().join("linked");
|
||||
create_directory_symlink(&target, &linked_cwd);
|
||||
|
||||
let mut cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
|
||||
cfg.cwd = linked_cwd.abs();
|
||||
|
||||
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
|
||||
assert_eq!(discovery, vec![cfg.cwd.join("AGENTS.md")]);
|
||||
|
||||
let res = get_user_instructions(&cfg).await.expect("doc expected");
|
||||
assert_eq!(res, "project doc");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn child_agents_message_after_global_instructions_uses_plain_separator() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let mut cfg = make_config(&tmp, /*limit*/ 4096, Some("global doc")).await;
|
||||
cfg.features.enable(Feature::ChildAgentsMd).unwrap();
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
let loaded = AgentsMdManager::new(&cfg)
|
||||
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
let global_agents = cfg.codex_home.join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
let expected = LoadedAgentsMd {
|
||||
entries: vec![
|
||||
InstructionEntry {
|
||||
contents: "global doc".to_string(),
|
||||
provenance: InstructionProvenance::User(global_agents),
|
||||
},
|
||||
InstructionEntry {
|
||||
contents: HIERARCHICAL_AGENTS_MESSAGE.to_string(),
|
||||
provenance: InstructionProvenance::Internal,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(loaded, expected);
|
||||
assert_eq!(
|
||||
loaded.text(),
|
||||
format!("global doc\n\n{HIERARCHICAL_AGENTS_MESSAGE}")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn instruction_sources_include_global_before_agents_md_docs() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
@@ -360,15 +497,79 @@ async fn instruction_sources_include_global_before_agents_md_docs() {
|
||||
fs::create_dir_all(&cfg.codex_home).unwrap();
|
||||
fs::write(&global_agents, "global doc").unwrap();
|
||||
|
||||
let sources = AgentsMdManager::new(&cfg)
|
||||
.instruction_sources(LOCAL_FS.as_ref())
|
||||
.await;
|
||||
let project_agents = AbsolutePathBuf::try_from(
|
||||
dunce::canonicalize(cfg.cwd.join("AGENTS.md")).expect("canonical project doc path"),
|
||||
)
|
||||
.expect("absolute project doc path");
|
||||
let mut warnings = Vec::new();
|
||||
let loaded = AgentsMdManager::new(&cfg)
|
||||
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
let project_agents = cfg.cwd.join("AGENTS.md");
|
||||
|
||||
assert_eq!(sources, vec![global_agents, project_agents]);
|
||||
let expected = LoadedAgentsMd {
|
||||
entries: vec![
|
||||
InstructionEntry {
|
||||
contents: "global doc".to_string(),
|
||||
provenance: InstructionProvenance::User(global_agents.clone()),
|
||||
},
|
||||
InstructionEntry {
|
||||
contents: "project doc".to_string(),
|
||||
provenance: InstructionProvenance::Project(project_agents.clone()),
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(loaded, expected);
|
||||
assert_eq!(
|
||||
loaded.sources().collect::<Vec<_>>(),
|
||||
vec![&global_agents, &project_agents]
|
||||
);
|
||||
assert_eq!(
|
||||
loaded.text(),
|
||||
format!("global doc{AGENTS_MD_SEPARATOR}project doc")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn child_agents_message_after_project_docs_is_not_an_instruction_source() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "project doc").unwrap();
|
||||
|
||||
let mut cfg = make_config(&tmp, /*limit*/ 4096, Some("global doc")).await;
|
||||
cfg.features.enable(Feature::ChildAgentsMd).unwrap();
|
||||
let global_agents = cfg.codex_home.join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
fs::create_dir_all(&cfg.codex_home).unwrap();
|
||||
fs::write(&global_agents, "global doc").unwrap();
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
let loaded = AgentsMdManager::new(&cfg)
|
||||
.user_instructions_with_fs(LOCAL_FS.as_ref(), &mut warnings)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
let project_agents = cfg.cwd.join("AGENTS.md");
|
||||
|
||||
let expected = LoadedAgentsMd {
|
||||
entries: vec![
|
||||
InstructionEntry {
|
||||
contents: "global doc".to_string(),
|
||||
provenance: InstructionProvenance::User(global_agents.clone()),
|
||||
},
|
||||
InstructionEntry {
|
||||
contents: "project doc".to_string(),
|
||||
provenance: InstructionProvenance::Project(project_agents.clone()),
|
||||
},
|
||||
InstructionEntry {
|
||||
contents: HIERARCHICAL_AGENTS_MESSAGE.to_string(),
|
||||
provenance: InstructionProvenance::Internal,
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(loaded, expected);
|
||||
assert_eq!(
|
||||
loaded.sources().collect::<Vec<_>>(),
|
||||
vec![&global_agents, &project_agents]
|
||||
);
|
||||
assert_eq!(
|
||||
loaded.text(),
|
||||
format!("global doc{AGENTS_MD_SEPARATOR}project doc\n\n{HIERARCHICAL_AGENTS_MESSAGE}")
|
||||
);
|
||||
}
|
||||
|
||||
/// AGENTS.override.md is preferred over AGENTS.md when both are present.
|
||||
|
||||
+13
-21
@@ -44,6 +44,7 @@ use codex_api::RawMemory as ApiRawMemory;
|
||||
use codex_api::RealtimeCallClient as ApiRealtimeCallClient;
|
||||
use codex_api::RealtimeSessionConfig as ApiRealtimeSessionConfig;
|
||||
use codex_api::Reasoning;
|
||||
use codex_api::ReasoningContext;
|
||||
use codex_api::RequestTelemetry;
|
||||
use codex_api::ReqwestTransport;
|
||||
use codex_api::ResponseCreateWsRequest;
|
||||
@@ -100,7 +101,6 @@ use tokio::sync::oneshot::error::TryRecvError;
|
||||
use tokio_tungstenite::tungstenite::Error;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::debug;
|
||||
use tracing::instrument;
|
||||
use tracing::trace;
|
||||
use tracing::warn;
|
||||
@@ -610,6 +610,7 @@ impl ModelClient {
|
||||
reasoning: effort.map(|effort| Reasoning {
|
||||
effort: Some(effort),
|
||||
summary: None,
|
||||
context: None,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -722,12 +723,17 @@ impl ModelClient {
|
||||
) -> Option<Reasoning> {
|
||||
if model_info.supports_reasoning_summaries {
|
||||
Some(Reasoning {
|
||||
effort: effort.or(model_info.default_reasoning_level),
|
||||
effort: effort.or_else(|| model_info.default_reasoning_level.clone()),
|
||||
summary: if summary == ReasoningSummaryConfig::None {
|
||||
None
|
||||
} else {
|
||||
Some(summary)
|
||||
},
|
||||
// When Responses Lite is disabled, omit context so Responses uses the default,
|
||||
// which is currently `current_turn`.
|
||||
context: model_info
|
||||
.use_responses_lite
|
||||
.then_some(ReasoningContext::AllTurns),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -776,7 +782,7 @@ impl ModelClient {
|
||||
input,
|
||||
tools,
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: prompt.parallel_tool_calls,
|
||||
parallel_tool_calls: prompt.parallel_tool_calls && !model_info.use_responses_lite,
|
||||
reasoning,
|
||||
store: provider.is_azure_responses_endpoint(),
|
||||
stream: true,
|
||||
@@ -967,18 +973,6 @@ impl ModelClientSession {
|
||||
.set_connection_reused(/*connection_reused*/ false);
|
||||
}
|
||||
|
||||
pub(crate) async fn send_response_processed(&self, response_id: &str) {
|
||||
let Some(connection) = self.websocket_session.connection.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = connection
|
||||
.send_response_processed(response_id.to_string())
|
||||
.await
|
||||
{
|
||||
debug!("failed to send response.processed websocket request: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// Builds shared Responses API transport options and request-body options.
|
||||
///
|
||||
@@ -1280,7 +1274,7 @@ impl ModelClientSession {
|
||||
&client_setup.api_provider,
|
||||
prompt,
|
||||
model_info,
|
||||
effort,
|
||||
effort.clone(),
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
)?;
|
||||
@@ -1389,7 +1383,7 @@ impl ModelClientSession {
|
||||
&client_setup.api_provider,
|
||||
prompt,
|
||||
model_info,
|
||||
effort,
|
||||
effort.clone(),
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
)?;
|
||||
@@ -1608,7 +1602,7 @@ impl ModelClientSession {
|
||||
prompt,
|
||||
model_info,
|
||||
session_telemetry,
|
||||
effort,
|
||||
effort.clone(),
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
@@ -1672,9 +1666,7 @@ fn parse_turn_metadata_header(turn_metadata_header: Option<&str>) -> Option<Head
|
||||
/// Meant to be called just before sending the request over the socket, to capture realistic
|
||||
/// transport timing.
|
||||
fn stamp_ws_stream_request_start_ms(request: &mut ResponsesWsRequest) {
|
||||
let ResponsesWsRequest::ResponseCreate(payload) = request else {
|
||||
return;
|
||||
};
|
||||
let ResponsesWsRequest::ResponseCreate(payload) = request;
|
||||
payload
|
||||
.client_metadata
|
||||
.get_or_insert_with(HashMap::new)
|
||||
|
||||
@@ -3,6 +3,7 @@ use codex_config::types::Personality;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_tools::ToolSpec;
|
||||
use futures::Stream;
|
||||
use serde_json::Value;
|
||||
@@ -53,7 +54,22 @@ impl Default for Prompt {
|
||||
|
||||
impl Prompt {
|
||||
pub(crate) fn get_formatted_input(&self) -> Vec<ResponseItem> {
|
||||
self.input.clone()
|
||||
self.input
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|item| {
|
||||
let ResponseItem::Message { role, content, .. } = &item else {
|
||||
return item;
|
||||
};
|
||||
if role != "assistant" {
|
||||
return item;
|
||||
}
|
||||
InterAgentCommunication::from_message_content(content)
|
||||
.filter(|communication| communication.encrypted_content.is_some())
|
||||
.map(|communication| communication.to_model_input_item())
|
||||
.unwrap_or(item)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,10 +70,51 @@ pub struct ThreadConfigSnapshot {
|
||||
pub personality: Option<Personality>,
|
||||
pub collaboration_mode: CollaborationMode,
|
||||
pub session_source: SessionSource,
|
||||
pub forked_from_thread_id: Option<ThreadId>,
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
pub thread_source: Option<ThreadSource>,
|
||||
}
|
||||
|
||||
/// Explains why `CodexThread::try_start_turn_if_idle` rejected an automatic
|
||||
/// idle turn.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TryStartTurnIfIdleRejectionReason {
|
||||
/// User/client-triggered mailbox work is already queued and must take
|
||||
/// priority over extension-initiated idle work.
|
||||
PendingTriggerTurn,
|
||||
/// The thread is in Plan mode, where automatic idle work must not start a
|
||||
/// new model turn.
|
||||
PlanMode,
|
||||
/// Another turn or task is active, or the idle reservation was lost before
|
||||
/// the automatic turn could start.
|
||||
Busy,
|
||||
}
|
||||
|
||||
/// Rejection returned when an extension asks to start automatic idle work but
|
||||
/// the thread is not eligible to run it.
|
||||
#[derive(Debug)]
|
||||
pub struct TryStartTurnIfIdleError {
|
||||
reason: TryStartTurnIfIdleRejectionReason,
|
||||
input: Vec<ResponseItem>,
|
||||
}
|
||||
|
||||
impl TryStartTurnIfIdleError {
|
||||
pub(crate) fn new(reason: TryStartTurnIfIdleRejectionReason, input: Vec<ResponseItem>) -> Self {
|
||||
Self { reason, input }
|
||||
}
|
||||
|
||||
/// Returns the stable reason the automatic idle turn was rejected.
|
||||
pub fn reason(&self) -> TryStartTurnIfIdleRejectionReason {
|
||||
self.reason
|
||||
}
|
||||
|
||||
/// Consumes the rejection and returns the original model-visible input
|
||||
/// unchanged, so callers can retry, drop, or log it explicitly.
|
||||
pub fn into_input(self) -> Vec<ResponseItem> {
|
||||
self.input
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadConfigSnapshot {
|
||||
pub fn sandbox_policy(&self) -> SandboxPolicy {
|
||||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
@@ -276,11 +317,23 @@ impl CodexThread {
|
||||
self.codex.session.inject_if_running(items).await
|
||||
}
|
||||
|
||||
/// Starts a regular turn with model-visible items only if the thread is idle.
|
||||
/// Starts an automatic regular turn with model-visible items only when idle
|
||||
/// work is allowed for this thread.
|
||||
///
|
||||
/// This is the required entry point for extensions that want to launch
|
||||
/// model-visible work from `ThreadLifecycleContributor::on_thread_idle`.
|
||||
/// The call succeeds only if no user/client-triggered turn is queued, no
|
||||
/// task is currently active, and the thread is not in Plan mode. Active
|
||||
/// Review tasks are rejected by the active-task check because Review turns
|
||||
/// are not steerable.
|
||||
///
|
||||
/// On rejection, the returned error includes a stable reason and carries
|
||||
/// the original `items` unchanged so the caller can decide whether to drop
|
||||
/// them, retry later, or log why no automatic turn was started.
|
||||
pub async fn try_start_turn_if_idle(
|
||||
&self,
|
||||
items: Vec<ResponseItem>,
|
||||
) -> Result<(), Vec<ResponseItem>> {
|
||||
) -> Result<(), TryStartTurnIfIdleError> {
|
||||
self.codex.session.try_start_turn_if_idle(items).await
|
||||
}
|
||||
|
||||
@@ -496,6 +549,11 @@ impl CodexThread {
|
||||
self.codex.thread_config_snapshot().await
|
||||
}
|
||||
|
||||
/// Returns the files that supplied the thread's loaded model instructions.
|
||||
pub async fn instruction_sources(&self) -> Vec<AbsolutePathBuf> {
|
||||
self.codex.instruction_sources().await
|
||||
}
|
||||
|
||||
pub async fn config(&self) -> Arc<crate::config::Config> {
|
||||
self.codex.session.get_config().await
|
||||
}
|
||||
|
||||
@@ -573,7 +573,7 @@ async fn drain_to_completed(
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
|
||||
@@ -242,7 +242,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
&prompt,
|
||||
&turn_context.model_info,
|
||||
CompactConversationRequestSettings {
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: turn_context.reasoning_summary,
|
||||
service_tier: if sess.services.auth_manager.auth_mode() == Some(AuthMode::ApiKey) {
|
||||
None
|
||||
@@ -348,6 +348,7 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
||||
}
|
||||
ResponseItem::Message { role, .. } if role == "assistant" => true,
|
||||
ResponseItem::Message { .. } => false,
|
||||
ResponseItem::AgentMessage { .. } => true,
|
||||
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
|
||||
ResponseItem::CompactionTrigger => false,
|
||||
ResponseItem::Reasoning { .. }
|
||||
|
||||
@@ -26,7 +26,6 @@ use codex_analytics::CompactionImplementation;
|
||||
use codex_analytics::CompactionPhase;
|
||||
use codex_analytics::CompactionReason;
|
||||
use codex_analytics::CompactionTrigger;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::items::ContextCompactionItem;
|
||||
@@ -281,7 +280,6 @@ async fn run_remote_compact_task_inner_impl(
|
||||
);
|
||||
let RemoteCompactionV2Output {
|
||||
compaction_output,
|
||||
response_id,
|
||||
token_usage,
|
||||
} = compaction_output_result?;
|
||||
if let Some(token_usage) = token_usage {
|
||||
@@ -314,18 +312,11 @@ async fn run_remote_compact_task_inner_impl(
|
||||
|
||||
sess.emit_turn_item_completed(turn_context, compaction_item)
|
||||
.await;
|
||||
if turn_context
|
||||
.features
|
||||
.enabled(Feature::ResponsesWebsocketResponseProcessed)
|
||||
{
|
||||
client_session.send_response_processed(&response_id).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct RemoteCompactionV2Output {
|
||||
compaction_output: ResponseItem,
|
||||
response_id: String,
|
||||
token_usage: Option<TokenUsage>,
|
||||
}
|
||||
|
||||
@@ -348,7 +339,7 @@ async fn run_remote_compaction_request_v2(
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
@@ -409,7 +400,7 @@ async fn collect_compaction_output(
|
||||
let mut output_item_count = 0usize;
|
||||
let mut compaction_count = 0usize;
|
||||
let mut compaction_output = None;
|
||||
let mut completed_response_id = None;
|
||||
let mut saw_completed = false;
|
||||
let mut completed_token_usage = None;
|
||||
while let Some(event) = stream.next().await {
|
||||
match event? {
|
||||
@@ -422,12 +413,8 @@ async fn collect_compaction_output(
|
||||
}
|
||||
}
|
||||
}
|
||||
ResponseEvent::Completed {
|
||||
response_id,
|
||||
token_usage,
|
||||
..
|
||||
} => {
|
||||
completed_response_id = Some(response_id);
|
||||
ResponseEvent::Completed { token_usage, .. } => {
|
||||
saw_completed = true;
|
||||
completed_token_usage = token_usage;
|
||||
break;
|
||||
}
|
||||
@@ -435,12 +422,12 @@ async fn collect_compaction_output(
|
||||
}
|
||||
}
|
||||
|
||||
let Some(response_id) = completed_response_id else {
|
||||
if !saw_completed {
|
||||
return Err(CodexErr::Stream(
|
||||
"remote compaction v2 stream closed before response.completed".to_string(),
|
||||
None,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
if compaction_count != 1 {
|
||||
return Err(CodexErr::Fatal(format!(
|
||||
@@ -453,7 +440,6 @@ async fn collect_compaction_output(
|
||||
};
|
||||
Ok(RemoteCompactionV2Output {
|
||||
compaction_output,
|
||||
response_id,
|
||||
token_usage: completed_token_usage,
|
||||
})
|
||||
}
|
||||
@@ -804,7 +790,6 @@ mod tests {
|
||||
.expect("compaction should be collected");
|
||||
|
||||
assert_eq!(output.compaction_output, compaction);
|
||||
assert_eq!(output.response_id, "resp-compact");
|
||||
assert_eq!(
|
||||
output.token_usage,
|
||||
Some(TokenUsage {
|
||||
|
||||
@@ -210,10 +210,8 @@ async fn load_config_normalizes_relative_cwd_override() -> std::io::Result<()> {
|
||||
#[tokio::test]
|
||||
async fn load_config_loads_global_agents_instructions() -> std::io::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(DEFAULT_AGENTS_MD_FILENAME),
|
||||
"\n global instructions \n",
|
||||
)?;
|
||||
let global_agents_path = codex_home.abs().join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
std::fs::write(&global_agents_path, "\n global instructions \n")?;
|
||||
|
||||
let mut config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
@@ -223,9 +221,14 @@ async fn load_config_loads_global_agents_instructions() -> std::io::Result<()> {
|
||||
.await?;
|
||||
let _ = config.features.enable(Feature::MemoryTool);
|
||||
|
||||
let user_instructions = config
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.expect("global instructions expected");
|
||||
assert_eq!(user_instructions.text(), "global instructions");
|
||||
assert_eq!(
|
||||
config.user_instructions.as_deref(),
|
||||
Some("global instructions")
|
||||
user_instructions.sources().collect::<Vec<_>>(),
|
||||
vec![&global_agents_path]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -237,7 +240,7 @@ async fn load_config_prefers_global_agents_override_instructions() -> std::io::R
|
||||
codex_home.path().join(DEFAULT_AGENTS_MD_FILENAME),
|
||||
"global instructions",
|
||||
)?;
|
||||
let global_agents_override_path = codex_home.path().join(LOCAL_AGENTS_MD_FILENAME);
|
||||
let global_agents_override_path = codex_home.abs().join(LOCAL_AGENTS_MD_FILENAME);
|
||||
std::fs::write(&global_agents_override_path, "local override instructions")?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
@@ -247,9 +250,14 @@ async fn load_config_prefers_global_agents_override_instructions() -> std::io::R
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user_instructions = config
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.expect("global override instructions expected");
|
||||
assert_eq!(user_instructions.text(), "local override instructions");
|
||||
assert_eq!(
|
||||
config.user_instructions.as_deref(),
|
||||
Some("local override instructions")
|
||||
user_instructions.sources().collect::<Vec<_>>(),
|
||||
vec![&global_agents_override_path]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -478,6 +486,32 @@ async fn load_config_resolves_experimental_request_user_input_enabled() -> std::
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_resolves_code_mode_config() -> std::io::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let config_toml: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
[features.code_mode]
|
||||
enabled = true
|
||||
excluded_tool_namespaces = ["mcp__codex_apps", "multi_agent_v1"]
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed");
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
config_toml,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.code_mode.excluded_tool_namespaces,
|
||||
vec!["mcp__codex_apps".to_string(), "multi_agent_v1".to_string()]
|
||||
);
|
||||
assert!(config.features.enabled(Feature::CodeMode));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_provider_auth_with_env_key() {
|
||||
let err = toml::from_str::<ConfigToml>(
|
||||
|
||||
@@ -225,7 +225,7 @@ impl ConfigDocument {
|
||||
);
|
||||
mutated |= self.write_optional_value(
|
||||
&["model_reasoning_effort"],
|
||||
effort.map(|effort| value(effort.to_string())),
|
||||
effort.as_ref().map(|effort| value(effort.to_string())),
|
||||
);
|
||||
mutated
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::agents_md::AgentsMdManager;
|
||||
pub use crate::agents_md::LoadedAgentsMd;
|
||||
use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::path_utils::normalize_for_native_workdir;
|
||||
@@ -59,6 +60,7 @@ use codex_core_plugins::PluginsConfigInput;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::AppsMcpPathOverrideConfigToml;
|
||||
use codex_features::CodeModeConfigToml;
|
||||
use codex_features::Feature;
|
||||
use codex_features::FeatureConfigSource;
|
||||
use codex_features::FeatureOverrides;
|
||||
@@ -200,6 +202,7 @@ All agents in the team, including the agents that you can assign tasks to, are e
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn.
|
||||
Child agents can also spawn their own sub-agents.
|
||||
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
|
||||
Use multi-agent capabilities only when there is a real reason to split the work; handle trivial or simple tasks directly.
|
||||
|
||||
You will receive messages in the analysis channel in the form:
|
||||
```
|
||||
@@ -216,6 +219,7 @@ You can spawn sub-agents to handle subtasks, and those sub-agents can spawn thei
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent.
|
||||
Child agents can also spawn their own sub-agents.
|
||||
Use multi-agent capabilities only when there is a real reason to split the work; handle trivial or simple tasks directly.
|
||||
|
||||
When you provide a response in the final channel, that content is immediately delivered back to your parent agent.
|
||||
|
||||
@@ -643,7 +647,7 @@ pub struct Config {
|
||||
pub show_raw_agent_reasoning: bool,
|
||||
|
||||
/// User-provided instructions from AGENTS.md.
|
||||
pub user_instructions: Option<String>,
|
||||
pub user_instructions: Option<LoadedAgentsMd>,
|
||||
|
||||
/// Base instructions override.
|
||||
pub base_instructions: Option<String>,
|
||||
@@ -979,6 +983,9 @@ pub struct Config {
|
||||
/// Whether to register the experimental request_user_input tool.
|
||||
pub experimental_request_user_input_enabled: bool,
|
||||
|
||||
/// Configuration for the experimental code-mode tool surface.
|
||||
pub code_mode: CodeModeConfig,
|
||||
|
||||
/// If set to `true`, used only the experimental unified exec tool.
|
||||
pub use_experimental_unified_exec_tool: bool,
|
||||
|
||||
@@ -1031,6 +1038,11 @@ pub struct Config {
|
||||
pub otel: codex_config::types::OtelConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct CodeModeConfig {
|
||||
pub excluded_tool_namespaces: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct MultiAgentV2Config {
|
||||
pub max_concurrent_threads_per_session: usize,
|
||||
@@ -2292,6 +2304,17 @@ fn resolve_experimental_request_user_input_enabled(config_toml: &ConfigToml) ->
|
||||
.is_none_or(|config| config.enabled)
|
||||
}
|
||||
|
||||
fn resolve_code_mode_config(config_toml: &ConfigToml) -> CodeModeConfig {
|
||||
let base = code_mode_toml_config(config_toml.features.as_ref());
|
||||
|
||||
CodeModeConfig {
|
||||
excluded_tool_namespaces: base
|
||||
.and_then(|config| config.excluded_tool_namespaces.as_ref())
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config {
|
||||
let base = multi_agent_v2_toml_config(config_toml.features.as_ref());
|
||||
let default = MultiAgentV2Config::default();
|
||||
@@ -2374,6 +2397,13 @@ fn resolve_optional_prompt_text(
|
||||
}
|
||||
}
|
||||
|
||||
fn code_mode_toml_config(features: Option<&FeaturesToml>) -> Option<&CodeModeConfigToml> {
|
||||
match features?.code_mode.as_ref()? {
|
||||
FeatureToml::Enabled(_) => None,
|
||||
FeatureToml::Config(config) => Some(config),
|
||||
}
|
||||
}
|
||||
|
||||
fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiAgentV2ConfigToml> {
|
||||
match features?.multi_agent_v2.as_ref()? {
|
||||
FeatureToml::Enabled(_) => None,
|
||||
@@ -2580,8 +2610,7 @@ impl Config {
|
||||
Some(&codex_home),
|
||||
&mut startup_warnings,
|
||||
)
|
||||
.await
|
||||
.map(|loaded| loaded.contents);
|
||||
.await;
|
||||
|
||||
// Destructure ConfigOverrides fully to ensure all overrides are applied.
|
||||
let ConfigOverrides {
|
||||
@@ -3015,6 +3044,7 @@ impl Config {
|
||||
let web_search_config = resolve_web_search_config(&cfg);
|
||||
let experimental_request_user_input_enabled =
|
||||
resolve_experimental_request_user_input_enabled(&cfg);
|
||||
let code_mode = resolve_code_mode_config(&cfg);
|
||||
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg);
|
||||
let apps_mcp_path_override = if features.enabled(Feature::AppsMcpPathOverride) {
|
||||
let base = apps_mcp_path_override_toml_config(cfg.features.as_ref());
|
||||
@@ -3566,6 +3596,7 @@ impl Config {
|
||||
web_search_mode: constrained_web_search_mode.value,
|
||||
web_search_config,
|
||||
experimental_request_user_input_enabled,
|
||||
code_mode,
|
||||
use_experimental_unified_exec_tool,
|
||||
background_terminal_max_timeout,
|
||||
ghost_snapshot,
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
use super::ContextualUserFragment;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// Maximum size of the extension's model-facing generated-image path hint.
|
||||
const MAX_IMAGE_GENERATION_OUTPUT_HINT_BYTES: usize = 1024;
|
||||
|
||||
/// Returns the extension's model-facing hint, or omits it if the path makes it too large.
|
||||
pub fn extension_image_generation_output_hint(
|
||||
image_output_dir: impl Display,
|
||||
image_output_path: impl Display,
|
||||
) -> Option<String> {
|
||||
let hint = image_generation_hint(image_output_dir, image_output_path);
|
||||
(hint.len() <= MAX_IMAGE_GENERATION_OUTPUT_HINT_BYTES).then_some(hint)
|
||||
}
|
||||
|
||||
fn image_generation_hint(
|
||||
image_output_dir: impl Display,
|
||||
image_output_path: impl Display,
|
||||
) -> String {
|
||||
format!(
|
||||
"Generated images are saved to {image_output_dir} as {image_output_path} 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."
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ImageGenerationInstructions {
|
||||
image_output_dir: String,
|
||||
@@ -30,9 +51,6 @@ impl ContextualUserFragment for ImageGenerationInstructions {
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
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.",
|
||||
self.image_output_dir, self.image_output_path
|
||||
)
|
||||
image_generation_hint(&self.image_output_dir, &self.image_output_path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ pub(crate) use environment_context::EnvironmentContext;
|
||||
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
|
||||
pub(crate) use hook_additional_context::HookAdditionalContext;
|
||||
pub(crate) use image_generation_instructions::ImageGenerationInstructions;
|
||||
pub use image_generation_instructions::extension_image_generation_output_hint;
|
||||
pub use internal_model_context::InternalContextSource;
|
||||
pub use internal_model_context::InternalModelContextFragment;
|
||||
pub use internal_model_context::InvalidInternalContextSource;
|
||||
|
||||
@@ -386,6 +386,7 @@ impl ContextManager {
|
||||
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
||||
},
|
||||
ResponseItem::Message { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
@@ -474,7 +475,8 @@ pub(crate) fn truncate_function_output_payload(
|
||||
fn is_api_message(message: &ResponseItem) -> bool {
|
||||
match message {
|
||||
ResponseItem::Message { role, .. } => role.as_str() != "system",
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
@@ -720,11 +722,15 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Other => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_user_turn_boundary(item: &ResponseItem) -> bool {
|
||||
if matches!(item, ResponseItem::AgentMessage { .. }) {
|
||||
return true;
|
||||
}
|
||||
let ResponseItem::Message { role, content, .. } = item else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,6 @@ use codex_execpolicy::blocking_append_network_rule;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_shell_command::is_dangerous_command::command_might_be_dangerous;
|
||||
use codex_shell_command::is_safe_command::is_known_safe_command;
|
||||
@@ -121,7 +120,6 @@ pub(crate) enum ExecPolicyCommandOrigin {
|
||||
pub(crate) struct UnmatchedCommandContext<'a> {
|
||||
pub(crate) approval_policy: AskForApproval,
|
||||
pub(crate) permission_profile: &'a PermissionProfile,
|
||||
pub(crate) file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
pub(crate) sandbox_cwd: &'a Path,
|
||||
pub(crate) sandbox_permissions: SandboxPermissions,
|
||||
pub(crate) used_complex_parsing: bool,
|
||||
@@ -242,7 +240,6 @@ pub(crate) struct ExecApprovalRequest<'a> {
|
||||
pub(crate) command: &'a [String],
|
||||
pub(crate) approval_policy: AskForApproval,
|
||||
pub(crate) permission_profile: PermissionProfile,
|
||||
pub(crate) file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
pub(crate) sandbox_cwd: &'a Path,
|
||||
pub(crate) sandbox_permissions: SandboxPermissions,
|
||||
pub(crate) prefix_rule: Option<Vec<String>>,
|
||||
@@ -277,7 +274,6 @@ impl ExecPolicyManager {
|
||||
command,
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
sandbox_permissions,
|
||||
prefix_rule,
|
||||
@@ -298,7 +294,6 @@ impl ExecPolicyManager {
|
||||
UnmatchedCommandContext {
|
||||
approval_policy,
|
||||
permission_profile: &permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
sandbox_permissions,
|
||||
used_complex_parsing,
|
||||
@@ -636,12 +631,12 @@ pub(crate) fn render_decision_for_unmatched_command(
|
||||
let UnmatchedCommandContext {
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
sandbox_permissions,
|
||||
used_complex_parsing,
|
||||
command_origin,
|
||||
} = context;
|
||||
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
|
||||
let is_known_safe = match command_origin {
|
||||
ExecPolicyCommandOrigin::Generic => is_known_safe_command(command),
|
||||
#[cfg(windows)]
|
||||
@@ -652,12 +647,8 @@ pub(crate) fn render_decision_for_unmatched_command(
|
||||
|
||||
// On Windows, ReadOnly sandbox is not a real sandbox, so special-case it
|
||||
// here.
|
||||
let environment_lacks_sandbox_protections = cfg!(windows)
|
||||
&& profile_is_managed_read_only(
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
);
|
||||
let environment_lacks_sandbox_protections =
|
||||
cfg!(windows) && profile_is_managed_read_only(permission_profile, sandbox_cwd);
|
||||
|
||||
if is_known_safe
|
||||
&& !used_complex_parsing
|
||||
@@ -751,9 +742,9 @@ pub(crate) fn render_decision_for_unmatched_command(
|
||||
|
||||
fn profile_is_managed_read_only(
|
||||
permission_profile: &PermissionProfile,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_cwd: &Path,
|
||||
) -> bool {
|
||||
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
|
||||
matches!(permission_profile, PermissionProfile::Managed { .. })
|
||||
&& matches!(
|
||||
file_system_sandbox_policy.kind,
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -104,31 +105,6 @@ async fn write_project_trust_config(
|
||||
.await
|
||||
}
|
||||
|
||||
fn read_only_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
}])
|
||||
}
|
||||
|
||||
fn workspace_write_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
FileSystemSandboxPolicy::workspace_write(
|
||||
&[],
|
||||
/*exclude_tmpdir_env_var*/ false,
|
||||
/*exclude_slash_tmp*/ false,
|
||||
)
|
||||
}
|
||||
|
||||
fn unrestricted_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
FileSystemSandboxPolicy::unrestricted()
|
||||
}
|
||||
|
||||
fn external_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
FileSystemSandboxPolicy::external_sandbox()
|
||||
}
|
||||
|
||||
async fn test_config() -> (TempDir, Config) {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
@@ -665,7 +641,6 @@ async fn evaluates_bash_lc_inner_commands() {
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -761,7 +736,6 @@ async fn evaluates_heredoc_script_against_prefix_rules() {
|
||||
command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -785,7 +759,6 @@ async fn omits_auto_amendment_for_heredoc_fallback_prompts() {
|
||||
],
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -809,7 +782,6 @@ async fn drops_requested_amendment_for_heredoc_fallback_prompts_when_it_wont_mat
|
||||
],
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: Some(vec![
|
||||
"python3".to_string(),
|
||||
@@ -837,7 +809,6 @@ async fn drops_requested_amendment_for_heredoc_fallback_prompts_when_it_matches(
|
||||
],
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: Some(vec!["python3".to_string()]),
|
||||
},
|
||||
@@ -862,7 +833,6 @@ async fn heredoc_with_variable_assignment_is_not_reduced_to_allowed_prefix() {
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -893,7 +863,6 @@ EOF"#
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
file_system_sandbox_policy: workspace_write_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -927,7 +896,6 @@ EOF"#
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
file_system_sandbox_policy: workspace_write_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -967,7 +935,6 @@ prefix_rule(
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -986,7 +953,6 @@ async fn exec_approval_requirement_prefers_execpolicy_match() {
|
||||
command: vec!["rm".to_string()],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1014,7 +980,6 @@ prefix_rule(pattern=["git"], decision="allow")
|
||||
command: vec![git_path, "status".to_string()],
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1048,7 +1013,6 @@ prefix_rule(pattern=["git"], decision="prompt")
|
||||
command: vec![disallowed_git_path.clone(), "status".to_string()],
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1075,7 +1039,6 @@ async fn requested_prefix_rule_can_approve_absolute_path_commands() {
|
||||
],
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
|
||||
},
|
||||
@@ -1098,7 +1061,6 @@ async fn exec_approval_requirement_respects_approval_policy() {
|
||||
command: vec!["rm".to_string()],
|
||||
approval_policy: AskForApproval::Never,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1126,7 +1088,6 @@ fn unmatched_granular_policy_still_prompts_for_restricted_sandbox_escalation() {
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
permission_profile: &PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
used_complex_parsing: false,
|
||||
@@ -1137,9 +1098,8 @@ fn unmatched_granular_policy_still_prompts_for_restricted_sandbox_escalation() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmatched_on_request_uses_split_filesystem_policy_for_escalation_prompts() {
|
||||
fn unmatched_on_request_uses_permission_profile_file_system_policy_for_escalation_prompts() {
|
||||
let command = vec!["madeup-cmd".to_string()];
|
||||
let restricted_file_system_policy = FileSystemSandboxPolicy::restricted(vec![]);
|
||||
|
||||
assert_eq!(
|
||||
Decision::Prompt,
|
||||
@@ -1147,8 +1107,7 @@ fn unmatched_on_request_uses_split_filesystem_policy_for_escalation_prompts() {
|
||||
&command,
|
||||
UnmatchedCommandContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: &PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: &restricted_file_system_policy,
|
||||
permission_profile: &PermissionProfile::read_only(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
used_complex_parsing: false,
|
||||
@@ -1169,7 +1128,6 @@ fn known_safe_on_request_still_prompts_for_restricted_sandbox_escalation() {
|
||||
UnmatchedCommandContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: &PermissionProfile::workspace_write(),
|
||||
file_system_sandbox_policy: &workspace_write_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
used_complex_parsing: false,
|
||||
@@ -1202,7 +1160,6 @@ fn managed_cwd_write_profile_is_not_read_only() {
|
||||
|
||||
assert!(!profile_is_managed_read_only(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/tmp/project")
|
||||
));
|
||||
}
|
||||
@@ -1233,7 +1190,6 @@ fn managed_unresolvable_write_profile_is_still_read_only() {
|
||||
|
||||
assert!(profile_is_managed_read_only(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/tmp/project")
|
||||
));
|
||||
}
|
||||
@@ -1250,7 +1206,6 @@ async fn exec_approval_requirement_prompts_for_inline_additional_permissions_und
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1273,7 +1228,6 @@ async fn exec_approval_requirement_prompts_for_known_safe_escalation_under_on_re
|
||||
command: vec!["echo".to_string(), "hello".to_string()],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
file_system_sandbox_policy: workspace_write_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1303,7 +1257,6 @@ async fn exec_approval_requirement_rejects_known_safe_escalation_when_granular_s
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
file_system_sandbox_policy: workspace_write_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1329,7 +1282,6 @@ async fn exec_approval_requirement_rejects_unmatched_sandbox_escalation_when_gra
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1365,7 +1317,6 @@ async fn mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision()
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
@@ -1403,7 +1354,6 @@ async fn mixed_rule_and_sandbox_prompt_rejects_when_granular_rules_are_disabled(
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
@@ -1428,7 +1378,6 @@ async fn exec_approval_requirement_falls_back_to_heuristics() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
@@ -1454,7 +1403,6 @@ async fn empty_bash_lc_script_falls_back_to_original_command() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
@@ -1484,7 +1432,6 @@ async fn whitespace_bash_lc_script_falls_back_to_original_command() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
@@ -1514,7 +1461,6 @@ async fn request_rule_uses_prefix_rule() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
|
||||
@@ -1547,7 +1493,6 @@ async fn request_rule_falls_back_when_prefix_rule_does_not_approve_all_commands(
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
|
||||
@@ -1587,7 +1532,6 @@ async fn heuristics_apply_when_other_commands_match_policy() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
@@ -1663,7 +1607,6 @@ async fn proposed_execpolicy_amendment_is_present_for_single_command_without_pol
|
||||
command: command.clone(),
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1683,7 +1626,6 @@ async fn proposed_execpolicy_amendment_is_omitted_when_policy_prompts() {
|
||||
command: vec!["rm".to_string()],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1707,7 +1649,6 @@ async fn proposed_execpolicy_amendment_is_present_for_multi_command_scripts() {
|
||||
],
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1737,7 +1678,6 @@ async fn proposed_execpolicy_amendment_uses_first_no_match_in_multi_command_scri
|
||||
command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1761,7 +1701,6 @@ async fn proposed_execpolicy_amendment_is_present_when_heuristics_allow() {
|
||||
command: command.clone(),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1785,7 +1724,6 @@ async fn proposed_execpolicy_amendment_is_suppressed_when_policy_matches_allow()
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1816,7 +1754,6 @@ prefix_rule(pattern=["cat"], decision="allow")
|
||||
command: command.clone(),
|
||||
approval_policy,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
file_system_sandbox_policy: workspace_write_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1848,7 +1785,6 @@ prefix_rule(pattern=["bash"], decision="allow")
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -2014,7 +1950,6 @@ async fn dangerous_rm_rf_requires_approval_in_danger_full_access() {
|
||||
command: command.clone(),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -2078,7 +2013,6 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
|
||||
command: &sneaky_command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: permissions,
|
||||
prefix_rule: None,
|
||||
@@ -2103,7 +2037,6 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
|
||||
command: &dangerous_command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: permissions,
|
||||
prefix_rule: None,
|
||||
@@ -2124,7 +2057,6 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
|
||||
command: &dangerous_command,
|
||||
approval_policy: AskForApproval::Never,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: permissions,
|
||||
prefix_rule: None,
|
||||
@@ -2146,7 +2078,6 @@ async fn dangerous_command_allowed_when_sandbox_is_explicitly_disabled() {
|
||||
permission_profile: PermissionProfile::External {
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
file_system_sandbox_policy: external_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -2171,7 +2102,6 @@ async fn dangerous_command_forbidden_in_external_sandbox_when_policy_matches() {
|
||||
permission_profile: PermissionProfile::External {
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
file_system_sandbox_policy: external_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -2188,7 +2118,6 @@ struct ExecApprovalRequirementScenario {
|
||||
command: Vec<String>,
|
||||
approval_policy: AskForApproval,
|
||||
permission_profile: PermissionProfile,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
prefix_rule: Option<Vec<String>>,
|
||||
}
|
||||
@@ -2212,7 +2141,6 @@ async fn exec_approval_requirement_for_command(
|
||||
command,
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_permissions,
|
||||
prefix_rule,
|
||||
} = test;
|
||||
@@ -2224,7 +2152,6 @@ async fn exec_approval_requirement_for_command(
|
||||
command: &command,
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions,
|
||||
prefix_rule,
|
||||
|
||||
@@ -15,7 +15,6 @@ async fn evaluates_powershell_inner_commands_against_prompt_rules() {
|
||||
],
|
||||
approval_policy: AskForApproval::Never,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -39,7 +38,6 @@ async fn evaluates_powershell_inner_commands_against_allow_rules() {
|
||||
],
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -81,7 +79,6 @@ fn unmatched_safe_powershell_words_are_allowed() {
|
||||
UnmatchedCommandContext {
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: &PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
used_complex_parsing: false,
|
||||
@@ -110,7 +107,6 @@ async fn unmatched_dangerous_powershell_inner_commands_require_approval() {
|
||||
],
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
|
||||
@@ -702,7 +702,7 @@ pub(super) async fn run_guardian_review_session(
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.any(|effort| effort.effort == codex_protocol::openai_models::ReasoningEffort::Low),
|
||||
Some(preset.default_reasoning_effort),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
);
|
||||
(review_model_id.to_string(), reasoning_effort)
|
||||
} else {
|
||||
@@ -712,7 +712,8 @@ pub(super) async fn run_guardian_review_session(
|
||||
.iter()
|
||||
.any(|preset| preset.effort == codex_protocol::openai_models::ReasoningEffort::Low),
|
||||
turn.reasoning_effort
|
||||
.or(turn.model_info.default_reasoning_level),
|
||||
.clone()
|
||||
.or_else(|| turn.model_info.default_reasoning_level.clone()),
|
||||
);
|
||||
(
|
||||
model_override
|
||||
@@ -725,7 +726,7 @@ pub(super) async fn run_guardian_review_session(
|
||||
turn.config.as_ref(),
|
||||
live_network_config.clone(),
|
||||
guardian_model.as_str(),
|
||||
guardian_reasoning_effort,
|
||||
guardian_reasoning_effort.clone(),
|
||||
);
|
||||
let guardian_config = match guardian_config {
|
||||
Ok(config) => config,
|
||||
|
||||
@@ -29,6 +29,7 @@ use tokio::sync::Semaphore;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::LoadedAgentsMd;
|
||||
use crate::codex_delegate::run_codex_thread_interactive;
|
||||
use crate::config::Config;
|
||||
use crate::config::Constrained;
|
||||
@@ -146,7 +147,7 @@ struct GuardianReviewSessionReuseKey {
|
||||
permissions: Permissions,
|
||||
developer_instructions: Option<String>,
|
||||
base_instructions: Option<String>,
|
||||
user_instructions: Option<String>,
|
||||
user_instructions: Option<LoadedAgentsMd>,
|
||||
compact_prompt: Option<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
mcp_servers: Constrained<HashMap<String, McpServerConfig>>,
|
||||
@@ -166,7 +167,7 @@ impl GuardianReviewSessionReuseKey {
|
||||
model_context_window: spawn_config.model_context_window,
|
||||
model_auto_compact_token_limit: spawn_config.model_auto_compact_token_limit,
|
||||
model_auto_compact_token_limit_scope: spawn_config.model_auto_compact_token_limit_scope,
|
||||
model_reasoning_effort: spawn_config.model_reasoning_effort,
|
||||
model_reasoning_effort: spawn_config.model_reasoning_effort.clone(),
|
||||
model_reasoning_summary: spawn_config.model_reasoning_summary,
|
||||
permissions: spawn_config.permissions.clone(),
|
||||
developer_instructions: spawn_config.developer_instructions.clone(),
|
||||
@@ -657,7 +658,8 @@ async fn run_review_on_session(
|
||||
let guardian_reasoning_effort = if model_info.supports_reasoning_summaries {
|
||||
params
|
||||
.reasoning_effort
|
||||
.or(model_info.default_reasoning_level)
|
||||
.clone()
|
||||
.or_else(|| model_info.default_reasoning_level.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -741,7 +743,7 @@ async fn run_review_on_session(
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: params.model.clone(),
|
||||
reasoning_effort: params.reasoning_effort,
|
||||
reasoning_effort: params.reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
@@ -1106,7 +1108,7 @@ mod tests {
|
||||
async fn test_review_params() -> GuardianReviewSessionParams {
|
||||
let (session, turn) = crate::session::tests::make_session_and_context().await;
|
||||
let model = turn.model_info.slug.clone();
|
||||
let reasoning_effort = turn.reasoning_effort;
|
||||
let reasoning_effort = turn.reasoning_effort.clone();
|
||||
let reasoning_summary = turn.reasoning_summary;
|
||||
let personality = turn.personality;
|
||||
#[allow(deprecated)]
|
||||
@@ -1115,7 +1117,7 @@ mod tests {
|
||||
turn.config.as_ref(),
|
||||
/*live_network_config*/ None,
|
||||
model.as_str(),
|
||||
reasoning_effort,
|
||||
reasoning_effort.clone(),
|
||||
)
|
||||
.expect("guardian config");
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ mod config_lock;
|
||||
pub use codex_thread::CodexThread;
|
||||
pub use codex_thread::CodexThreadSettingsOverrides;
|
||||
pub use codex_thread::ThreadConfigSnapshot;
|
||||
pub use codex_thread::TryStartTurnIfIdleError;
|
||||
pub use codex_thread::TryStartTurnIfIdleRejectionReason;
|
||||
pub use session::turn_context::TurnContext;
|
||||
mod agent;
|
||||
mod attestation;
|
||||
@@ -81,7 +83,6 @@ mod sandbox_tags;
|
||||
pub mod sandboxing;
|
||||
mod session_prefix;
|
||||
mod session_startup_prewarm;
|
||||
mod shell_detect;
|
||||
pub mod skills;
|
||||
pub(crate) use skills::SkillInjections;
|
||||
pub(crate) use skills::SkillLoadOutcome;
|
||||
@@ -97,6 +98,7 @@ pub(crate) use skills::manager;
|
||||
pub(crate) use skills::maybe_emit_implicit_skill_invocation;
|
||||
pub(crate) use skills::skills_load_input_from_config;
|
||||
mod stream_events_utils;
|
||||
pub use stream_events_utils::image_generation_artifact_path;
|
||||
pub mod test_support;
|
||||
mod unified_exec;
|
||||
pub mod windows_sandbox;
|
||||
@@ -128,6 +130,7 @@ pub(crate) mod agents_md;
|
||||
pub use agents_md::AgentsMdManager;
|
||||
pub use agents_md::DEFAULT_AGENTS_MD_FILENAME;
|
||||
pub use agents_md::LOCAL_AGENTS_MD_FILENAME;
|
||||
pub use agents_md::LoadedAgentsMd;
|
||||
mod rollout;
|
||||
pub(crate) mod safety;
|
||||
mod session_rollout_init_error;
|
||||
|
||||
@@ -129,7 +129,7 @@ fn save_config_resolved_fields(
|
||||
) -> anyhow::Result<()> {
|
||||
lock_config.web_search = Some(config.web_search_mode.value());
|
||||
lock_config.model_provider = Some(config.model_provider_id.clone());
|
||||
lock_config.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort;
|
||||
lock_config.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort.clone();
|
||||
lock_config.model_verbosity = config.model_verbosity;
|
||||
lock_config.include_permissions_instructions = Some(config.include_permissions_instructions);
|
||||
lock_config.include_apps_instructions = Some(config.include_apps_instructions);
|
||||
@@ -251,6 +251,14 @@ mod tests {
|
||||
spec.key
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
features.code_mode,
|
||||
Some(FeatureToml::Enabled(
|
||||
sc.original_config_do_not_use
|
||||
.features
|
||||
.enabled(Feature::CodeMode)
|
||||
))
|
||||
);
|
||||
|
||||
let multi_agent_v2 = features
|
||||
.multi_agent_v2
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use super::input_queue::TurnInput;
|
||||
use super::session::Session;
|
||||
use super::turn_context::TurnContext;
|
||||
use crate::codex_thread::TryStartTurnIfIdleError;
|
||||
use crate::codex_thread::TryStartTurnIfIdleRejectionReason;
|
||||
use crate::state::ActiveTurn;
|
||||
use crate::state::TurnState;
|
||||
use crate::tasks::RegularTask;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -32,22 +35,40 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a regular turn with the provided items only if the session is idle.
|
||||
/// Starts a regular turn with the provided items only if automatic idle work
|
||||
/// is allowed for the current session state.
|
||||
///
|
||||
/// This is the shared gate for extension-initiated idle work. It refuses to
|
||||
/// start a turn when user/client-triggered work is queued, any task is still
|
||||
/// active, or the session is currently in Plan mode. Active Review tasks are
|
||||
/// covered by the active-task check because Review turns are not steerable.
|
||||
pub(crate) async fn try_start_turn_if_idle(
|
||||
self: &Arc<Self>,
|
||||
input: Vec<ResponseItem>,
|
||||
) -> Result<(), Vec<ResponseItem>> {
|
||||
) -> Result<(), TryStartTurnIfIdleError> {
|
||||
if input.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if self.input_queue.has_trigger_turn_mailbox_items().await {
|
||||
return Err(input);
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
|
||||
input,
|
||||
));
|
||||
}
|
||||
if self.collaboration_mode().await.mode == ModeKind::Plan {
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PlanMode,
|
||||
input,
|
||||
));
|
||||
}
|
||||
|
||||
let turn_state = {
|
||||
let mut active_turn = self.active_turn.lock().await;
|
||||
if active_turn.is_some() {
|
||||
return Err(input);
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::Busy,
|
||||
input,
|
||||
));
|
||||
}
|
||||
let active_turn = active_turn.get_or_insert_with(ActiveTurn::default);
|
||||
Arc::clone(&active_turn.turn_state)
|
||||
@@ -56,18 +77,32 @@ impl Session {
|
||||
if self.input_queue.has_trigger_turn_mailbox_items().await {
|
||||
self.clear_reserved_idle_turn(&turn_state).await;
|
||||
self.maybe_start_turn_for_pending_work().await;
|
||||
return Err(input);
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
|
||||
input,
|
||||
));
|
||||
}
|
||||
|
||||
let turn_context = self
|
||||
.new_default_turn_with_sub_id(uuid::Uuid::new_v4().to_string())
|
||||
.await;
|
||||
if turn_context.collaboration_mode.mode == ModeKind::Plan {
|
||||
self.clear_reserved_idle_turn(&turn_state).await;
|
||||
self.maybe_start_turn_for_pending_work().await;
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PlanMode,
|
||||
input,
|
||||
));
|
||||
}
|
||||
self.maybe_emit_unknown_model_warning_for_turn(turn_context.as_ref())
|
||||
.await;
|
||||
if self.input_queue.has_trigger_turn_mailbox_items().await {
|
||||
self.clear_reserved_idle_turn(&turn_state).await;
|
||||
self.maybe_start_turn_for_pending_work().await;
|
||||
return Err(input);
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
|
||||
input,
|
||||
));
|
||||
}
|
||||
let still_reserved = {
|
||||
let active_turn = self.active_turn.lock().await;
|
||||
@@ -77,7 +112,10 @@ impl Session {
|
||||
};
|
||||
if !still_reserved {
|
||||
self.clear_reserved_idle_turn(&turn_state).await;
|
||||
return Err(input);
|
||||
return Err(TryStartTurnIfIdleError::new(
|
||||
TryStartTurnIfIdleRejectionReason::Busy,
|
||||
input,
|
||||
));
|
||||
}
|
||||
|
||||
self.input_queue
|
||||
|
||||
@@ -359,6 +359,7 @@ use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TurnModerationMetadataEvent;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_tools::ToolEnvironmentMode;
|
||||
@@ -507,12 +508,13 @@ impl Codex {
|
||||
|
||||
let primary_environment = environment_selections.primary_environment();
|
||||
let mut user_instruction_warnings = Vec::new();
|
||||
let user_instructions = AgentsMdManager::new(&config)
|
||||
.user_instructions(
|
||||
primary_environment.as_deref(),
|
||||
&mut user_instruction_warnings,
|
||||
)
|
||||
.await;
|
||||
let user_instructions = if let Some(primary_environment) = primary_environment {
|
||||
AgentsMdManager::new(&config)
|
||||
.user_instructions(primary_environment.as_ref(), &mut user_instruction_warnings)
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
config.startup_warnings.extend(user_instruction_warnings);
|
||||
|
||||
let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) {
|
||||
@@ -578,7 +580,7 @@ impl Codex {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model: model.clone(),
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
@@ -800,6 +802,17 @@ impl Codex {
|
||||
state.session_configuration.thread_config_snapshot()
|
||||
}
|
||||
|
||||
pub(crate) async fn instruction_sources(&self) -> Vec<AbsolutePathBuf> {
|
||||
let state = self.session.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |instructions| {
|
||||
instructions.sources().cloned().collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
|
||||
let state = self.session.state.lock().await;
|
||||
state.session_configuration.environments.clone()
|
||||
@@ -2625,6 +2638,15 @@ impl Session {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn emit_turn_moderation_metadata(
|
||||
self: &Arc<Self>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
metadata: TurnModerationMetadataEvent,
|
||||
) {
|
||||
self.send_event(turn_context, EventMsg::TurnModerationMetadata(metadata))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn replace_history(
|
||||
&self,
|
||||
items: Vec<ResponseItem>,
|
||||
@@ -3364,6 +3386,9 @@ pub(crate) fn emit_subagent_session_started(
|
||||
session_id: session_id.to_string(),
|
||||
thread_id: thread_id.to_string(),
|
||||
parent_thread_id: parent_thread_id.map(|thread_id| thread_id.to_string()),
|
||||
forked_from_thread_id: thread_config
|
||||
.forked_from_thread_id
|
||||
.map(|thread_id| thread_id.to_string()),
|
||||
product_client_id: client_name.clone(),
|
||||
client_name,
|
||||
client_version,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_core_skills::HostLoadedSkills;
|
||||
use codex_protocol::openai_models::ToolMode;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
@@ -73,7 +74,7 @@ pub(super) async fn spawn_review_thread(
|
||||
let auth_manager_for_context = auth_manager.clone();
|
||||
let provider_for_context = provider.clone();
|
||||
let session_telemetry_for_context = session_telemetry.clone();
|
||||
let reasoning_effort = per_turn_config.model_reasoning_effort;
|
||||
let reasoning_effort = per_turn_config.model_reasoning_effort.clone();
|
||||
let reasoning_summary = per_turn_config
|
||||
.model_reasoning_summary
|
||||
.unwrap_or(model_info.default_reasoning_summary);
|
||||
@@ -100,6 +101,13 @@ pub(super) async fn spawn_review_thread(
|
||||
parent_turn_context.network.is_some(),
|
||||
));
|
||||
|
||||
let extension_data = Arc::new(codex_extension_api::ExtensionData::new(
|
||||
review_turn_id.clone(),
|
||||
));
|
||||
extension_data.insert(HostLoadedSkills::new(
|
||||
parent_turn_context.turn_skills.outcome.clone(),
|
||||
));
|
||||
|
||||
let review_turn_context = TurnContext {
|
||||
sub_id: review_turn_id.clone(),
|
||||
trace_id: current_span_trace_id(),
|
||||
@@ -143,7 +151,7 @@ pub(super) async fn spawn_review_thread(
|
||||
dynamic_tools: parent_turn_context.dynamic_tools.clone(),
|
||||
truncation_policy: model_info.truncation_policy.into(),
|
||||
turn_metadata_state,
|
||||
extension_data: Arc::new(codex_extension_api::ExtensionData::new(review_turn_id)),
|
||||
extension_data,
|
||||
turn_skills: TurnSkillsContext::new(parent_turn_context.turn_skills.outcome.clone()),
|
||||
turn_timing_state: Arc::new(TurnTimingState::default()),
|
||||
server_model_warning_emitted: AtomicBool::new(false),
|
||||
|
||||
@@ -74,7 +74,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let rollout_items = vec![RolloutItem::TurnContext(previous_context_item)];
|
||||
@@ -113,7 +113,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let turn_id = previous_context_item
|
||||
@@ -963,7 +963,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let previous_turn_id = previous_context_item
|
||||
@@ -1042,7 +1042,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
}))
|
||||
.expect("serialize expected reference context item")
|
||||
@@ -1071,7 +1071,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let previous_turn_id = previous_context_item
|
||||
@@ -1190,7 +1190,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
|
||||
@@ -1308,7 +1308,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let previous_turn_id = previous_context_item
|
||||
@@ -1467,7 +1467,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let previous_turn_id = previous_context_item
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::input_queue::InputQueue;
|
||||
use super::*;
|
||||
use crate::agents_md::LoadedAgentsMd;
|
||||
use crate::config::ConstraintError;
|
||||
use crate::goals::GoalRuntimeState;
|
||||
use crate::skills::SkillError;
|
||||
@@ -54,8 +55,9 @@ pub(crate) struct SessionConfiguration {
|
||||
/// Developer instructions that supplement the base instructions.
|
||||
pub(super) developer_instructions: Option<String>,
|
||||
|
||||
/// Model instructions that are appended to the base instructions.
|
||||
pub(super) user_instructions: Option<String>,
|
||||
/// Model instructions that are appended to the base instructions and the
|
||||
/// files that supplied them.
|
||||
pub(super) user_instructions: Option<LoadedAgentsMd>,
|
||||
|
||||
/// Personality preference for the model.
|
||||
pub(super) personality: Option<Personality>,
|
||||
@@ -185,6 +187,7 @@ impl SessionConfiguration {
|
||||
personality: self.personality,
|
||||
collaboration_mode: self.collaboration_mode.clone(),
|
||||
session_source: self.session_source.clone(),
|
||||
forked_from_thread_id: self.forked_from_thread_id,
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
thread_source: self.thread_source,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::turn_context::TurnEnvironment;
|
||||
use super::*;
|
||||
use crate::codex_thread::TryStartTurnIfIdleRejectionReason;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config::ConfigOverrides;
|
||||
use crate::config::test_config;
|
||||
@@ -2558,7 +2559,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let turn_id = previous_context_item
|
||||
@@ -3145,7 +3146,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -3253,7 +3254,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -3784,7 +3785,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -3830,6 +3831,85 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn emit_subagent_session_started_includes_fork_lineage_from_session_configuration() {
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/codex/analytics-events/events"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
|
||||
let analytics_events_client = AnalyticsEventsClient::new(
|
||||
auth_manager,
|
||||
server.uri(),
|
||||
/*analytics_enabled*/ Some(true),
|
||||
);
|
||||
|
||||
let parent_thread_id = ThreadId::new();
|
||||
let forked_from_thread_id = ThreadId::new();
|
||||
let child_thread_id = ThreadId::new();
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
session_configuration.forked_from_thread_id = Some(forked_from_thread_id);
|
||||
|
||||
emit_subagent_session_started(
|
||||
&analytics_events_client,
|
||||
AppServerClientMetadata {
|
||||
client_name: Some("codex-tui".to_string()),
|
||||
client_version: Some("1.0.0".to_string()),
|
||||
},
|
||||
SessionId::from(child_thread_id),
|
||||
child_thread_id,
|
||||
Some(parent_thread_id),
|
||||
session_configuration.thread_config_snapshot(),
|
||||
SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
},
|
||||
);
|
||||
|
||||
let event = timeout(Duration::from_secs(1), async {
|
||||
'wait_for_event: loop {
|
||||
if let Some(requests) = server.received_requests().await {
|
||||
for request in requests {
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&request.body).expect("valid analytics payload");
|
||||
if let Some(event) = payload["events"].as_array().and_then(|events| {
|
||||
events
|
||||
.iter()
|
||||
.find(|event| event["event_type"] == "codex_thread_initialized")
|
||||
}) {
|
||||
break 'wait_for_event event.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("subagent initialization analytics should be emitted");
|
||||
|
||||
assert_eq!(
|
||||
event["event_params"]["parent_thread_id"],
|
||||
parent_thread_id.to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
event["event_params"]["forked_from_thread_id"],
|
||||
forked_from_thread_id.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
fn turn_environments_for_tests(
|
||||
environment: &Arc<codex_exec_server::Environment>,
|
||||
cwd: &codex_utils_absolute_path::AbsolutePathBuf,
|
||||
@@ -4534,7 +4614,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model,
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
@@ -4636,7 +4716,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -4879,7 +4959,7 @@ async fn make_session_with_config_and_rx(
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model,
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
@@ -4984,7 +5064,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model,
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
@@ -5919,7 +5999,7 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
@@ -6726,7 +6806,7 @@ where
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -8770,7 +8850,90 @@ async fn try_start_turn_if_idle_rejects_active_turn_without_injecting() {
|
||||
.await
|
||||
.expect_err("active turn should reject idle-only input");
|
||||
|
||||
assert_eq!(vec![item], err);
|
||||
assert_eq!(TryStartTurnIfIdleRejectionReason::Busy, err.reason());
|
||||
assert_eq!(vec![item], err.into_input());
|
||||
assert_eq!(
|
||||
Vec::<TurnInput>::new(),
|
||||
sess.input_queue.get_pending_input(&sess.active_turn).await
|
||||
);
|
||||
|
||||
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn try_start_turn_if_idle_rejects_plan_mode_without_injecting() {
|
||||
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let mut collaboration_mode = sess.collaboration_mode().await;
|
||||
collaboration_mode.mode = ModeKind::Plan;
|
||||
{
|
||||
let mut state = sess.state.lock().await;
|
||||
state.session_configuration.collaboration_mode = collaboration_mode;
|
||||
}
|
||||
|
||||
let item = user_message("synthetic idle input");
|
||||
let err = sess
|
||||
.try_start_turn_if_idle(vec![item.clone()])
|
||||
.await
|
||||
.expect_err("plan mode should reject automatic idle input");
|
||||
|
||||
assert_eq!(TryStartTurnIfIdleRejectionReason::PlanMode, err.reason());
|
||||
assert_eq!(vec![item], err.into_input());
|
||||
assert!(sess.active_turn.lock().await.is_none());
|
||||
assert_eq!(
|
||||
Vec::<TurnInput>::new(),
|
||||
sess.input_queue.get_pending_input(&sess.active_turn).await
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn try_start_turn_if_idle_rejects_pending_trigger_turn_without_injecting() {
|
||||
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
|
||||
sess.input_queue
|
||||
.enqueue_mailbox_communication(InterAgentCommunication::new(
|
||||
AgentPath::root(),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"pending trigger".to_string(),
|
||||
/*trigger_turn*/ true,
|
||||
))
|
||||
.await;
|
||||
|
||||
let item = user_message("synthetic idle input");
|
||||
let err = sess
|
||||
.try_start_turn_if_idle(vec![item.clone()])
|
||||
.await
|
||||
.expect_err("pending trigger-turn mail should reject automatic idle input");
|
||||
|
||||
assert_eq!(
|
||||
TryStartTurnIfIdleRejectionReason::PendingTriggerTurn,
|
||||
err.reason()
|
||||
);
|
||||
assert_eq!(vec![item], err.into_input());
|
||||
assert!(sess.active_turn.lock().await.is_none());
|
||||
assert!(sess.input_queue.has_trigger_turn_mailbox_items().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn try_start_turn_if_idle_rejects_active_review_turn_without_injecting() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Review,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let item = user_message("synthetic idle input");
|
||||
let err = sess
|
||||
.try_start_turn_if_idle(vec![item.clone()])
|
||||
.await
|
||||
.expect_err("active review turn should reject automatic idle input");
|
||||
|
||||
assert_eq!(TryStartTurnIfIdleRejectionReason::Busy, err.reason());
|
||||
assert_eq!(vec![item], err.into_input());
|
||||
assert_eq!(
|
||||
Vec::<TurnInput>::new(),
|
||||
sess.input_queue.get_pending_input(&sess.active_turn).await
|
||||
@@ -10705,7 +10868,6 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique thread settings Arc");
|
||||
turn_context_mut.permission_profile = PermissionProfile::Disabled;
|
||||
|
||||
let file_system_sandbox_policy = turn_context.file_system_sandbox_policy();
|
||||
let command = session.user_shell().derive_exec_args(
|
||||
command_script,
|
||||
turn_context.config.permissions.allow_login_shell,
|
||||
@@ -10717,7 +10879,6 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
command: &command,
|
||||
approval_policy: turn_context.approval_policy.value(),
|
||||
permission_profile: turn_context.permission_profile(),
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
#[allow(deprecated)]
|
||||
sandbox_cwd: turn_context.cwd.as_path(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
|
||||
@@ -69,6 +69,7 @@ use codex_analytics::InvocationType;
|
||||
use codex_analytics::TurnResolvedConfigFact;
|
||||
use codex_analytics::build_track_events_context;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_core_skills::injection::InjectedHostSkillPrompts;
|
||||
use codex_extension_api::TurnInputContext;
|
||||
use codex_extension_api::TurnInputEnvironment;
|
||||
use codex_features::Feature;
|
||||
@@ -535,6 +536,9 @@ async fn build_skills_and_plugins(
|
||||
)
|
||||
.await;
|
||||
|
||||
let injected_host_skill_prompts = turn_context
|
||||
.extension_data
|
||||
.get::<InjectedHostSkillPrompts>();
|
||||
let SkillInjections {
|
||||
items: skill_injections,
|
||||
warnings: skill_warnings,
|
||||
@@ -591,7 +595,16 @@ async fn build_skills_and_plugins(
|
||||
.track_plugin_used(tracking.clone(), plugin);
|
||||
}
|
||||
|
||||
let mut injection_items = skill_items;
|
||||
let mut injection_items: Vec<ResponseItem> = match injected_host_skill_prompts {
|
||||
Some(injected_host_skill_prompts) => skill_injections
|
||||
.iter()
|
||||
.filter(|skill| !injected_host_skill_prompts.contains_path(&skill.path))
|
||||
.map(|skill| {
|
||||
ContextualUserFragment::into(crate::context::SkillInstructions::from(skill))
|
||||
})
|
||||
.collect(),
|
||||
None => skill_items,
|
||||
};
|
||||
injection_items.extend(plugin_items);
|
||||
injection_items.extend(extension_injection_items);
|
||||
Some((injection_items, explicitly_enabled_connectors))
|
||||
@@ -685,7 +698,7 @@ async fn track_turn_resolved_config_analytics(
|
||||
permission_profile: turn_context.permission_profile(),
|
||||
#[allow(deprecated)]
|
||||
permission_profile_cwd: turn_context.cwd.to_path_buf(),
|
||||
reasoning_effort: turn_context.reasoning_effort,
|
||||
reasoning_effort: turn_context.reasoning_effort.clone(),
|
||||
reasoning_summary: Some(turn_context.reasoning_summary),
|
||||
service_tier: turn_context
|
||||
.config
|
||||
@@ -1379,6 +1392,7 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<String> {
|
||||
| EventMsg::RealtimeConversationClosed(_)
|
||||
| EventMsg::ModelReroute(_)
|
||||
| EventMsg::ModelVerification(_)
|
||||
| EventMsg::TurnModerationMetadata(_)
|
||||
| EventMsg::ContextCompacted(_)
|
||||
| EventMsg::ThreadRolledBack(_)
|
||||
| EventMsg::TurnStarted(_)
|
||||
@@ -1786,7 +1800,7 @@ async fn try_run_sampling_request(
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
@@ -1814,7 +1828,6 @@ async fn try_run_sampling_request(
|
||||
!sess.services.extensions.turn_item_contributors().is_empty();
|
||||
let mut active_item_is_streaming_to_client = false;
|
||||
let receiving_span = trace_span!("receiving_stream");
|
||||
let mut completed_response_id: Option<String> = None;
|
||||
let outcome: CodexResult<SamplingRequestResult> = loop {
|
||||
let handle_responses = trace_span!(
|
||||
parent: &receiving_span,
|
||||
@@ -1912,6 +1925,7 @@ async fn try_run_sampling_request(
|
||||
role == "assistant" && matches!(phase, Some(MessagePhase::Commentary))
|
||||
}
|
||||
ResponseItem::Reasoning { .. } => true,
|
||||
ResponseItem::AgentMessage { .. } => false,
|
||||
ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
@@ -2044,6 +2058,10 @@ async fn try_run_sampling_request(
|
||||
.await;
|
||||
}
|
||||
}
|
||||
ResponseEvent::TurnModerationMetadata(metadata) => {
|
||||
sess.emit_turn_moderation_metadata(&turn_context, metadata)
|
||||
.await;
|
||||
}
|
||||
ResponseEvent::ServerReasoningIncluded(included) => {
|
||||
sess.set_server_reasoning_included(included).await;
|
||||
}
|
||||
@@ -2058,9 +2076,9 @@ async fn try_run_sampling_request(
|
||||
sess.services.models_manager.refresh_if_new_etag(etag).await;
|
||||
}
|
||||
ResponseEvent::Completed {
|
||||
response_id,
|
||||
token_usage,
|
||||
end_turn,
|
||||
..
|
||||
} => {
|
||||
flush_assistant_text_segments_all(
|
||||
&sess,
|
||||
@@ -2076,7 +2094,6 @@ async fn try_run_sampling_request(
|
||||
if let Some(false) = end_turn {
|
||||
needs_follow_up = true;
|
||||
}
|
||||
completed_response_id = Some(response_id);
|
||||
break Ok(SamplingRequestResult {
|
||||
needs_follow_up,
|
||||
last_agent_message,
|
||||
@@ -2200,15 +2217,6 @@ async fn try_run_sampling_request(
|
||||
)
|
||||
.await;
|
||||
|
||||
if sess
|
||||
.features
|
||||
.enabled(Feature::ResponsesWebsocketResponseProcessed)
|
||||
&& outcome.is_ok()
|
||||
&& let Some(response_id) = completed_response_id.as_deref()
|
||||
{
|
||||
client_session.send_response_processed(response_id).await;
|
||||
}
|
||||
|
||||
drain_in_flight(&mut in_flight, sess.clone(), turn_context.clone()).await?;
|
||||
|
||||
if should_emit_token_count {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::*;
|
||||
use crate::SkillLoadOutcome;
|
||||
use crate::agents_md::LoadedAgentsMd;
|
||||
use crate::config::GhostSnapshotConfig;
|
||||
use crate::environment_selection::ResolvedTurnEnvironments;
|
||||
use codex_core_skills::HostLoadedSkills;
|
||||
use codex_model_provider::SharedModelProvider;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_protocol::SessionId;
|
||||
@@ -134,7 +136,8 @@ impl TurnContext {
|
||||
pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
|
||||
if self.model_info.supports_reasoning_summaries {
|
||||
self.reasoning_effort
|
||||
.or(self.model_info.default_reasoning_level)
|
||||
.clone()
|
||||
.or_else(|| self.model_info.default_reasoning_level.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -194,28 +197,29 @@ impl TurnContext {
|
||||
let supported_reasoning_levels = model_info
|
||||
.supported_reasoning_levels
|
||||
.iter()
|
||||
.map(|preset| preset.effort)
|
||||
.map(|preset| preset.effort.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort {
|
||||
let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort.clone()
|
||||
{
|
||||
if supported_reasoning_levels.contains(¤t_reasoning_effort) {
|
||||
Some(current_reasoning_effort)
|
||||
} else {
|
||||
supported_reasoning_levels
|
||||
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
|
||||
.copied()
|
||||
.or(model_info.default_reasoning_level)
|
||||
.cloned()
|
||||
.or_else(|| model_info.default_reasoning_level.clone())
|
||||
}
|
||||
} else {
|
||||
supported_reasoning_levels
|
||||
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
|
||||
.copied()
|
||||
.or(model_info.default_reasoning_level)
|
||||
.cloned()
|
||||
.or_else(|| model_info.default_reasoning_level.clone())
|
||||
};
|
||||
config.model_reasoning_effort = reasoning_effort;
|
||||
config.model_reasoning_effort = reasoning_effort.clone();
|
||||
|
||||
let collaboration_mode = self.collaboration_mode.with_updates(
|
||||
Some(model.clone()),
|
||||
Some(reasoning_effort),
|
||||
Some(reasoning_effort.clone()),
|
||||
/*developer_instructions*/ None,
|
||||
);
|
||||
let features = self.features.clone();
|
||||
@@ -361,7 +365,7 @@ impl TurnContext {
|
||||
collaboration_mode: Some(self.collaboration_mode.clone()),
|
||||
multi_agent_version: Some(self.multi_agent_version),
|
||||
realtime_active: Some(self.realtime_active),
|
||||
effort: self.reasoning_effort,
|
||||
effort: self.reasoning_effort.clone(),
|
||||
summary: ReasoningSummaryConfig::Auto,
|
||||
}
|
||||
}
|
||||
@@ -526,6 +530,7 @@ impl Session {
|
||||
));
|
||||
let (current_date, timezone) = local_time_context();
|
||||
let extension_data = Arc::new(codex_extension_api::ExtensionData::new(sub_id.clone()));
|
||||
extension_data.insert(HostLoadedSkills::new(Arc::clone(&skills_outcome)));
|
||||
TurnContext {
|
||||
sub_id,
|
||||
trace_id: current_span_trace_id(),
|
||||
@@ -549,7 +554,10 @@ impl Session {
|
||||
app_server_client_name: session_configuration.app_server_client_name.clone(),
|
||||
developer_instructions: session_configuration.developer_instructions.clone(),
|
||||
compact_prompt: session_configuration.compact_prompt.clone(),
|
||||
user_instructions: session_configuration.user_instructions.clone(),
|
||||
user_instructions: session_configuration
|
||||
.user_instructions
|
||||
.as_ref()
|
||||
.map(LoadedAgentsMd::text),
|
||||
collaboration_mode: session_configuration.collaboration_mode.clone(),
|
||||
multi_agent_version,
|
||||
personality: session_configuration.personality,
|
||||
|
||||
@@ -271,7 +271,7 @@ async fn schedule_startup_prewarm_inner(
|
||||
&startup_prompt,
|
||||
&startup_turn_context.model_info,
|
||||
&startup_turn_context.session_telemetry,
|
||||
startup_turn_context.reasoning_effort,
|
||||
startup_turn_context.reasoning_effort.clone(),
|
||||
startup_turn_context.reasoning_summary,
|
||||
startup_turn_context.config.service_tier.clone(),
|
||||
startup_turn_metadata_header.as_deref(),
|
||||
|
||||
+16
-312
@@ -1,19 +1,12 @@
|
||||
use crate::shell_detect::detect_shell_type;
|
||||
use crate::shell_snapshot::ShellSnapshot;
|
||||
use codex_shell_command::shell_detect::DetectedShell;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::watch;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub enum ShellType {
|
||||
Zsh,
|
||||
Bash,
|
||||
PowerShell,
|
||||
Sh,
|
||||
Cmd,
|
||||
}
|
||||
pub use codex_shell_command::shell_detect::ShellType;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Shell {
|
||||
@@ -29,13 +22,7 @@ pub struct Shell {
|
||||
|
||||
impl Shell {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self.shell_type {
|
||||
ShellType::Zsh => "zsh",
|
||||
ShellType::Bash => "bash",
|
||||
ShellType::PowerShell => "powershell",
|
||||
ShellType::Sh => "sh",
|
||||
ShellType::Cmd => "cmd",
|
||||
}
|
||||
self.shell_type.name()
|
||||
}
|
||||
|
||||
/// Takes a string of shell and returns the full list of command args to
|
||||
@@ -88,319 +75,36 @@ impl PartialEq for Shell {
|
||||
|
||||
impl Eq for Shell {}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn get_user_shell_path() -> Option<PathBuf> {
|
||||
let uid = unsafe { libc::getuid() };
|
||||
use std::ffi::CStr;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::ptr;
|
||||
|
||||
let mut passwd = MaybeUninit::<libc::passwd>::uninit();
|
||||
|
||||
// We cannot use getpwuid here: it returns pointers into libc-managed
|
||||
// storage, which is not safe to read concurrently on all targets (the musl
|
||||
// static build used by the CLI can segfault when parallel callers race on
|
||||
// that buffer). getpwuid_r keeps the passwd data in caller-owned memory.
|
||||
let suggested_buffer_len = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) };
|
||||
let buffer_len = usize::try_from(suggested_buffer_len)
|
||||
.ok()
|
||||
.filter(|len| *len > 0)
|
||||
.unwrap_or(1024);
|
||||
let mut buffer = vec![0; buffer_len];
|
||||
|
||||
loop {
|
||||
let mut result = ptr::null_mut();
|
||||
let status = unsafe {
|
||||
libc::getpwuid_r(
|
||||
uid,
|
||||
passwd.as_mut_ptr(),
|
||||
buffer.as_mut_ptr().cast(),
|
||||
buffer.len(),
|
||||
&mut result,
|
||||
)
|
||||
};
|
||||
|
||||
if status == 0 {
|
||||
if result.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let passwd = unsafe { passwd.assume_init_ref() };
|
||||
if passwd.pw_shell.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let shell_path = unsafe { CStr::from_ptr(passwd.pw_shell) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
return Some(PathBuf::from(shell_path));
|
||||
}
|
||||
|
||||
if status != libc::ERANGE {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Retry with a larger buffer until libc can materialize the passwd entry.
|
||||
let new_len = buffer.len().checked_mul(2)?;
|
||||
if new_len > 1024 * 1024 {
|
||||
return None;
|
||||
}
|
||||
buffer.resize(new_len, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn get_user_shell_path() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
fn file_exists(path: &PathBuf) -> Option<PathBuf> {
|
||||
if std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) {
|
||||
Some(PathBuf::from(path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_shell_path(
|
||||
shell_type: ShellType,
|
||||
provided_path: Option<&PathBuf>,
|
||||
binary_name: &str,
|
||||
fallback_paths: &[&str],
|
||||
) -> Option<PathBuf> {
|
||||
// If exact provided path exists, use it
|
||||
if provided_path.and_then(file_exists).is_some() {
|
||||
return provided_path.cloned();
|
||||
}
|
||||
|
||||
// Check if the shell we are trying to load is user's default shell
|
||||
// if just use it
|
||||
let default_shell_path = get_user_shell_path();
|
||||
if let Some(default_shell_path) = default_shell_path
|
||||
&& detect_shell_type(&default_shell_path) == Some(shell_type)
|
||||
&& file_exists(&default_shell_path).is_some()
|
||||
{
|
||||
return Some(default_shell_path);
|
||||
}
|
||||
|
||||
if let Ok(path) = which::which(binary_name) {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
for path in fallback_paths {
|
||||
//check exists
|
||||
if let Some(path) = file_exists(&PathBuf::from(path)) {
|
||||
return Some(path);
|
||||
impl From<DetectedShell> for Shell {
|
||||
fn from(detected: DetectedShell) -> Self {
|
||||
Self {
|
||||
shell_type: detected.shell_type,
|
||||
shell_path: detected.shell_path,
|
||||
shell_snapshot: empty_shell_snapshot_receiver(),
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
const ZSH_FALLBACK_PATHS: &[&str] = &["/bin/zsh"];
|
||||
|
||||
fn get_zsh_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
||||
let shell_path = get_shell_path(ShellType::Zsh, path, "zsh", ZSH_FALLBACK_PATHS);
|
||||
|
||||
shell_path.map(|shell_path| Shell {
|
||||
shell_type: ShellType::Zsh,
|
||||
shell_path,
|
||||
shell_snapshot: empty_shell_snapshot_receiver(),
|
||||
})
|
||||
}
|
||||
|
||||
const BASH_FALLBACK_PATHS: &[&str] = &["/bin/bash"];
|
||||
|
||||
fn get_bash_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
||||
let shell_path = get_shell_path(ShellType::Bash, path, "bash", BASH_FALLBACK_PATHS);
|
||||
|
||||
shell_path.map(|shell_path| Shell {
|
||||
shell_type: ShellType::Bash,
|
||||
shell_path,
|
||||
shell_snapshot: empty_shell_snapshot_receiver(),
|
||||
})
|
||||
}
|
||||
|
||||
const SH_FALLBACK_PATHS: &[&str] = &["/bin/sh"];
|
||||
|
||||
fn get_sh_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
||||
let shell_path = get_shell_path(ShellType::Sh, path, "sh", SH_FALLBACK_PATHS);
|
||||
|
||||
shell_path.map(|shell_path| Shell {
|
||||
shell_type: ShellType::Sh,
|
||||
shell_path,
|
||||
shell_snapshot: empty_shell_snapshot_receiver(),
|
||||
})
|
||||
}
|
||||
|
||||
// Note the `pwsh` and `powershell` fallback paths are where the respective
|
||||
// shells are commonly installed on GitHub Actions Windows runners, but may not
|
||||
// be present on all Windows machines:
|
||||
// https://docs.github.com/en/actions/tutorials/build-and-test-code/powershell
|
||||
|
||||
#[cfg(windows)]
|
||||
const PWSH_FALLBACK_PATHS: &[&str] = &[r#"C:\Program Files\PowerShell\7\pwsh.exe"#];
|
||||
#[cfg(not(windows))]
|
||||
const PWSH_FALLBACK_PATHS: &[&str] = &["/usr/local/bin/pwsh"];
|
||||
|
||||
#[cfg(windows)]
|
||||
const POWERSHELL_FALLBACK_PATHS: &[&str] =
|
||||
&[r#"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"#];
|
||||
#[cfg(not(windows))]
|
||||
const POWERSHELL_FALLBACK_PATHS: &[&str] = &[];
|
||||
|
||||
fn get_powershell_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
||||
let shell_path = get_shell_path(ShellType::PowerShell, path, "pwsh", PWSH_FALLBACK_PATHS)
|
||||
.or_else(|| {
|
||||
get_shell_path(
|
||||
ShellType::PowerShell,
|
||||
path,
|
||||
"powershell",
|
||||
POWERSHELL_FALLBACK_PATHS,
|
||||
)
|
||||
});
|
||||
|
||||
shell_path.map(|shell_path| Shell {
|
||||
shell_type: ShellType::PowerShell,
|
||||
shell_path,
|
||||
shell_snapshot: empty_shell_snapshot_receiver(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_cmd_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
||||
let shell_path = get_shell_path(ShellType::Cmd, path, "cmd", &[]);
|
||||
|
||||
shell_path.map(|shell_path| Shell {
|
||||
shell_type: ShellType::Cmd,
|
||||
shell_path,
|
||||
shell_snapshot: empty_shell_snapshot_receiver(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
fn ultimate_fallback_shell() -> Shell {
|
||||
if cfg!(windows) {
|
||||
Shell {
|
||||
shell_type: ShellType::Cmd,
|
||||
shell_path: PathBuf::from("cmd.exe"),
|
||||
shell_snapshot: empty_shell_snapshot_receiver(),
|
||||
}
|
||||
} else {
|
||||
Shell {
|
||||
shell_type: ShellType::Sh,
|
||||
shell_path: PathBuf::from("/bin/sh"),
|
||||
shell_snapshot: empty_shell_snapshot_receiver(),
|
||||
}
|
||||
}
|
||||
codex_shell_command::shell_detect::ultimate_fallback_shell().into()
|
||||
}
|
||||
|
||||
pub fn get_shell_by_model_provided_path(shell_path: &PathBuf) -> Shell {
|
||||
detect_shell_type(shell_path)
|
||||
.and_then(|shell_type| get_shell(shell_type, Some(shell_path)))
|
||||
.unwrap_or(ultimate_fallback_shell())
|
||||
codex_shell_command::shell_detect::get_shell_by_model_provided_path(shell_path).into()
|
||||
}
|
||||
|
||||
pub fn get_shell(shell_type: ShellType, path: Option<&PathBuf>) -> Option<Shell> {
|
||||
match shell_type {
|
||||
ShellType::Zsh => get_zsh_shell(path),
|
||||
ShellType::Bash => get_bash_shell(path),
|
||||
ShellType::PowerShell => get_powershell_shell(path),
|
||||
ShellType::Sh => get_sh_shell(path),
|
||||
ShellType::Cmd => get_cmd_shell(path),
|
||||
}
|
||||
codex_shell_command::shell_detect::get_shell(shell_type, path).map(Into::into)
|
||||
}
|
||||
|
||||
pub fn default_user_shell() -> Shell {
|
||||
default_user_shell_from_path(get_user_shell_path())
|
||||
codex_shell_command::shell_detect::default_user_shell().into()
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "macos"))]
|
||||
fn default_user_shell_from_path(user_shell_path: Option<PathBuf>) -> Shell {
|
||||
if cfg!(windows) {
|
||||
get_shell(ShellType::PowerShell, /*path*/ None).unwrap_or(ultimate_fallback_shell())
|
||||
} else {
|
||||
let user_default_shell = user_shell_path
|
||||
.and_then(|shell| detect_shell_type(&shell))
|
||||
.and_then(|shell_type| get_shell(shell_type, /*path*/ None));
|
||||
|
||||
let shell_with_fallback = if cfg!(target_os = "macos") {
|
||||
user_default_shell
|
||||
.or_else(|| get_shell(ShellType::Zsh, /*path*/ None))
|
||||
.or_else(|| get_shell(ShellType::Bash, /*path*/ None))
|
||||
} else {
|
||||
user_default_shell
|
||||
.or_else(|| get_shell(ShellType::Bash, /*path*/ None))
|
||||
.or_else(|| get_shell(ShellType::Zsh, /*path*/ None))
|
||||
};
|
||||
|
||||
shell_with_fallback.unwrap_or(ultimate_fallback_shell())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod detect_shell_type_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_shell_type() {
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("zsh")),
|
||||
Some(ShellType::Zsh)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("bash")),
|
||||
Some(ShellType::Bash)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("pwsh")),
|
||||
Some(ShellType::PowerShell)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("powershell")),
|
||||
Some(ShellType::PowerShell)
|
||||
);
|
||||
assert_eq!(detect_shell_type(&PathBuf::from("fish")), None);
|
||||
assert_eq!(detect_shell_type(&PathBuf::from("other")), None);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("/bin/zsh")),
|
||||
Some(ShellType::Zsh)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("/bin/bash")),
|
||||
Some(ShellType::Bash)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("powershell.exe")),
|
||||
Some(ShellType::PowerShell)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from(if cfg!(windows) {
|
||||
"C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
||||
} else {
|
||||
"/usr/local/bin/pwsh"
|
||||
})),
|
||||
Some(ShellType::PowerShell)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("pwsh.exe")),
|
||||
Some(ShellType::PowerShell)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("/usr/local/bin/pwsh")),
|
||||
Some(ShellType::PowerShell)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("/bin/sh")),
|
||||
Some(ShellType::Sh)
|
||||
);
|
||||
assert_eq!(detect_shell_type(&PathBuf::from("sh")), Some(ShellType::Sh));
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("cmd")),
|
||||
Some(ShellType::Cmd)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_shell_type(&PathBuf::from("cmd.exe")),
|
||||
Some(ShellType::Cmd)
|
||||
);
|
||||
}
|
||||
codex_shell_command::shell_detect::default_user_shell_from_path(user_shell_path).into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
use crate::shell::ShellType;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) fn detect_shell_type(shell_path: &PathBuf) -> Option<ShellType> {
|
||||
match shell_path.as_os_str().to_str() {
|
||||
Some("zsh") => Some(ShellType::Zsh),
|
||||
Some("sh") => Some(ShellType::Sh),
|
||||
Some("cmd") => Some(ShellType::Cmd),
|
||||
Some("bash") => Some(ShellType::Bash),
|
||||
Some("pwsh") => Some(ShellType::PowerShell),
|
||||
Some("powershell") => Some(ShellType::PowerShell),
|
||||
_ => {
|
||||
let shell_name = shell_path.file_stem();
|
||||
if let Some(shell_name) = shell_name {
|
||||
let shell_name_path = Path::new(shell_name);
|
||||
if shell_name_path != Path::new(shell_path) {
|
||||
return detect_shell_type(&shell_name_path.to_path_buf());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,9 +151,7 @@ impl ShellSnapshot {
|
||||
});
|
||||
|
||||
// Make the new snapshot.
|
||||
if let Err(err) =
|
||||
write_shell_snapshot(shell.shell_type.clone(), &temp_path, session_cwd).await
|
||||
{
|
||||
if let Err(err) = write_shell_snapshot(shell.shell_type, &temp_path, session_cwd).await {
|
||||
tracing::warn!(
|
||||
"Failed to create shell snapshot for {}: {err:?}",
|
||||
shell.name()
|
||||
@@ -203,7 +201,7 @@ async fn write_shell_snapshot(
|
||||
if shell_type == ShellType::PowerShell || shell_type == ShellType::Cmd {
|
||||
bail!("Shell snapshot not supported yet for {shell_type:?}");
|
||||
}
|
||||
let shell = get_shell(shell_type.clone(), /*path*/ None)
|
||||
let shell = get_shell(shell_type, /*path*/ None)
|
||||
.with_context(|| format!("No available shell for {shell_type:?}"))?;
|
||||
|
||||
let raw_snapshot = capture_snapshot(&shell, cwd).await?;
|
||||
@@ -225,7 +223,7 @@ async fn write_shell_snapshot(
|
||||
}
|
||||
|
||||
async fn capture_snapshot(shell: &Shell, cwd: &AbsolutePathBuf) -> Result<String> {
|
||||
let shell_type = shell.shell_type.clone();
|
||||
let shell_type = shell.shell_type;
|
||||
match shell_type {
|
||||
ShellType::Zsh => run_shell_script(shell, &zsh_snapshot_script(), cwd).await,
|
||||
ShellType::Bash => run_shell_script(shell, &bash_snapshot_script(), cwd).await,
|
||||
|
||||
@@ -38,7 +38,8 @@ use tracing::warn;
|
||||
|
||||
const GENERATED_IMAGE_ARTIFACTS_DIR: &str = "generated_images";
|
||||
|
||||
pub(crate) fn image_generation_artifact_path(
|
||||
/// Returns the host-owned default artifact path for a generated image.
|
||||
pub fn image_generation_artifact_path(
|
||||
codex_home: &AbsolutePathBuf,
|
||||
session_id: &str,
|
||||
call_id: &str,
|
||||
|
||||
@@ -83,7 +83,7 @@ async fn handle_spawn_agent(
|
||||
sender_thread_id: session.thread_id,
|
||||
prompt: prompt.clone(),
|
||||
model: args.model.clone().unwrap_or_default(),
|
||||
reasoning_effort: args.reasoning_effort.unwrap_or_default(),
|
||||
reasoning_effort: args.reasoning_effort.clone().unwrap_or_default(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
@@ -94,14 +94,18 @@ async fn handle_spawn_agent(
|
||||
config.service_tier = Some(service_tier.clone());
|
||||
}
|
||||
if args.fork_context {
|
||||
reject_full_fork_spawn_overrides(role_name, args.model.as_deref(), args.reasoning_effort)?;
|
||||
reject_full_fork_spawn_overrides(
|
||||
role_name,
|
||||
args.model.as_deref(),
|
||||
args.reasoning_effort.clone(),
|
||||
)?;
|
||||
} else {
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
&mut config,
|
||||
args.model.as_deref(),
|
||||
args.reasoning_effort,
|
||||
args.reasoning_effort.clone(),
|
||||
)
|
||||
.await?;
|
||||
apply_role_to_config(&mut config, role_name)
|
||||
@@ -174,7 +178,7 @@ async fn handle_spawn_agent(
|
||||
.unwrap_or_else(|| args.model.clone().unwrap_or_default());
|
||||
let effective_reasoning_effort = agent_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.reasoning_effort)
|
||||
.and_then(|snapshot| snapshot.reasoning_effort.clone())
|
||||
.unwrap_or(args.reasoning_effort.unwrap_or_default());
|
||||
let nickname = new_agent_nickname.clone();
|
||||
session
|
||||
|
||||
@@ -224,7 +224,8 @@ fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallE
|
||||
config.model_provider = turn.provider.info().clone();
|
||||
config.model_reasoning_effort = turn
|
||||
.reasoning_effort
|
||||
.or(turn.model_info.default_reasoning_level);
|
||||
.clone()
|
||||
.or_else(|| turn.model_info.default_reasoning_level.clone());
|
||||
config.model_reasoning_summary = Some(turn.reasoning_summary);
|
||||
config.developer_instructions = turn.developer_instructions.clone();
|
||||
config.compact_prompt = turn.compact_prompt.clone();
|
||||
@@ -304,7 +305,7 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
|
||||
validate_spawn_agent_reasoning_effort(
|
||||
&selected_model_name,
|
||||
&selected_model_info.supported_reasoning_levels,
|
||||
reasoning_effort,
|
||||
&reasoning_effort,
|
||||
)?;
|
||||
config.model_reasoning_effort = Some(reasoning_effort);
|
||||
} else {
|
||||
@@ -318,7 +319,7 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
|
||||
validate_spawn_agent_reasoning_effort(
|
||||
&turn.model_info.slug,
|
||||
&turn.model_info.supported_reasoning_levels,
|
||||
reasoning_effort,
|
||||
&reasoning_effort,
|
||||
)?;
|
||||
config.model_reasoning_effort = Some(reasoning_effort);
|
||||
}
|
||||
@@ -404,11 +405,11 @@ fn find_spawn_agent_model_name(
|
||||
fn validate_spawn_agent_reasoning_effort(
|
||||
model: &str,
|
||||
supported_reasoning_levels: &[ReasoningEffortPreset],
|
||||
requested_reasoning_effort: ReasoningEffort,
|
||||
requested_reasoning_effort: &ReasoningEffort,
|
||||
) -> Result<(), FunctionCallError> {
|
||||
if supported_reasoning_levels
|
||||
.iter()
|
||||
.any(|preset| preset.effort == requested_reasoning_effort)
|
||||
.any(|preset| &preset.effort == requested_reasoning_effort)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str =
|
||||
const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str =
|
||||
"Service tier override for the new agent. Omit unless explicitly requested.";
|
||||
const MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT_DESCRIPTION: usize = 5;
|
||||
const MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION: usize = 64;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SpawnAgentToolOptions {
|
||||
@@ -166,7 +167,8 @@ pub fn create_send_message_tool() -> ToolSpec {
|
||||
"message".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Message text to queue on the target agent.".to_string(),
|
||||
)),
|
||||
))
|
||||
.with_encrypted(),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -198,13 +200,14 @@ pub fn create_followup_task_tool() -> ToolSpec {
|
||||
"message".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Message text to send to the target agent.".to_string(),
|
||||
)),
|
||||
))
|
||||
.with_encrypted(),
|
||||
),
|
||||
]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "followup_task".to_string(),
|
||||
description: "Send a follow-up task to an existing non-root target agent and trigger a turn in that target. If the target is currently mid-turn, the message is queued and will be used to start the target's next turn, after the current turn completes."
|
||||
description: "Send a follow-up task to an existing non-root target agent and trigger a turn if it is idle. If the target is already running, deliver the task promptly at message boundaries while sampling, or after the pending tool call completes."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
@@ -594,7 +597,10 @@ fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<St
|
||||
BTreeMap::from([
|
||||
(
|
||||
"message".to_string(),
|
||||
JsonSchema::string(Some("Initial plain-text task for the new agent.".to_string())),
|
||||
JsonSchema::string(Some(
|
||||
"Initial plain-text task for the new agent.".to_string(),
|
||||
))
|
||||
.with_encrypted(),
|
||||
),
|
||||
(
|
||||
"agent_type".to_string(),
|
||||
@@ -762,13 +768,20 @@ fn spawn_agent_models_description(models: &[ModelPreset]) -> String {
|
||||
let model_descriptions = visible_models
|
||||
.into_iter()
|
||||
.map(|model| {
|
||||
let default_reasoning_effort = model.default_reasoning_effort;
|
||||
let default_reasoning_effort = &model.default_reasoning_effort;
|
||||
let efforts = model
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.map(|preset| {
|
||||
let effort = preset.effort;
|
||||
if effort == default_reasoning_effort {
|
||||
let effort = preset.effort.as_str();
|
||||
let effort = match effort
|
||||
.char_indices()
|
||||
.nth(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION)
|
||||
{
|
||||
Some((index, _)) => &effort[..index],
|
||||
None => effort,
|
||||
};
|
||||
if &preset.effort == default_reasoning_effort {
|
||||
format!("{effort} (default)")
|
||||
} else {
|
||||
effort.to_string()
|
||||
|
||||
@@ -81,6 +81,12 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
|
||||
assert!(!description.contains("hidden-model"));
|
||||
assert!(properties.contains_key("task_name"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert_eq!(
|
||||
properties
|
||||
.get("message")
|
||||
.and_then(|schema| schema.encrypted),
|
||||
Some(true)
|
||||
);
|
||||
assert!(properties.contains_key("fork_turns"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert!(!properties.contains_key("fork_context"));
|
||||
@@ -141,6 +147,12 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
|
||||
|
||||
assert!(properties.contains_key("fork_context"));
|
||||
assert!(!properties.contains_key("fork_turns"));
|
||||
assert_eq!(
|
||||
properties
|
||||
.get("message")
|
||||
.and_then(|schema| schema.encrypted),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
properties
|
||||
.get("model")
|
||||
@@ -186,6 +198,27 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
|
||||
assert!(!description.contains("`sixth-model`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_caps_reasoning_effort_value_length() {
|
||||
let mut model = model_preset("visible", /*show_in_picker*/ true);
|
||||
let custom_effort = ReasoningEffort::Custom(
|
||||
"é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION + 1),
|
||||
);
|
||||
model.default_reasoning_effort = custom_effort.clone();
|
||||
model.supported_reasoning_efforts = vec![ReasoningEffortPreset {
|
||||
effort: custom_effort,
|
||||
description: "Model-defined".to_string(),
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
spawn_agent_models_description(&[model]),
|
||||
format!(
|
||||
"Available model overrides (optional; inherited parent model is preferred):\n- `visible-model`: visible description Reasoning efforts: {} (default). Service tiers: priority.",
|
||||
"é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
|
||||
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
|
||||
@@ -238,6 +271,12 @@ fn send_message_tool_requires_message_and_has_no_output_schema() {
|
||||
.expect("send_message should use object params");
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert_eq!(
|
||||
properties
|
||||
.get("message")
|
||||
.and_then(|schema| schema.encrypted),
|
||||
Some(true)
|
||||
);
|
||||
assert!(!properties.contains_key("interrupt"));
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert_eq!(
|
||||
@@ -257,6 +296,7 @@ fn send_message_tool_requires_message_and_has_no_output_schema() {
|
||||
fn followup_task_tool_requires_message_and_has_no_output_schema() {
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
name,
|
||||
description,
|
||||
parameters,
|
||||
output_schema,
|
||||
..
|
||||
@@ -265,6 +305,10 @@ fn followup_task_tool_requires_message_and_has_no_output_schema() {
|
||||
panic!("followup_task should be a function tool");
|
||||
};
|
||||
assert_eq!(name, "followup_task");
|
||||
assert_eq!(
|
||||
description,
|
||||
"Send a follow-up task to an existing non-root target agent and trigger a turn if it is idle. If the target is already running, deliver the task promptly at message boundaries while sampling, or after the pending tool call completes."
|
||||
);
|
||||
assert_eq!(
|
||||
parameters.schema_type,
|
||||
Some(JsonSchemaType::Single(JsonSchemaPrimitiveType::Object))
|
||||
@@ -275,6 +319,12 @@ fn followup_task_tool_requires_message_and_has_no_output_schema() {
|
||||
.expect("followup_task should use object params");
|
||||
assert!(properties.contains_key("target"));
|
||||
assert!(properties.contains_key("message"));
|
||||
assert_eq!(
|
||||
properties
|
||||
.get("message")
|
||||
.and_then(|schema| schema.encrypted),
|
||||
Some(true)
|
||||
);
|
||||
assert!(!properties.contains_key("items"));
|
||||
assert_eq!(
|
||||
parameters.required.as_ref(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::LoadedAgentsMd;
|
||||
use crate::ThreadManager;
|
||||
use crate::config::AgentRoleConfig;
|
||||
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
|
||||
@@ -1151,7 +1152,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
||||
turn.clone(),
|
||||
"spawn_agent",
|
||||
function_payload(json!({
|
||||
"message": "inspect this repo",
|
||||
"message": "encrypted-spawn-message",
|
||||
"task_name": "test_process"
|
||||
})),
|
||||
))
|
||||
@@ -1187,7 +1188,8 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
||||
if communication.author == AgentPath::root()
|
||||
&& communication.recipient.as_str() == "/root/test_process"
|
||||
&& communication.other_recipients.is_empty()
|
||||
&& communication.content == "inspect this repo"
|
||||
&& communication.content.is_empty()
|
||||
&& communication.encrypted_content.as_deref() == Some("encrypted-spawn-message")
|
||||
&& communication.trigger_turn
|
||||
)
|
||||
}));
|
||||
@@ -1199,7 +1201,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
||||
"send_message",
|
||||
function_payload(json!({
|
||||
"target": "test_process",
|
||||
"message": "continue"
|
||||
"message": "encrypted-send-message"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
@@ -1213,7 +1215,8 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
||||
if communication.author == AgentPath::root()
|
||||
&& communication.recipient.as_str() == "/root/test_process"
|
||||
&& communication.other_recipients.is_empty()
|
||||
&& communication.content == "continue"
|
||||
&& communication.content.is_empty()
|
||||
&& communication.encrypted_content.as_deref() == Some("encrypted-send-message")
|
||||
&& !communication.trigger_turn
|
||||
)
|
||||
}));
|
||||
@@ -1395,7 +1398,7 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
|
||||
"send_message",
|
||||
function_payload(json!({
|
||||
"target": "/root",
|
||||
"message": "done"
|
||||
"message": "encrypted-done"
|
||||
})),
|
||||
))
|
||||
.await
|
||||
@@ -1409,7 +1412,8 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
|
||||
if communication.author == child_path
|
||||
&& communication.recipient == AgentPath::root()
|
||||
&& communication.other_recipients.is_empty()
|
||||
&& communication.content == "done"
|
||||
&& communication.content.is_empty()
|
||||
&& communication.encrypted_content.as_deref() == Some("encrypted-done")
|
||||
&& !communication.trigger_turn
|
||||
)
|
||||
}));
|
||||
@@ -1499,7 +1503,7 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_message() {
|
||||
async fn multi_agent_v2_list_agents_returns_completed_status_without_encrypted_spawn_preview() {
|
||||
let (mut session, mut turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let root = manager
|
||||
@@ -1585,10 +1589,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
|
||||
.find(|agent| agent.agent_name == "/root/worker")
|
||||
.expect("worker agent should be listed");
|
||||
assert_eq!(worker.agent_status, json!({"completed": "done"}));
|
||||
assert_eq!(
|
||||
worker.last_task_message.as_deref(),
|
||||
Some("inspect this repo")
|
||||
);
|
||||
assert_eq!(worker.last_task_message, None);
|
||||
assert_eq!(success, Some(true));
|
||||
}
|
||||
|
||||
@@ -1867,7 +1868,8 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
|
||||
if communication.author == AgentPath::root()
|
||||
&& communication.recipient.as_str() == "/root/worker"
|
||||
&& communication.other_recipients.is_empty()
|
||||
&& communication.content == "continue"
|
||||
&& communication.content.is_empty()
|
||||
&& communication.encrypted_content.as_deref() == Some("continue")
|
||||
&& !communication.trigger_turn
|
||||
)));
|
||||
}
|
||||
@@ -4373,7 +4375,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
expected.base_instructions = Some(base_instructions.text);
|
||||
expected.model = Some(turn.model_info.slug.clone());
|
||||
expected.model_provider = turn.provider.info().clone();
|
||||
expected.model_reasoning_effort = turn.reasoning_effort;
|
||||
expected.model_reasoning_effort = turn.reasoning_effort.clone();
|
||||
expected.model_reasoning_summary = Some(turn.reasoning_summary);
|
||||
expected.developer_instructions = turn.developer_instructions.clone();
|
||||
expected.compact_prompt = turn.compact_prompt.clone();
|
||||
@@ -4399,7 +4401,10 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
async fn build_agent_spawn_config_preserves_base_user_instructions() {
|
||||
let (_session, mut turn) = make_session_and_context().await;
|
||||
let mut base_config = (*turn.config).clone();
|
||||
base_config.user_instructions = Some("base-user".to_string());
|
||||
base_config.user_instructions = Some(LoadedAgentsMd::new_user(
|
||||
"base-user".to_string(),
|
||||
base_config.codex_home.join("AGENTS.md"),
|
||||
));
|
||||
turn.user_instructions = Some("resolved-user".to_string());
|
||||
turn.config = Arc::new(base_config.clone());
|
||||
let base_instructions = BaseInstructions {
|
||||
@@ -4427,7 +4432,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
|
||||
expected.base_instructions = None;
|
||||
expected.model = Some(turn.model_info.slug.clone());
|
||||
expected.model_provider = turn.provider.info().clone();
|
||||
expected.model_reasoning_effort = turn.reasoning_effort;
|
||||
expected.model_reasoning_effort = turn.reasoning_effort.clone();
|
||||
expected.model_reasoning_summary = Some(turn.reasoning_summary);
|
||||
expected.developer_instructions = turn.developer_instructions.clone();
|
||||
expected.compact_prompt = turn.compact_prompt.clone();
|
||||
|
||||
@@ -22,6 +22,7 @@ use codex_protocol::protocol::CollabCloseBeginEvent;
|
||||
use codex_protocol::protocol::CollabCloseEndEvent;
|
||||
use codex_protocol::protocol::CollabWaitingBeginEvent;
|
||||
use codex_protocol::protocol::CollabWaitingEndEvent;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_tools::ToolName;
|
||||
use serde::Deserialize;
|
||||
@@ -42,3 +43,17 @@ mod message_tool;
|
||||
mod send_message;
|
||||
mod spawn;
|
||||
pub(crate) mod wait;
|
||||
|
||||
pub(super) fn communication_from_tool_message(
|
||||
author: AgentPath,
|
||||
recipient: AgentPath,
|
||||
message: String,
|
||||
) -> InterAgentCommunication {
|
||||
InterAgentCommunication::new_encrypted(
|
||||
author,
|
||||
recipient,
|
||||
Vec::new(),
|
||||
message,
|
||||
/*trigger_turn*/ true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Shared argument parsing and dispatch for the v2 text-only agent messaging tools.
|
||||
//! Shared argument parsing and dispatch for the v2 agent messaging tools.
|
||||
//!
|
||||
//! `send_message` and `followup_task` share the same submission path and differ only in whether the
|
||||
//! resulting `InterAgentCommunication` should wake the target immediately.
|
||||
@@ -55,20 +55,21 @@ fn message_content(message: String) -> Result<String, FunctionCallError> {
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Handles the shared MultiAgentV2 plain-text message flow for both `send_message` and `followup_task`.
|
||||
/// Handles the shared MultiAgentV2 message flow for both `send_message` and `followup_task`.
|
||||
pub(crate) async fn handle_message_string_tool(
|
||||
invocation: ToolInvocation,
|
||||
mode: MessageDeliveryMode,
|
||||
target: String,
|
||||
message: String,
|
||||
) -> Result<FunctionToolOutput, FunctionCallError> {
|
||||
let prompt = message_content(message)?;
|
||||
let message = message_content(message)?;
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
call_id,
|
||||
..
|
||||
} = invocation;
|
||||
let prompt = String::new();
|
||||
let receiver_thread_id = resolve_agent_target(&session, &turn, &target).await?;
|
||||
let receiver_agent = session
|
||||
.services
|
||||
@@ -101,15 +102,11 @@ pub(crate) async fn handle_message_string_tool(
|
||||
let receiver_agent_path = receiver_agent.agent_path.clone().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel("target agent is missing an agent_path".to_string())
|
||||
})?;
|
||||
let communication = InterAgentCommunication::new(
|
||||
turn.session_source
|
||||
.get_agent_path()
|
||||
.unwrap_or_else(AgentPath::root),
|
||||
receiver_agent_path,
|
||||
Vec::new(),
|
||||
prompt.clone(),
|
||||
/*trigger_turn*/ true,
|
||||
);
|
||||
let author = turn
|
||||
.session_source
|
||||
.get_agent_path()
|
||||
.unwrap_or_else(AgentPath::root);
|
||||
let communication = communication_from_tool_message(author, receiver_agent_path, message);
|
||||
let result = session
|
||||
.services
|
||||
.agent_control
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::*;
|
||||
use crate::agent::control::SpawnAgentForkMode;
|
||||
use crate::agent::control::SpawnAgentOptions;
|
||||
use crate::agent::control::render_input_preview;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::agent::role::DEFAULT_ROLE_NAME;
|
||||
use crate::agent::role::apply_role_to_config;
|
||||
@@ -9,7 +8,6 @@ use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
@@ -61,8 +59,9 @@ async fn handle_spawn_agent(
|
||||
.map(str::trim)
|
||||
.filter(|role| !role.is_empty());
|
||||
|
||||
let message = args.message.clone();
|
||||
let initial_operation = parse_collab_input(Some(args.message), /*items*/ None)?;
|
||||
let prompt = render_input_preview(&initial_operation);
|
||||
let prompt = String::new();
|
||||
|
||||
let session_source = turn.session_source.clone();
|
||||
let child_depth = next_thread_spawn_depth(&session_source);
|
||||
@@ -75,7 +74,7 @@ async fn handle_spawn_agent(
|
||||
sender_thread_id: session.thread_id,
|
||||
prompt: prompt.clone(),
|
||||
model: args.model.clone().unwrap_or_default(),
|
||||
reasoning_effort: args.reasoning_effort.unwrap_or_default(),
|
||||
reasoning_effort: args.reasoning_effort.clone().unwrap_or_default(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
@@ -86,14 +85,18 @@ async fn handle_spawn_agent(
|
||||
config.service_tier = Some(service_tier.clone());
|
||||
}
|
||||
if matches!(fork_mode, Some(SpawnAgentForkMode::FullHistory)) {
|
||||
reject_full_fork_spawn_overrides(role_name, args.model.as_deref(), args.reasoning_effort)?;
|
||||
reject_full_fork_spawn_overrides(
|
||||
role_name,
|
||||
args.model.as_deref(),
|
||||
args.reasoning_effort.clone(),
|
||||
)?;
|
||||
} else {
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
&mut config,
|
||||
args.model.as_deref(),
|
||||
args.reasoning_effort,
|
||||
args.reasoning_effort.clone(),
|
||||
)
|
||||
.await?;
|
||||
apply_role_to_config(&mut config, role_name)
|
||||
@@ -125,17 +128,12 @@ async fn handle_spawn_agent(
|
||||
.iter()
|
||||
.all(|item| matches!(item, UserInput::Text { .. })) =>
|
||||
{
|
||||
Op::InterAgentCommunication {
|
||||
communication: InterAgentCommunication::new(
|
||||
turn.session_source
|
||||
.get_agent_path()
|
||||
.unwrap_or_else(AgentPath::root),
|
||||
recipient,
|
||||
Vec::new(),
|
||||
prompt.clone(),
|
||||
/*trigger_turn*/ true,
|
||||
),
|
||||
}
|
||||
let author = turn
|
||||
.session_source
|
||||
.get_agent_path()
|
||||
.unwrap_or_else(AgentPath::root);
|
||||
let communication = communication_from_tool_message(author, recipient, message);
|
||||
Op::InterAgentCommunication { communication }
|
||||
}
|
||||
(_, initial_operation) => initial_operation,
|
||||
},
|
||||
@@ -188,7 +186,7 @@ async fn handle_spawn_agent(
|
||||
.unwrap_or_else(|| args.model.clone().unwrap_or_default());
|
||||
let effective_reasoning_effort = agent_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.reasoning_effort)
|
||||
.and_then(|snapshot| snapshot.reasoning_effort.clone())
|
||||
.unwrap_or(args.reasoning_effort.unwrap_or_default());
|
||||
let nickname = new_agent_nickname.clone();
|
||||
session
|
||||
|
||||
@@ -160,7 +160,6 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
);
|
||||
emitter.begin(event_ctx).await;
|
||||
|
||||
let file_system_sandbox_policy = turn.file_system_sandbox_policy();
|
||||
let exec_approval_requirement = session
|
||||
.services
|
||||
.exec_policy
|
||||
@@ -168,7 +167,6 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
command: &exec_params.command,
|
||||
approval_policy: turn.approval_policy.value(),
|
||||
permission_profile: turn.permission_profile(),
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
#[allow(deprecated)]
|
||||
sandbox_cwd: turn.cwd.as_path(),
|
||||
sandbox_permissions: if effective_additional_permissions.permissions_preapproved {
|
||||
|
||||
@@ -182,7 +182,7 @@ impl ToolExecutor<ToolInvocation> for ShellCommandHandler {
|
||||
session.thread_id,
|
||||
turn.config.permissions.allow_login_shell,
|
||||
)?;
|
||||
let shell_type = Some(session.user_shell().shell_type.clone());
|
||||
let shell_type = Some(session.user_shell().shell_type);
|
||||
run_exec_like(RunExecLikeArgs {
|
||||
tool_name,
|
||||
exec_params,
|
||||
|
||||
@@ -122,7 +122,7 @@ pub(crate) fn get_command(
|
||||
let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref());
|
||||
Ok(ResolvedCommand {
|
||||
command: shell.derive_exec_args(&args.cmd, use_login_shell),
|
||||
shell_type: shell.shell_type.clone(),
|
||||
shell_type: shell.shell_type,
|
||||
})
|
||||
}
|
||||
UnifiedExecShellMode::ZshFork(zsh_fork_config) => {
|
||||
|
||||
@@ -39,6 +39,7 @@ use codex_protocol::protocol::NetworkPolicyRuleAction;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use std::time::Instant;
|
||||
|
||||
pub(crate) struct ToolOrchestrator {
|
||||
sandbox: SandboxManager,
|
||||
@@ -256,6 +257,7 @@ impl ToolOrchestrator {
|
||||
network_denial_cancellation_token: None,
|
||||
};
|
||||
|
||||
let initial_attempt_start = Instant::now();
|
||||
let (first_result, first_deferred_network_approval) = Self::run_attempt(
|
||||
tool,
|
||||
req,
|
||||
@@ -264,6 +266,7 @@ impl ToolOrchestrator {
|
||||
managed_network_active,
|
||||
)
|
||||
.await;
|
||||
let initial_duration = initial_attempt_start.elapsed();
|
||||
match first_result {
|
||||
Ok(out) => {
|
||||
// We have a successful initial result
|
||||
@@ -284,12 +287,26 @@ impl ToolOrchestrator {
|
||||
None
|
||||
};
|
||||
if network_policy_decision.is_some() && network_approval_context.is_none() {
|
||||
otel.sandbox_outcome(
|
||||
&otel_tn,
|
||||
otel_ci,
|
||||
"denied",
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
})));
|
||||
}
|
||||
if !tool.escalate_on_failure() {
|
||||
otel.sandbox_outcome(
|
||||
&otel_tn,
|
||||
otel_ci,
|
||||
"denied",
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
@@ -312,6 +329,13 @@ impl ToolOrchestrator {
|
||||
ExecApprovalRequirement::NeedsApproval { .. }
|
||||
);
|
||||
if !allow_on_request_network_prompt {
|
||||
otel.sandbox_outcome(
|
||||
&otel_tn,
|
||||
otel_ci,
|
||||
"denied",
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
@@ -319,6 +343,13 @@ impl ToolOrchestrator {
|
||||
}
|
||||
}
|
||||
if !unsandboxed_allowed && network_approval_context.is_none() {
|
||||
otel.sandbox_outcome(
|
||||
&otel_tn,
|
||||
otel_ci,
|
||||
"denied",
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
@@ -400,15 +431,51 @@ impl ToolOrchestrator {
|
||||
};
|
||||
|
||||
// Second attempt.
|
||||
let escalated_attempt_start = Instant::now();
|
||||
let (retry_result, retry_deferred_network_approval) =
|
||||
Self::run_attempt(tool, req, tool_ctx, &retry_attempt, managed_network_active)
|
||||
.await;
|
||||
retry_result.map(|output| OrchestratorRunResult {
|
||||
output,
|
||||
deferred_network_approval: retry_deferred_network_approval,
|
||||
})
|
||||
let escalated_duration = escalated_attempt_start.elapsed();
|
||||
match retry_result {
|
||||
Ok(output) => {
|
||||
otel.sandbox_outcome(
|
||||
&otel_tn,
|
||||
otel_ci,
|
||||
"escalated",
|
||||
initial_duration,
|
||||
Some(escalated_duration),
|
||||
);
|
||||
Ok(OrchestratorRunResult {
|
||||
output,
|
||||
deferred_network_approval: retry_deferred_network_approval,
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(outcome) = sandbox_outcome_from_tool_error(&err) {
|
||||
otel.sandbox_outcome(
|
||||
&otel_tn,
|
||||
otel_ci,
|
||||
outcome,
|
||||
initial_duration,
|
||||
Some(escalated_duration),
|
||||
);
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(outcome) = sandbox_outcome_from_tool_error(&err) {
|
||||
otel.sandbox_outcome(
|
||||
&otel_tn,
|
||||
otel_ci,
|
||||
outcome,
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,6 +576,15 @@ impl ToolOrchestrator {
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_outcome_from_tool_error(err: &ToolError) -> Option<&'static str> {
|
||||
match err {
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { .. })) => Some("denied"),
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Timeout { .. })) => Some("timed_out"),
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Signal(_))) => Some("signal"),
|
||||
ToolError::Rejected(_) | ToolError::Codex(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_denial_reason_from_output(_output: &ExecToolCallOutput) -> String {
|
||||
// Keep approval reason terse and stable for UX/tests, but accept the
|
||||
// output so we can evolve heuristics later without touching call sites.
|
||||
|
||||
@@ -622,7 +622,6 @@ impl EscalationPolicy for CoreShellActionProvider {
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: self.approval_policy,
|
||||
permission_profile: self.permission_profile.clone(),
|
||||
file_system_sandbox_policy: &self.file_system_sandbox_policy,
|
||||
sandbox_cwd: self.sandbox_policy_cwd.as_path(),
|
||||
sandbox_permissions: self.approval_sandbox_permissions,
|
||||
enable_shell_wrapper_parsing:
|
||||
@@ -677,7 +676,6 @@ fn evaluate_intercepted_exec_policy(
|
||||
let InterceptedExecPolicyContext {
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
sandbox_permissions,
|
||||
enable_shell_wrapper_parsing,
|
||||
@@ -705,7 +703,6 @@ fn evaluate_intercepted_exec_policy(
|
||||
crate::exec_policy::UnmatchedCommandContext {
|
||||
approval_policy,
|
||||
permission_profile: &permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
sandbox_permissions,
|
||||
used_complex_parsing,
|
||||
@@ -727,7 +724,6 @@ fn evaluate_intercepted_exec_policy(
|
||||
struct InterceptedExecPolicyContext<'a> {
|
||||
approval_policy: AskForApproval,
|
||||
permission_profile: PermissionProfile,
|
||||
file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
sandbox_cwd: &'a Path,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
enable_shell_wrapper_parsing: bool,
|
||||
|
||||
@@ -522,7 +522,6 @@ fn evaluate_intercepted_exec_policy_uses_wrapper_command_when_shell_wrapper_pars
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: sandbox_cwd.as_path(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
enable_shell_wrapper_parsing: enable_intercepted_exec_policy_shell_wrapper_parsing,
|
||||
@@ -575,7 +574,6 @@ fn evaluate_intercepted_exec_policy_matches_inner_shell_commands_when_enabled()
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: sandbox_cwd.as_path(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
enable_shell_wrapper_parsing: enable_intercepted_exec_policy_shell_wrapper_parsing,
|
||||
@@ -619,7 +617,6 @@ host_executable(name = "git", paths = ["{git_path_literal}"])
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: sandbox_cwd.as_path(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
enable_shell_wrapper_parsing: false,
|
||||
@@ -747,7 +744,6 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default(
|
||||
let argv = ["printf".to_string(), "hello".to_string()];
|
||||
let approval_policy = AskForApproval::OnRequest;
|
||||
let permission_profile = PermissionProfile::workspace_write();
|
||||
let file_system_sandbox_policy = read_only_file_system_sandbox_policy();
|
||||
let sandbox_cwd = test_sandbox_cwd();
|
||||
|
||||
let preapproved = evaluate_intercepted_exec_policy(
|
||||
@@ -757,7 +753,6 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default(
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy,
|
||||
permission_profile: permission_profile.clone(),
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
sandbox_cwd: sandbox_cwd.as_path(),
|
||||
sandbox_permissions: super::approval_sandbox_permissions(
|
||||
SandboxPermissions::WithAdditionalPermissions,
|
||||
@@ -773,7 +768,6 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default(
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
sandbox_cwd: sandbox_cwd.as_path(),
|
||||
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
|
||||
enable_shell_wrapper_parsing: false,
|
||||
@@ -808,7 +802,6 @@ host_executable(name = "git", paths = ["{allowed_git_literal}"])
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: sandbox_cwd.as_path(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
enable_shell_wrapper_parsing: false,
|
||||
|
||||
@@ -207,7 +207,12 @@ fn build_model_visible_specs_and_registry(
|
||||
if exposure.is_direct() && !is_hidden_by_code_mode_only(turn_context, &tool_name, exposure)
|
||||
{
|
||||
let spec = runtime.spec();
|
||||
specs.push(spec_for_model_request(turn_context, exposure, spec));
|
||||
specs.push(spec_for_model_request(
|
||||
turn_context,
|
||||
exposure,
|
||||
&tool_name,
|
||||
spec,
|
||||
));
|
||||
}
|
||||
}
|
||||
specs.extend(hosted_specs);
|
||||
@@ -226,12 +231,14 @@ fn build_model_visible_specs_and_registry(
|
||||
fn spec_for_model_request(
|
||||
turn_context: &TurnContext,
|
||||
exposure: ToolExposure,
|
||||
tool_name: &ToolName,
|
||||
spec: ToolSpec,
|
||||
) -> ToolSpec {
|
||||
if matches!(
|
||||
turn_context.tool_mode,
|
||||
ToolMode::CodeMode | ToolMode::CodeModeOnly
|
||||
) && exposure != ToolExposure::DirectModelOnly
|
||||
&& !is_excluded_from_code_mode(turn_context, tool_name)
|
||||
&& codex_code_mode::is_code_mode_nested_tool(spec.name())
|
||||
{
|
||||
codex_tools::augment_tool_spec_for_code_mode(spec)
|
||||
@@ -405,10 +412,19 @@ fn is_hidden_by_code_mode_only(
|
||||
))
|
||||
}
|
||||
|
||||
fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName) -> bool {
|
||||
tool_name.namespace.as_ref().is_some_and(|namespace| {
|
||||
turn_context
|
||||
.config
|
||||
.code_mode
|
||||
.excluded_tool_namespaces
|
||||
.contains(namespace)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_code_mode_executors(
|
||||
turn_context: &TurnContext,
|
||||
executors: &[Arc<dyn CoreToolRuntime>],
|
||||
deferred_tools_available: bool,
|
||||
) -> Vec<Arc<dyn CoreToolRuntime>> {
|
||||
if !matches!(
|
||||
turn_context.tool_mode,
|
||||
@@ -419,6 +435,8 @@ fn build_code_mode_executors(
|
||||
|
||||
let mut code_mode_nested_tool_specs = Vec::new();
|
||||
let mut exec_prompt_tool_specs = Vec::new();
|
||||
let mut deferred_tools_available = false;
|
||||
let deferred_tools_guidance_enabled = search_tool_enabled(turn_context);
|
||||
for executor in executors {
|
||||
let exposure = executor.exposure();
|
||||
if exposure == ToolExposure::DirectModelOnly {
|
||||
@@ -428,9 +446,19 @@ fn build_code_mode_executors(
|
||||
if exposure == ToolExposure::Hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_excluded_from_code_mode(turn_context, &executor.tool_name()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let spec = executor.spec();
|
||||
|
||||
if exposure != ToolExposure::Deferred {
|
||||
if exposure == ToolExposure::Deferred {
|
||||
// Only show deferred-tool guidance when supported and an included spec is usable by code mode.
|
||||
deferred_tools_available |= deferred_tools_guidance_enabled
|
||||
&& !collect_code_mode_exec_prompt_tool_definitions(std::iter::once(&spec))
|
||||
.is_empty();
|
||||
} else {
|
||||
exec_prompt_tool_specs.push(spec.clone());
|
||||
}
|
||||
code_mode_nested_tool_specs.push(spec);
|
||||
@@ -734,10 +762,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mu
|
||||
SpawnAgentHandler::new(SpawnAgentToolOptions {
|
||||
available_models: turn_context.available_models.clone(),
|
||||
agent_type_description,
|
||||
hide_agent_type_model_reasoning: turn_context
|
||||
.config
|
||||
.multi_agent_v2
|
||||
.hide_spawn_agent_metadata,
|
||||
hide_agent_type_model_reasoning: false,
|
||||
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
|
||||
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
|
||||
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
|
||||
@@ -839,16 +864,7 @@ fn prepend_code_mode_executors(
|
||||
planned_tools: &mut PlannedTools,
|
||||
) {
|
||||
let turn_context = context.turn_context;
|
||||
let deferred_tools_available = search_tool_enabled(turn_context)
|
||||
&& planned_tools
|
||||
.runtimes()
|
||||
.iter()
|
||||
.any(|executor| executor.exposure() == ToolExposure::Deferred);
|
||||
let code_mode_executors = build_code_mode_executors(
|
||||
turn_context,
|
||||
planned_tools.runtimes(),
|
||||
deferred_tools_available,
|
||||
);
|
||||
let code_mode_executors = build_code_mode_executors(turn_context, planned_tools.runtimes());
|
||||
planned_tools.runtimes.splice(0..0, code_mode_executors);
|
||||
}
|
||||
|
||||
|
||||
@@ -938,6 +938,42 @@ async fn code_mode_only_exposes_code_executor_and_hides_nested_tools() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn excluded_deferred_namespaces_do_not_enable_nested_tool_guidance() {
|
||||
let plan = probe_with(
|
||||
|turn| {
|
||||
set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]);
|
||||
set_feature(turn, Feature::Collab, /*enabled*/ false);
|
||||
turn.model_info.supports_search_tool = true;
|
||||
update_config(turn, |config| {
|
||||
config.code_mode.excluded_tool_namespaces = vec!["excluded".to_string()];
|
||||
});
|
||||
},
|
||||
ToolPlanInputs {
|
||||
dynamic_tools: vec![dynamic_tool(
|
||||
Some("excluded"),
|
||||
"lookup",
|
||||
/*defer_loading*/ true,
|
||||
)],
|
||||
..ToolPlanInputs::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let ToolSpec::Freeform(exec) = plan.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME) else {
|
||||
panic!("expected code mode exec tool");
|
||||
};
|
||||
assert!(
|
||||
!exec
|
||||
.description
|
||||
.contains("Some deferred nested tools may be omitted")
|
||||
);
|
||||
plan.assert_registered_contains(&[
|
||||
&ToolName::namespaced("excluded", "lookup").to_string(),
|
||||
"tool_search",
|
||||
]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_feature_selects_one_agent_tool_family() {
|
||||
let v1 = probe(|turn| {
|
||||
@@ -967,6 +1003,30 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
|
||||
"wait_agent".to_string(),
|
||||
]
|
||||
);
|
||||
let ToolSpec::Namespace(namespace) = v1.visible_spec(MULTI_AGENT_V1_NAMESPACE) else {
|
||||
panic!("expected v1 multi-agent namespace");
|
||||
};
|
||||
let Some(ResponsesApiNamespaceTool::Function(spawn_agent)) =
|
||||
namespace.tools.iter().find(|tool| {
|
||||
matches!(
|
||||
tool,
|
||||
ResponsesApiNamespaceTool::Function(tool) if tool.name == "spawn_agent"
|
||||
)
|
||||
})
|
||||
else {
|
||||
panic!("expected v1 spawn_agent function");
|
||||
};
|
||||
let properties = spawn_agent
|
||||
.parameters
|
||||
.properties
|
||||
.as_ref()
|
||||
.expect("spawn_agent should use object params");
|
||||
for property in ["agent_type", "model", "reasoning_effort", "service_tier"] {
|
||||
assert!(
|
||||
properties.contains_key(property),
|
||||
"expected v1 spawn_agent to expose `{property}`"
|
||||
);
|
||||
}
|
||||
|
||||
let v2 = probe(|turn| {
|
||||
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true);
|
||||
@@ -1011,6 +1071,30 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_message_schemas_are_encrypted() {
|
||||
let plan = probe(|turn| {
|
||||
set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true);
|
||||
})
|
||||
.await;
|
||||
for tool_name in ["spawn_agent", "send_message", "followup_task"] {
|
||||
let ToolSpec::Function(tool) = plan.visible_spec(tool_name) else {
|
||||
panic!("expected {tool_name} function spec");
|
||||
};
|
||||
let properties = tool
|
||||
.parameters
|
||||
.properties
|
||||
.as_ref()
|
||||
.expect("tool should use object params");
|
||||
assert_eq!(
|
||||
properties
|
||||
.get("message")
|
||||
.and_then(|schema| schema.encrypted),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_mode_selector_overrides_feature_flags() {
|
||||
let direct = probe(|turn| {
|
||||
|
||||
@@ -147,6 +147,7 @@ fn response_event_records_turn_ttft(event: &ResponseEvent) -> bool {
|
||||
ResponseEvent::Created
|
||||
| ResponseEvent::ServerModel(_)
|
||||
| ResponseEvent::ModelVerifications(_)
|
||||
| ResponseEvent::TurnModerationMetadata(_)
|
||||
| ResponseEvent::ServerReasoningIncluded(_)
|
||||
| ResponseEvent::ToolCallInputDelta { .. }
|
||||
| ResponseEvent::Completed { .. }
|
||||
@@ -177,6 +178,7 @@ fn response_item_records_turn_ttft(item: &ResponseItem) -> bool {
|
||||
})
|
||||
})
|
||||
}
|
||||
ResponseItem::AgentMessage { .. } => false,
|
||||
ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::CustomToolCall { .. }
|
||||
|
||||
@@ -1011,7 +1011,6 @@ impl UnifiedExecProcessManager {
|
||||
};
|
||||
let mut orchestrator = ToolOrchestrator::new();
|
||||
let mut runtime = UnifiedExecRuntime::new(self, request.shell_mode.clone());
|
||||
let file_system_sandbox_policy = context.turn.file_system_sandbox_policy();
|
||||
let exec_approval_requirement = context
|
||||
.session
|
||||
.services
|
||||
@@ -1020,7 +1019,6 @@ impl UnifiedExecProcessManager {
|
||||
command: &request.command,
|
||||
approval_policy: context.turn.approval_policy.value(),
|
||||
permission_profile: context.turn.permission_profile(),
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
// The process cwd may be model-controlled. Policy resolution
|
||||
// stays anchored to the selected turn environment cwd instead.
|
||||
sandbox_cwd: request.sandbox_cwd.as_path(),
|
||||
@@ -1034,7 +1032,7 @@ impl UnifiedExecProcessManager {
|
||||
.await;
|
||||
let req = UnifiedExecToolRequest {
|
||||
command: request.command.clone(),
|
||||
shell_type: request.shell_type.clone(),
|
||||
shell_type: request.shell_type,
|
||||
hook_command: request.hook_command.clone(),
|
||||
process_id: request.process_id,
|
||||
cwd,
|
||||
|
||||
Reference in New Issue
Block a user