Compare commits

...

15 Commits

Author SHA1 Message Date
David Wiesen
a68f7fcc7c Avoid fatal ACL probe errors during sandbox refresh 2026-04-08 10:00:43 -07:00
Ahmed Ibrahim
06d88b7e81 Add realtime transport config (#17097)
Adds realtime.transport config with websocket as the default and webrtc
wired through the effective config.

Co-authored-by: Codex <noreply@openai.com>
2026-04-08 09:53:53 -07:00
Eric Traut
18171b1931 Skip MCP auth probing for disabled servers (#17098)
Addresses #16971

Problem: Disabled MCP servers were still queried for streamable HTTP
auth status during MCP inventory, so unreachable disabled entries could
add startup latency.

Solution: Return `Unsupported` immediately for disabled MCP server
configs before bearer token/OAuth status discovery.
2026-04-08 09:36:07 -07:00
Eric Traut
5c95e4588e Fix TUI crash when resuming the current thread (#17086)
Problem: Resuming the live TUI thread through `/resume` could
unsubscribe and reconnect the same app-server thread, leaving the UI
crashed or disconnected.

Solution: No-op `/resume` only when the selected thread is the currently
attached active thread; keep the normal resume path for
stale/displayed-only threads so recovery and reattach still work.
2026-04-08 09:35:54 -07:00
Eric Traut
dc5feb916d Show global AGENTS.md in /status (#17091)
Addresses #3793

Problem: /status only reported project-level AGENTS files, so sessions
with a loaded global $CODEX_HOME/AGENTS.md still showed Agents.md as
<none>.

Solution: Track the global instructions file loaded during config
initialization and prepend that path to the /status Agents.md summary,
with coverage for AGENTS.md, AGENTS.override.md, and global-plus-project
ordering.
2026-04-08 09:04:32 -07:00
pakrym-oai
4c07dd4d25 Configure multi_agent_v2 spawn agent hints (#17071)
Allow multi_agent_v2 features to have its own temporary configuration
under `[features.multi_agent_v2]`

```
[features.multi_agent_v2]
enabled = true
usage_hint_enabled = false
usage_hint_text = "Custom delegation guidance."
hide_spawn_agent_metadata = true
```

Absent `usage_hint_text` means use the default hint.

```
[features]
multi_agent_v2 = true
```

still works as the boolean shorthand.
2026-04-08 08:42:18 -07:00
jif-oai
2250fdd54a codex debug 14 (guardian approved) (#17130)
Removes lines 92-98 from core/templates/agents/orchestrator.md.
2026-04-08 14:14:32 +01:00
jif-oai
34fd336e7b codex debug 12 (guardian approved) (#17128)
Removes lines 78-84 from core/templates/agents/orchestrator.md.
2026-04-08 14:14:28 +01:00
jif-oai
6ee4680a81 codex debug 10 (guardian approved) (#17126)
Removes lines 64-70 from core/templates/agents/orchestrator.md.
2026-04-08 14:14:24 +01:00
jif-oai
34422855bb codex debug 8 (guardian approved) (#17124)
Removes lines 50-56 from core/templates/agents/orchestrator.md.
2026-04-08 14:14:19 +01:00
jif-oai
9601f2af4b codex debug 6 (guardian approved) (#17122)
Removes lines 36-42 from core/templates/agents/orchestrator.md.
2026-04-08 14:14:15 +01:00
jif-oai
99a12b78c2 codex debug 4 (guardian approved) (#17120)
Removes lines 22-28 from core/templates/agents/orchestrator.md.
2026-04-08 14:14:11 +01:00
jif-oai
11eff760d1 codex debug 2 (guardian approved) (#17118)
Removes lines 8-14 from core/templates/agents/orchestrator.md.
2026-04-08 14:14:06 +01:00
jif-oai
2b65f24de6 codex debug 15 (guardian approved) (#17131)
Removes lines 99-106 from core/templates/agents/orchestrator.md.
2026-04-08 14:11:01 +01:00
jif-oai
95d27bfe8c codex debug 13 (guardian approved) (#17129)
Removes lines 85-91 from core/templates/agents/orchestrator.md.
2026-04-08 14:10:54 +01:00
24 changed files with 746 additions and 103 deletions

View File

@@ -157,6 +157,10 @@ async fn compute_auth_status(
config: &McpServerConfig,
store_mode: OAuthCredentialsStoreMode,
) -> Result<McpAuthStatus> {
if !config.enabled {
return Ok(McpAuthStatus::Unsupported);
}
match &config.transport {
McpServerTransportConfig::Stdio { .. } => Ok(McpAuthStatus::Unsupported),
McpServerTransportConfig::StreamableHttp {

View File

@@ -445,6 +445,15 @@ pub enum RealtimeWsMode {
Transcription,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RealtimeTransport {
#[serde(rename = "webrtc")]
WebRtc,
#[default]
Websocket,
}
pub use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion;
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
@@ -453,6 +462,7 @@ pub struct RealtimeConfig {
pub version: RealtimeWsVersion,
#[serde(rename = "type")]
pub session_type: RealtimeWsMode,
pub transport: RealtimeTransport,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
@@ -461,6 +471,7 @@ pub struct RealtimeToml {
pub version: Option<RealtimeWsVersion>,
#[serde(rename = "type")]
pub session_type: Option<RealtimeWsMode>,
pub transport: Option<RealtimeTransport>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]

View File

@@ -25,6 +25,15 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
if feature.id == codex_features::Feature::Artifact {
continue;
}
if feature.id == codex_features::Feature::MultiAgentV2 {
validation.properties.insert(
feature.key.to_string(),
schema_gen.subschema_for::<codex_features::FeatureToml<
codex_features::MultiAgentV2ConfigToml,
>>(),
);
continue;
}
validation
.properties
.insert(feature.key.to_string(), schema_gen.subschema_for::<bool>());

View File

@@ -362,9 +362,6 @@
"connectors": {
"type": "boolean"
},
"debug_hide_spawn_agent_metadata": {
"type": "boolean"
},
"default_mode_request_user_input": {
"type": "boolean"
},
@@ -426,7 +423,7 @@
"type": "boolean"
},
"multi_agent_v2": {
"type": "boolean"
"$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml"
},
"personality": {
"type": "boolean"
@@ -621,6 +618,16 @@
},
"type": "object"
},
"FeatureToml_for_MultiAgentV2ConfigToml": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/definitions/MultiAgentV2ConfigToml"
}
]
},
"FeedbackConfigToml": {
"additionalProperties": false,
"properties": {
@@ -980,6 +987,24 @@
],
"type": "object"
},
"MultiAgentV2ConfigToml": {
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"hide_spawn_agent_metadata": {
"type": "boolean"
},
"usage_hint_enabled": {
"type": "boolean"
},
"usage_hint_text": {
"type": "string"
}
},
"type": "object"
},
"NetworkDomainPermissionToml": {
"enum": [
"allow",
@@ -1472,6 +1497,9 @@
"RealtimeToml": {
"additionalProperties": false,
"properties": {
"transport": {
"$ref": "#/definitions/RealtimeTransport"
},
"type": {
"$ref": "#/definitions/RealtimeWsMode"
},
@@ -1481,6 +1509,13 @@
},
"type": "object"
},
"RealtimeTransport": {
"enum": [
"webrtc",
"websocket"
],
"type": "string"
},
"RealtimeWsMode": {
"enum": [
"conversational",
@@ -2075,9 +2110,6 @@
"connectors": {
"type": "boolean"
},
"debug_hide_spawn_agent_metadata": {
"type": "boolean"
},
"default_mode_request_user_input": {
"type": "boolean"
},
@@ -2139,7 +2171,7 @@
"type": "boolean"
},
"multi_agent_v2": {
"type": "boolean"
"$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml"
},
"personality": {
"type": "boolean"

View File

@@ -964,6 +964,9 @@ impl TurnContext {
.with_web_search_config(self.tools_config.web_search_config.clone())
.with_allow_login_shell(self.tools_config.allow_login_shell)
.with_has_environment(self.tools_config.has_environment)
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
&config.agent_roles,
));
@@ -1488,6 +1491,9 @@ impl Session {
.with_web_search_config(per_turn_config.web_search_config.clone())
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
.with_has_environment(environment.is_some())
.with_spawn_agent_usage_hint(per_turn_config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(per_turn_config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(per_turn_config.multi_agent_v2.hide_spawn_agent_metadata)
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
&per_turn_config.agent_roles,
));
@@ -5676,6 +5682,9 @@ async fn spawn_review_thread(
.with_web_search_config(/*web_search_config*/ None)
.with_allow_login_shell(config.permissions.allow_login_shell)
.with_has_environment(parent_turn_context.environment.is_some())
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
&config.agent_roles,
));

View File

@@ -12,6 +12,7 @@ use codex_config::config_toml::ProjectConfig;
use codex_config::config_toml::RealtimeAudioConfig;
use codex_config::config_toml::RealtimeConfig;
use codex_config::config_toml::RealtimeToml;
use codex_config::config_toml::RealtimeTransport;
use codex_config::config_toml::RealtimeWsMode;
use codex_config::config_toml::RealtimeWsVersion;
use codex_config::config_toml::ToolsToml;
@@ -128,6 +129,56 @@ fn load_config_normalizes_relative_cwd_override() -> std::io::Result<()> {
Ok(())
}
#[test]
fn load_config_records_global_agents_path() -> std::io::Result<()> {
let codex_home = tempdir()?;
let global_agents_path = codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME);
std::fs::write(&global_agents_path, "\n global instructions \n")?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
codex_home.abs().into_path_buf(),
)?;
assert_eq!(
config.user_instructions.as_deref(),
Some("global instructions")
);
assert_eq!(
config.user_instructions_path.as_deref(),
Some(global_agents_path.as_path())
);
Ok(())
}
#[test]
fn load_config_records_preferred_global_agents_override_path() -> std::io::Result<()> {
let codex_home = tempdir()?;
std::fs::write(
codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME),
"global instructions",
)?;
let global_agents_override_path = codex_home.path().join(LOCAL_PROJECT_DOC_FILENAME);
std::fs::write(&global_agents_override_path, "local override instructions")?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
codex_home.abs().into_path_buf(),
)?;
assert_eq!(
config.user_instructions.as_deref(),
Some("local override instructions")
);
assert_eq!(
config.user_instructions_path.as_deref(),
Some(global_agents_override_path.as_path())
);
Ok(())
}
#[test]
fn test_toml_parsing() {
let history_with_persistence = r#"
@@ -1746,7 +1797,7 @@ fn feature_table_overrides_legacy_flags() -> std::io::Result<()> {
let mut entries = BTreeMap::new();
entries.insert("apply_patch_freeform".to_string(), false);
let cfg = ConfigToml {
features: Some(FeaturesToml { entries }),
features: Some(FeaturesToml::from(entries)),
..Default::default()
};
@@ -1794,7 +1845,7 @@ fn responses_websocket_features_do_not_change_wire_api() -> std::io::Result<()>
let mut entries = BTreeMap::new();
entries.insert(feature_key.to_string(), true);
let cfg = ConfigToml {
features: Some(FeaturesToml { entries }),
features: Some(FeaturesToml::from(entries)),
..Default::default()
};
@@ -3005,14 +3056,14 @@ async fn set_feature_enabled_updates_profile() -> anyhow::Result<()> {
profile
.features
.as_ref()
.and_then(|features| features.entries.get("guardian_approval")),
Some(&true),
.and_then(|features| features.entries().get("guardian_approval").copied()),
Some(true),
);
assert_eq!(
parsed
.features
.as_ref()
.and_then(|features| features.entries.get("guardian_approval")),
.and_then(|features| features.entries().get("guardian_approval").copied()),
None,
);
@@ -3047,14 +3098,14 @@ async fn set_feature_enabled_persists_default_false_feature_disable_in_profile()
profile
.features
.as_ref()
.and_then(|features| features.entries.get("guardian_approval")),
Some(&false),
.and_then(|features| features.entries().get("guardian_approval").copied()),
Some(false),
);
assert_eq!(
parsed
.features
.as_ref()
.and_then(|features| features.entries.get("guardian_approval")),
.and_then(|features| features.entries().get("guardian_approval").copied()),
None,
);
@@ -3087,15 +3138,15 @@ async fn set_feature_enabled_profile_disable_overrides_root_enable() -> anyhow::
parsed
.features
.as_ref()
.and_then(|features| features.entries.get("guardian_approval")),
Some(&true),
.and_then(|features| features.entries().get("guardian_approval").copied()),
Some(true),
);
assert_eq!(
profile
.features
.as_ref()
.and_then(|features| features.entries.get("guardian_approval")),
Some(&false),
.and_then(|features| features.entries().get("guardian_approval").copied()),
Some(false),
);
Ok(())
@@ -4457,6 +4508,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
user_instructions: None,
user_instructions_path: None,
notify: None,
cwd: fixture.cwd(),
cli_auth_credentials_store_mode: Default::default(),
@@ -4520,6 +4572,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
multi_agent_v2: MultiAgentV2Config::default(),
features: Features::with_defaults().into(),
suppress_unstable_features_warning: false,
active_profile: Some("o3".to_string()),
@@ -4602,6 +4655,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
user_instructions: None,
user_instructions_path: None,
notify: None,
cwd: fixture.cwd(),
cli_auth_credentials_store_mode: Default::default(),
@@ -4665,6 +4719,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
multi_agent_v2: MultiAgentV2Config::default(),
features: Features::with_defaults().into(),
suppress_unstable_features_warning: false,
active_profile: Some("gpt3".to_string()),
@@ -4745,6 +4800,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
user_instructions: None,
user_instructions_path: None,
notify: None,
cwd: fixture.cwd(),
cli_auth_credentials_store_mode: Default::default(),
@@ -4808,6 +4864,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
multi_agent_v2: MultiAgentV2Config::default(),
features: Features::with_defaults().into(),
suppress_unstable_features_warning: false,
active_profile: Some("zdr".to_string()),
@@ -4874,6 +4931,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
user_instructions: None,
user_instructions_path: None,
notify: None,
cwd: fixture.cwd(),
cli_auth_credentials_store_mode: Default::default(),
@@ -4937,6 +4995,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
multi_agent_v2: MultiAgentV2Config::default(),
features: Features::with_defaults().into(),
suppress_unstable_features_warning: false,
active_profile: Some("gpt5".to_string()),
@@ -6096,6 +6155,71 @@ smart_approvals = true
Ok(())
}
#[tokio::test]
async fn multi_agent_v2_config_from_feature_table() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"[features.multi_agent_v2]
enabled = true
usage_hint_enabled = false
usage_hint_text = "Custom delegation guidance."
hide_spawn_agent_metadata = true
"#,
)?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.build()
.await?;
assert!(config.features.enabled(Feature::MultiAgentV2));
assert!(!config.multi_agent_v2.usage_hint_enabled);
assert_eq!(
config.multi_agent_v2.usage_hint_text.as_deref(),
Some("Custom delegation guidance.")
);
assert!(config.multi_agent_v2.hide_spawn_agent_metadata);
Ok(())
}
#[tokio::test]
async fn profile_multi_agent_v2_config_overrides_base() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"profile = "no_hint"
[features.multi_agent_v2]
usage_hint_enabled = true
usage_hint_text = "base hint"
hide_spawn_agent_metadata = true
[profiles.no_hint.features.multi_agent_v2]
usage_hint_enabled = false
usage_hint_text = "profile hint"
hide_spawn_agent_metadata = false
"#,
)?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.build()
.await?;
assert!(!config.multi_agent_v2.usage_hint_enabled);
assert_eq!(
config.multi_agent_v2.usage_hint_text.as_deref(),
Some("profile hint")
);
assert!(!config.multi_agent_v2.hide_spawn_agent_metadata);
Ok(())
}
#[tokio::test]
async fn feature_requirements_normalize_runtime_feature_mutations() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
@@ -6364,6 +6488,7 @@ fn realtime_loads_from_config_toml() -> std::io::Result<()> {
[realtime]
version = "v2"
type = "transcription"
transport = "webrtc"
"#,
)
.expect("TOML deserialization should succeed");
@@ -6373,6 +6498,7 @@ type = "transcription"
Some(RealtimeToml {
version: Some(RealtimeWsVersion::V2),
session_type: Some(RealtimeWsMode::Transcription),
transport: Some(RealtimeTransport::WebRtc),
})
);
@@ -6388,6 +6514,7 @@ type = "transcription"
RealtimeConfig {
version: RealtimeWsVersion::V2,
session_type: RealtimeWsMode::Transcription,
transport: RealtimeTransport::WebRtc,
}
);
Ok(())

View File

@@ -202,9 +202,9 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature
let mut explicit_settings = Vec::new();
if let Some(features) = cfg.features.as_ref() {
for (key, enabled) in &features.entries {
if let Some(feature) = feature_for_key(key) {
explicit_settings.push((format!("features.{key}"), feature, *enabled));
for (key, enabled) in features.entries() {
if let Some(feature) = feature_for_key(&key) {
explicit_settings.push((format!("features.{key}"), feature, enabled));
}
}
}
@@ -224,12 +224,12 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature
}
for (profile_name, profile) in &cfg.profiles {
if let Some(features) = profile.features.as_ref() {
for (key, enabled) in &features.entries {
if let Some(feature) = feature_for_key(key) {
for (key, enabled) in features.entries() {
if let Some(feature) = feature_for_key(&key) {
explicit_settings.push((
format!("profiles.{profile_name}.features.{key}"),
feature,
*enabled,
enabled,
));
}
}

View File

@@ -52,7 +52,10 @@ use codex_config::types::WindowsSandboxModeToml;
use codex_features::Feature;
use codex_features::FeatureConfigSource;
use codex_features::FeatureOverrides;
use codex_features::FeatureToml;
use codex_features::Features;
use codex_features::FeaturesToml;
use codex_features::MultiAgentV2ConfigToml;
use codex_login::AuthManagerConfig;
use codex_mcp::McpConfig;
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
@@ -242,6 +245,9 @@ pub struct Config {
/// User-provided instructions from AGENTS.md.
pub user_instructions: Option<String>,
/// Path to the global AGENTS file loaded into `user_instructions`.
pub user_instructions_path: Option<PathBuf>,
/// Base instructions override.
pub base_instructions: Option<String>,
@@ -520,6 +526,9 @@ pub struct Config {
/// Settings for ghost snapshots (used for undo).
pub ghost_snapshot: GhostSnapshotConfig,
/// Settings specific to the task-path-based multi-agent tool surface.
pub multi_agent_v2: MultiAgentV2Config,
/// Centralized feature flags; source of truth for feature gating.
pub features: ManagedFeatures,
@@ -564,6 +573,23 @@ pub struct Config {
pub otel: codex_config::types::OtelConfig,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultiAgentV2Config {
pub usage_hint_enabled: bool,
pub usage_hint_text: Option<String>,
pub hide_spawn_agent_metadata: bool,
}
impl Default for MultiAgentV2Config {
fn default() -> Self {
Self {
usage_hint_enabled: true,
usage_hint_text: None,
hide_spawn_agent_metadata: false,
}
}
}
impl AuthManagerConfig for Config {
fn codex_home(&self) -> PathBuf {
self.codex_home.clone()
@@ -1279,6 +1305,42 @@ fn resolve_web_search_config(
}
}
fn resolve_multi_agent_v2_config(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
) -> MultiAgentV2Config {
let base = multi_agent_v2_toml_config(config_toml.features.as_ref());
let profile = multi_agent_v2_toml_config(config_profile.features.as_ref());
let default = MultiAgentV2Config::default();
let usage_hint_enabled = profile
.and_then(|config| config.usage_hint_enabled)
.or_else(|| base.and_then(|config| config.usage_hint_enabled))
.unwrap_or(default.usage_hint_enabled);
let usage_hint_text = profile
.and_then(|config| config.usage_hint_text.as_ref())
.or_else(|| base.and_then(|config| config.usage_hint_text.as_ref()))
.cloned()
.or(default.usage_hint_text);
let hide_spawn_agent_metadata = profile
.and_then(|config| config.hide_spawn_agent_metadata)
.or_else(|| base.and_then(|config| config.hide_spawn_agent_metadata))
.unwrap_or(default.hide_spawn_agent_metadata);
MultiAgentV2Config {
usage_hint_enabled,
usage_hint_text,
hide_spawn_agent_metadata,
}
}
fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiAgentV2ConfigToml> {
match features?.multi_agent_v2.as_ref()? {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
}
}
pub(crate) fn resolve_web_search_mode_for_turn(
web_search_mode: &Constrained<WebSearchMode>,
sandbox_policy: &SandboxPolicy,
@@ -1349,7 +1411,10 @@ impl Config {
network: network_requirements,
} = config_layer_stack.requirements().clone();
let user_instructions = Self::load_instructions(Some(&codex_home));
let (user_instructions, user_instructions_path) =
Self::load_instructions(Some(&codex_home))
.map(|loaded| (Some(loaded.contents), Some(loaded.path)))
.unwrap_or((None, None));
let mut startup_warnings = Vec::new();
// Destructure ConfigOverrides fully to ensure all overrides are applied.
@@ -1607,6 +1672,7 @@ impl Config {
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
.unwrap_or(WebSearchMode::Cached);
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg, &config_profile);
let agent_roles =
agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)?;
@@ -1941,6 +2007,7 @@ impl Config {
enforce_residency: enforce_residency.value,
notify: cfg.notify,
user_instructions,
user_instructions_path,
base_instructions,
personality,
developer_instructions,
@@ -2028,6 +2095,7 @@ impl Config {
.map_or_else(RealtimeConfig::default, |realtime| RealtimeConfig {
version: realtime.version.unwrap_or_default(),
session_type: realtime.session_type.unwrap_or_default(),
transport: realtime.transport.unwrap_or_default(),
}),
experimental_realtime_ws_backend_prompt: cfg.experimental_realtime_ws_backend_prompt,
experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context,
@@ -2040,6 +2108,7 @@ impl Config {
use_experimental_unified_exec_tool,
background_terminal_max_timeout,
ghost_snapshot,
multi_agent_v2,
features,
suppress_unstable_features_warning: cfg
.suppress_unstable_features_warning
@@ -2107,7 +2176,7 @@ impl Config {
Ok(config)
}
fn load_instructions(codex_dir: Option<&Path>) -> Option<String> {
fn load_instructions(codex_dir: Option<&Path>) -> Option<LoadedUserInstructions> {
let base = codex_dir?;
for candidate in [LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME] {
let mut path = base.to_path_buf();
@@ -2115,7 +2184,10 @@ impl Config {
if let Ok(contents) = std::fs::read_to_string(&path) {
let trimmed = contents.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
return Some(LoadedUserInstructions {
contents: trimmed.to_string(),
path,
});
}
}
}
@@ -2189,6 +2261,11 @@ impl Config {
}
}
struct LoadedUserInstructions {
contents: String,
path: PathBuf,
}
pub(crate) fn uses_deprecated_instructions_file(config_layer_stack: &ConfigLayerStack) -> bool {
config_layer_stack
.layers_high_to_low()

View File

@@ -5,7 +5,6 @@ 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;
use codex_features::Feature;
use codex_protocol::AgentPath;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::protocol::InterAgentCommunication;
@@ -207,10 +206,7 @@ impl ToolHandler for Handler {
)
})?;
let hide_agent_metadata = turn
.config
.features
.enabled(Feature::DebugHideSpawnAgentMetadata);
let hide_agent_metadata = turn.config.multi_agent_v2.hide_spawn_agent_metadata;
if hide_agent_metadata {
Ok(SpawnAgentResult::HiddenMetadata { task_name })
} else {

View File

@@ -32,6 +32,7 @@ use codex_tools::ZshForkConfig;
use codex_tools::mcp_call_tool_result_output_schema;
use codex_tools::mcp_tool_to_deferred_responses_api_tool;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::assert_regex_match;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::path::PathBuf;
@@ -158,6 +159,39 @@ fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a Co
.unwrap_or_else(|| panic!("expected tool {expected_name}"))
}
fn multi_agent_v2_tools_config() -> ToolsConfig {
let config = test_config();
let model_info = construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Collab);
features.enable(Feature::MultiAgentV2);
let available_models = Vec::new();
ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
})
}
fn multi_agent_v2_spawn_agent_description(tools_config: &ToolsConfig) -> String {
let (tools, _) = build_specs(
tools_config,
/*mcp_tools*/ None,
/*app_tools*/ None,
&[],
)
.build();
let spawn_agent = find_tool(&tools, "spawn_agent");
let ToolSpec::Function(ResponsesApiTool { description, .. }) = &spawn_agent.spec else {
panic!("spawn_agent should be a function tool");
};
description.clone()
}
fn model_info_from_models_json(slug: &str) -> ModelInfo {
let config = test_config();
let response = bundled_models_response()
@@ -599,6 +633,44 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
);
}
#[test]
fn spawn_agent_description_omits_usage_hint_when_disabled() {
let tools_config = multi_agent_v2_tools_config()
.with_spawn_agent_usage_hint(/*spawn_agent_usage_hint*/ false);
let description = multi_agent_v2_spawn_agent_description(&tools_config);
assert_regex_match(
r#"(?sx)
^\s*
No\ picker-visible\ models\ are\ currently\ loaded\.
\s+Spawn\ a\ sub-agent\ for\ a\ well-scoped\ task\.
\s+Returns\ the\ canonical\ task\ name\ for\ the\ spawned\ agent,\ plus\ the\ user-facing\ nickname\ when\ available\.
\s*$
"#,
&description,
);
}
#[test]
fn spawn_agent_description_uses_configured_usage_hint_text() {
let tools_config = multi_agent_v2_tools_config().with_spawn_agent_usage_hint_text(Some(
/*spawn_agent_usage_hint_text*/ "Custom delegation guidance only.".to_string(),
));
let description = multi_agent_v2_spawn_agent_description(&tools_config);
assert_regex_match(
r#"(?sx)
^\s*
No\ picker-visible\ models\ are\ currently\ loaded\.
\s+Spawn\ a\ sub-agent\ for\ a\ well-scoped\ task\.
\s+Returns\ the\ canonical\ task\ name\ for\ the\ spawned\ agent,\ plus\ the\ user-facing\ nickname\ when\ available\.
\s+Custom\ delegation\ guidance\ only\.
\s*$
"#,
&description,
);
}
#[test]
fn tool_suggest_requires_apps_and_plugins_features() {
let model_info = search_capable_model_info();

View File

@@ -89,7 +89,7 @@ pub fn resolve_windows_sandbox_private_desktop(cfg: &ConfigToml, profile: &Confi
}
fn legacy_windows_sandbox_keys_present(features: Option<&FeaturesToml>) -> bool {
let Some(entries) = features.map(|features| &features.entries) else {
let Some(entries) = features.map(FeaturesToml::entries) else {
return false;
};
entries.contains_key(Feature::WindowsSandboxElevated.key())
@@ -100,8 +100,8 @@ fn legacy_windows_sandbox_keys_present(features: Option<&FeaturesToml>) -> bool
pub fn legacy_windows_sandbox_mode(
features: Option<&FeaturesToml>,
) -> Option<WindowsSandboxModeToml> {
let entries = features.map(|features| &features.entries)?;
legacy_windows_sandbox_mode_from_entries(entries)
let entries = features.map(FeaturesToml::entries)?;
legacy_windows_sandbox_mode_from_entries(&entries)
}
pub fn legacy_windows_sandbox_mode_from_entries(

View File

@@ -109,7 +109,7 @@ fn resolve_windows_sandbox_mode_falls_back_to_legacy_keys() {
/*value*/ true,
);
let cfg = ConfigToml {
features: Some(FeaturesToml { entries }),
features: Some(FeaturesToml::from(entries)),
..Default::default()
};
@@ -127,9 +127,7 @@ fn resolve_windows_sandbox_mode_profile_legacy_false_blocks_top_level_legacy_tru
/*value*/ false,
);
let profile = ConfigProfile {
features: Some(FeaturesToml {
entries: profile_entries,
}),
features: Some(FeaturesToml::from(profile_entries)),
..Default::default()
};
@@ -139,9 +137,7 @@ fn resolve_windows_sandbox_mode_profile_legacy_false_blocks_top_level_legacy_tru
/*value*/ true,
);
let cfg = ConfigToml {
features: Some(FeaturesToml {
entries: cfg_entries,
}),
features: Some(FeaturesToml::from(cfg_entries)),
..Default::default()
};

View File

@@ -1,10 +1,3 @@
- Output will be rendered in a command line interface or minimal UI so keep responses tight, scannable, and low-noise. Generally avoid the use of emojis. You may format with GitHub-flavored Markdown.
- Never use nested bullets. Keep lists flat (single level). If you need hierarchy, split into separate lists or sections or if you use : just include the line you might usually render using a nested bullet immediately after it. For numbered lists, only use the `1. 2. 3.` style markers (with a period), never `1)`.
- When writing a final assistant response, state the solution first before explaining your answer. The complexity of the answer should match the task. If the task is simple, your answer should be short. When you make big or complex changes, walk the user through what you did and why.
- Headers are optional, only use them when you think they are necessary. If you do use them, use short Title Case (1-3 words) wrapped in **…**. Don't add a blank line.
- Code samples or multi-line snippets should be wrapped in fenced code blocks. Include an info string as often as possible.
- Never output the content of large files, just provide references. Use inline code to make file paths clickable; each reference should have a stand alone path, even if it's the same file. Paths may be absolute, workspace-relative, a//b/ diff-prefixed, or bare filename/suffix; locations may be :line[:column] or #Lline[Ccolumn] (1-based; column defaults to 1). Do not use file://, vscode://, or https://, and do not provide line ranges. Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5
- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as `date`), you should do so.
- Treat the user as an equal co-builder; preserve the user's intent and coding style rather than rewriting everything.
- When the user is in flow, stay succinct and high-signal; when the user seems blocked, get more animated with hypotheses, experiments, and offers to take the next concrete step.
@@ -40,13 +33,6 @@ When the user asks for a review, you default to a code-review mindset. Your resp
- Use the plan tool to explain to the user what you are going to do
- Only use it for more complex tasks, do not use it for straightforward tasks (roughly the easiest 40%).
- Do not make single-step plans. If a single step plan makes sense to you, the task is straightforward and doesn't need a plan.
- When you made a plan, update it after having performed one of the sub-tasks that you shared on the plan.
# Sub-agents
If `spawn_agent` is unavailable or fails, ignore this section and proceed solo.
## Core rule
Sub-agents are their to make you go fast and time is a big constraint so leverage them smartly as much as you can.
## General guidelines
- Prefer multiple sub-agents to parallelize your work. Time is a constraint so parallelism resolve the task faster.
@@ -54,11 +40,3 @@ Sub-agents are their to make you go fast and time is a big constraint so leverag
- If the user asks a question, answer it first, then continue coordinating sub-agents.
- When you ask sub-agent to do the work for you, your only role becomes to coordinate them. Do not perform the actual work while they are working.
- When you have plan with multiple step, process them in parallel by spawning one agent per step when this is possible.
- Choose the correct agent type.
## Flow
1. Understand the task.
2. Spawn the optimal necessary sub-agents.
3. Coordinate them via wait_agent / send_input.
4. Iterate on this. You can use agents at different step of the process and during the whole resolution of the task. Never forget to use them.
5. Ask the user before shutting sub-agents down unless you need to because you reached the agent limit.

View File

@@ -0,0 +1,23 @@
use crate::FeatureConfig;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MultiAgentV2ConfigToml {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_hint_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_hint_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hide_spawn_agent_metadata: Option<bool>,
}
impl FeatureConfig for MultiAgentV2ConfigToml {
fn enabled(&self) -> Option<bool> {
self.enabled
}
}

View File

@@ -14,7 +14,9 @@ use std::collections::BTreeMap;
use std::collections::BTreeSet;
use toml::Table;
mod feature_configs;
mod legacy;
pub use feature_configs::MultiAgentV2ConfigToml;
use legacy::LegacyFeatureToggles;
pub use legacy::legacy_feature_keys;
@@ -138,8 +140,6 @@ pub enum Feature {
Collab,
/// Enable task-path-based multi-agent routing.
MultiAgentV2,
/// Hide spawn_agent agent/model override fields from the model-visible tool schema.
DebugHideSpawnAgentMetadata,
/// Enable CSV-backed agent job tools.
SpawnCsv,
/// Enable apps.
@@ -398,7 +398,7 @@ impl Features {
.apply(&mut features);
if let Some(feature_entries) = source.features {
features.apply_map(&feature_entries.entries);
features.apply_toml(feature_entries);
}
}
@@ -492,8 +492,61 @@ pub fn is_known_feature_key(key: &str) -> bool {
/// Deserializable features table for TOML.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
pub struct FeaturesToml {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multi_agent_v2: Option<FeatureToml<MultiAgentV2ConfigToml>>,
/// Boolean feature toggles keyed by canonical or legacy feature name.
#[serde(flatten)]
pub entries: BTreeMap<String, bool>,
entries: BTreeMap<String, bool>,
}
impl Features {
fn apply_toml(&mut self, features: &FeaturesToml) {
let entries = features.entries();
self.apply_map(&entries);
}
}
impl FeaturesToml {
pub fn entries(&self) -> BTreeMap<String, bool> {
let mut entries = self.entries.clone();
if let Some(enabled) = self.multi_agent_v2.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::MultiAgentV2.key().to_string(), enabled);
}
entries
}
}
impl From<BTreeMap<String, bool>> for FeaturesToml {
fn from(entries: BTreeMap<String, bool>) -> Self {
Self {
entries,
..Default::default()
}
}
}
// To be used for features that need more configuration than just enabled/disabled and
// require a custom config struct under `[features]`.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[serde(untagged)]
pub enum FeatureToml<T> {
Enabled(bool),
Config(T),
}
impl<T: FeatureConfig> FeatureToml<T> {
pub fn enabled(&self) -> Option<bool> {
match self {
Self::Enabled(enabled) => Some(*enabled),
Self::Config(config) => config.enabled(),
}
}
}
// A trait to be implemented by custom feature config structs when defining a feature that needs more configuration than
// just enabled/disabled.
pub trait FeatureConfig {
fn enabled(&self) -> Option<bool>;
}
/// Single, easy-to-read registry of all feature definitions.
@@ -708,12 +761,6 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::DebugHideSpawnAgentMetadata,
key: "debug_hide_spawn_agent_metadata",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::SpawnCsv,
key: "enable_fanout",

View File

@@ -1,6 +1,7 @@
use crate::Feature;
use crate::FeatureConfigSource;
use crate::FeatureOverrides;
use crate::FeatureToml;
use crate::Features;
use crate::FeaturesToml;
use crate::Stage;
@@ -211,12 +212,14 @@ fn from_sources_applies_base_profile_and_overrides() {
base_entries.insert("plugins".to_string(), true);
let base_features = FeaturesToml {
entries: base_entries,
..Default::default()
};
let mut profile_entries = BTreeMap::new();
profile_entries.insert("code_mode_only".to_string(), true);
let profile_features = FeaturesToml {
entries: profile_entries,
..Default::default()
};
let features = Features::from_sources(
@@ -242,6 +245,81 @@ fn from_sources_applies_base_profile_and_overrides() {
assert_eq!(features.enabled(Feature::WebSearchRequest), false);
}
#[test]
fn multi_agent_v2_feature_config_deserializes_boolean_toggle() {
let features: FeaturesToml = toml::from_str(
r#"
multi_agent_v2 = true
"#,
)
.expect("features table should deserialize");
assert_eq!(
features.entries(),
BTreeMap::from([("multi_agent_v2".to_string(), true)])
);
assert_eq!(features.multi_agent_v2, Some(FeatureToml::Enabled(true)));
}
#[test]
fn multi_agent_v2_feature_config_deserializes_table() {
let features: FeaturesToml = toml::from_str(
r#"
[multi_agent_v2]
enabled = true
usage_hint_enabled = false
usage_hint_text = "Custom delegation guidance."
hide_spawn_agent_metadata = true
"#,
)
.expect("features table should deserialize");
assert_eq!(
features.entries(),
BTreeMap::from([("multi_agent_v2".to_string(), true)])
);
assert_eq!(
features.multi_agent_v2,
Some(crate::FeatureToml::Config(crate::MultiAgentV2ConfigToml {
enabled: Some(true),
usage_hint_enabled: Some(false),
usage_hint_text: Some("Custom delegation guidance.".to_string()),
hide_spawn_agent_metadata: Some(true),
}))
);
}
#[test]
fn multi_agent_v2_feature_config_usage_hint_enabled_does_not_enable_feature() {
let features_toml: FeaturesToml = toml::from_str(
r#"
[multi_agent_v2]
usage_hint_enabled = false
"#,
)
.expect("features table should deserialize");
let features = Features::from_sources(
FeatureConfigSource {
features: Some(&features_toml),
..Default::default()
},
FeatureConfigSource::default(),
FeatureOverrides::default(),
);
assert_eq!(features.enabled(Feature::MultiAgentV2), false);
assert_eq!(features_toml.entries(), BTreeMap::new());
assert_eq!(
features_toml.multi_agent_v2,
Some(crate::FeatureToml::Config(crate::MultiAgentV2ConfigToml {
enabled: None,
usage_hint_enabled: Some(false),
usage_hint_text: None,
hide_spawn_agent_metadata: None,
}))
);
}
#[test]
fn unstable_warning_event_only_mentions_enabled_under_development_features() {
let mut configured_features = Table::new();

View File

@@ -11,6 +11,8 @@ pub struct SpawnAgentToolOptions<'a> {
pub available_models: &'a [ModelPreset],
pub agent_type_description: String,
pub hide_agent_type_model_reasoning: bool,
pub include_usage_hint: bool,
pub usage_hint_text: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -35,6 +37,8 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions<'_>) -> ToolSpe
description: spawn_agent_tool_description(
available_models_description.as_deref(),
return_value_description,
options.include_usage_hint,
options.usage_hint_text,
),
strict: false,
defer_loading: None,
@@ -68,6 +72,8 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpe
description: spawn_agent_tool_description(
available_models_description.as_deref(),
return_value_description,
options.include_usage_hint,
options.usage_hint_text,
),
strict: false,
defer_loading: None,
@@ -580,20 +586,40 @@ fn hide_spawn_agent_metadata_options(properties: &mut BTreeMap<String, JsonSchem
fn spawn_agent_tool_description(
available_models_description: Option<&str>,
return_value_description: &str,
include_usage_hint: bool,
usage_hint_text: Option<String>,
) -> String {
let agent_role_guidance = available_models_description
.map(|description| {
format!(
"Agent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself.\n{description}"
)
let agent_role_guidance = available_models_description.unwrap_or_default();
let tool_description = format!(
r#"
{agent_role_guidance}
Spawn a sub-agent for a well-scoped task. {return_value_description}"#
);
if !include_usage_hint {
return tool_description;
}
if let Some(usage_hint_text) = usage_hint_text {
return format!(
r#"
{tool_description}
{usage_hint_text}"#
);
}
let agent_role_usage_hint = available_models_description
.map(|_| {
"Agent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself."
})
.unwrap_or_default();
format!(
r#"
Only use `spawn_agent` if and only if the user explicitly asks for sub-agents, delegation, or parallel agent work.
Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn.
{agent_role_guidance}
Spawn a sub-agent for a well-scoped task. {return_value_description} This spawn_agent tool provides you access to smaller but more efficient sub-agents. A mini model can solve many tasks faster than the main model. You should follow the rules and guidelines below to use this tool.
{tool_description}
This spawn_agent tool provides you access to smaller but more efficient sub-agents. A mini model can solve many tasks faster than the main model. You should follow the rules and guidelines below to use this tool.
Only use `spawn_agent` if and only if the user explicitly asks for sub-agents, delegation, or parallel agent work.
Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn.
{agent_role_usage_hint}
### When to delegate vs. do the subtask yourself
- First, quickly analyze the overall user task and form a succinct high-level plan. Identify which tasks are immediate blockers on the critical path, and which tasks are sidecar tasks that are needed but can run in parallel without blocking the next local step. As part of that plan, explicitly decide what immediate task you should do locally right now. Do this planning step before delegating to agents so you do not hand off the immediate blocking task to a submodel and then waste time waiting on it.

View File

@@ -38,6 +38,8 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
include_usage_hint: true,
usage_hint_text: None,
});
let ToolSpec::Function(ResponsesApiTool {
@@ -84,6 +86,8 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
available_models: &[],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: false,
include_usage_hint: true,
usage_hint_text: None,
});
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else {

View File

@@ -105,6 +105,8 @@ pub struct ToolsConfig {
pub collab_tools: bool,
pub multi_agent_v2: bool,
pub hide_spawn_agent_metadata: bool,
pub spawn_agent_usage_hint: bool,
pub spawn_agent_usage_hint_text: Option<String>,
pub default_mode_request_user_input: bool,
pub experimental_supported_tools: Vec<String>,
pub agent_jobs_tools: bool,
@@ -141,7 +143,6 @@ impl ToolsConfig {
include_js_repl && features.enabled(Feature::JsReplToolsOnly);
let include_collab_tools = features.enabled(Feature::Collab);
let include_multi_agent_v2 = features.enabled(Feature::MultiAgentV2);
let hide_spawn_agent_metadata = features.enabled(Feature::DebugHideSpawnAgentMetadata);
let include_agent_jobs = features.enabled(Feature::SpawnCsv);
let include_default_mode_request_user_input =
features.enabled(Feature::DefaultModeRequestUserInput);
@@ -219,7 +220,9 @@ impl ToolsConfig {
can_request_original_image_detail: include_original_image_detail,
collab_tools: include_collab_tools,
multi_agent_v2: include_multi_agent_v2,
hide_spawn_agent_metadata,
hide_spawn_agent_metadata: false,
spawn_agent_usage_hint: true,
spawn_agent_usage_hint_text: None,
default_mode_request_user_input: include_default_mode_request_user_input,
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
agent_jobs_tools: include_agent_jobs,
@@ -233,6 +236,24 @@ impl ToolsConfig {
self
}
pub fn with_spawn_agent_usage_hint(mut self, spawn_agent_usage_hint: bool) -> Self {
self.spawn_agent_usage_hint = spawn_agent_usage_hint;
self
}
pub fn with_spawn_agent_usage_hint_text(
mut self,
spawn_agent_usage_hint_text: Option<String>,
) -> Self {
self.spawn_agent_usage_hint_text = spawn_agent_usage_hint_text;
self
}
pub fn with_hide_spawn_agent_metadata(mut self, hide_spawn_agent_metadata: bool) -> Self {
self.hide_spawn_agent_metadata = hide_spawn_agent_metadata;
self
}
pub fn with_allow_login_shell(mut self, allow_login_shell: bool) -> Self {
self.allow_login_shell = allow_login_shell;
self

View File

@@ -376,6 +376,8 @@ pub fn build_tool_registry_plan(
available_models: &config.available_models,
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
@@ -419,6 +421,8 @@ pub fn build_tool_registry_plan(
available_models: &config.available_models,
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,

View File

@@ -1940,6 +1940,8 @@ fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions<'_> {
available_models: &config.available_models,
agent_type_description: agent_type_description(config, DEFAULT_AGENT_TYPE_DESCRIPTION),
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
}
}

View File

@@ -1693,6 +1693,21 @@ impl App {
self.active_thread_id.or(self.chat_widget.thread_id())
}
fn ignore_same_thread_resume(
&mut self,
target_session: &crate::resume_picker::SessionTarget,
) -> bool {
if self.active_thread_id != Some(target_session.thread_id) {
return false;
};
self.chat_widget.add_info_message(
format!("Already viewing {}.", target_session.display_label()),
/*hint*/ None,
);
true
}
/// Mirrors the visible thread into the contextual footer row.
///
/// The footer sometimes shows ambient context instead of an instructional hint. In multi-agent
@@ -4074,6 +4089,10 @@ impl App {
.await?
{
SessionSelection::Resume(target_session) => {
if self.ignore_same_thread_resume(&target_session) {
tui.frame_requester().schedule_frame();
return Ok(AppRunControl::Continue);
}
let current_cwd = self.config.cwd.to_path_buf();
let resume_cwd = if self.remote_app_server_url.is_some() {
current_cwd.clone()
@@ -6520,6 +6539,53 @@ mod tests {
);
}
#[tokio::test]
async fn ignore_same_thread_resume_reports_noop_for_current_thread() {
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
let thread_id = ThreadId::new();
let session = test_thread_session(thread_id, PathBuf::from("/tmp/project"));
app.chat_widget.handle_thread_session(session.clone());
app.thread_event_channels.insert(
thread_id,
ThreadEventChannel::new_with_session(
THREAD_EVENT_CHANNEL_CAPACITY,
session,
Vec::new(),
),
);
app.activate_thread_channel(thread_id).await;
while app_event_rx.try_recv().is_ok() {}
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
path: Some(PathBuf::from("/tmp/project")),
thread_id,
});
assert!(ignored);
let cell = match app_event_rx.try_recv() {
Ok(AppEvent::InsertHistoryCell(cell)) => cell,
other => panic!("expected info message after same-thread resume, saw {other:?}"),
};
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
assert!(rendered.contains("Already viewing /tmp/project."));
}
#[tokio::test]
async fn ignore_same_thread_resume_allows_reattaching_displayed_inactive_thread() {
let mut app = make_test_app().await;
let thread_id = ThreadId::new();
let session = test_thread_session(thread_id, PathBuf::from("/tmp/project"));
app.chat_widget.handle_thread_session(session);
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
path: Some(PathBuf::from("/tmp/project")),
thread_id,
});
assert!(!ignored);
assert!(app.transcript_cells.is_empty());
}
#[tokio::test]
async fn enqueue_primary_thread_session_replays_buffered_approval_after_attach() -> Result<()> {
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;

View File

@@ -43,6 +43,10 @@ pub(crate) async fn discover_agents_summary(config: &Config) -> io::Result<Strin
pub(crate) fn compose_agents_summary(config: &Config, paths: &[AbsolutePathBuf]) -> String {
let mut rels: Vec<String> = Vec::new();
if let Some(path) = config.user_instructions_path.as_deref() {
rels.push(format_directory_display(path, /*max_width*/ None));
}
for p in paths {
let file_name = p
.file_name()
@@ -189,7 +193,21 @@ fn title_case(s: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use codex_core::DEFAULT_PROJECT_DOC_FILENAME;
use codex_core::LOCAL_PROJECT_DOC_FILENAME;
use codex_core::config::ConfigBuilder;
use pretty_assertions::assert_eq;
use std::fs;
use tempfile::TempDir;
async fn test_config(codex_home: &TempDir, cwd: &TempDir) -> Config {
ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(cwd.path().to_path_buf()))
.build()
.await
.expect("load config")
}
#[test]
fn plan_type_display_name_remaps_display_labels() {
@@ -211,4 +229,61 @@ mod tests {
assert_eq!(plan_type_display_name(plan_type), expected);
}
}
#[tokio::test]
async fn discover_agents_summary_includes_global_agents_path() {
let codex_home = TempDir::new().expect("temp codex home");
let cwd = TempDir::new().expect("temp cwd");
let global_agents_path = codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME);
fs::write(&global_agents_path, "global instructions").expect("write global agents");
let config = test_config(&codex_home, &cwd).await;
assert_eq!(
discover_agents_summary(&config).await.expect("summary"),
format_directory_display(&global_agents_path, /*max_width*/ None)
);
}
#[tokio::test]
async fn discover_agents_summary_names_global_agents_override() {
let codex_home = TempDir::new().expect("temp codex home");
let cwd = TempDir::new().expect("temp cwd");
fs::write(
codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME),
"global instructions",
)
.expect("write global agents");
let override_path = codex_home.path().join(LOCAL_PROJECT_DOC_FILENAME);
fs::write(&override_path, "override instructions").expect("write global override");
let config = test_config(&codex_home, &cwd).await;
assert_eq!(
discover_agents_summary(&config).await.expect("summary"),
format_directory_display(&override_path, /*max_width*/ None)
);
}
#[tokio::test]
async fn discover_agents_summary_orders_global_before_project_agents() {
let codex_home = TempDir::new().expect("temp codex home");
let cwd = TempDir::new().expect("temp cwd");
let global_agents_path = codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME);
fs::write(&global_agents_path, "global instructions").expect("write global agents");
fs::write(
cwd.path().join(DEFAULT_PROJECT_DOC_FILENAME),
"project instructions",
)
.expect("write project agents");
let config = test_config(&codex_home, &cwd).await;
let summary = discover_agents_summary(&config).await.expect("summary");
let mut paths = summary.split(", ");
assert_eq!(
paths.next(),
Some(format_directory_display(&global_agents_path, /*max_width*/ None).as_str())
);
let project_path = paths.next().expect("project agents path");
assert!(project_path.ends_with(DEFAULT_PROJECT_DOC_FILENAME));
assert_eq!(paths.next(), None);
}
}

View File

@@ -160,7 +160,6 @@ fn apply_read_acls(
/*label*/ None,
access_mask,
access_label,
refresh_errors,
log,
)?;
if builtin_has {
@@ -172,7 +171,6 @@ fn apply_read_acls(
Some("sandbox_group"),
access_mask,
access_label,
refresh_errors,
log,
)?;
if sandbox_has {
@@ -216,7 +214,6 @@ fn read_mask_allows_or_log(
label: Option<&str>,
read_mask: u32,
access_label: &str,
refresh_errors: &mut Vec<String>,
log: &mut File,
) -> Result<bool> {
match path_mask_allows(root, psids, read_mask, /*require_all_bits*/ true) {
@@ -225,16 +222,10 @@ fn read_mask_allows_or_log(
let label_suffix = label
.map(|value| format!(" for {value}"))
.unwrap_or_default();
refresh_errors.push(format!(
"{access_label} mask check failed on {}{}: {}",
root.display(),
label_suffix,
e
));
log_line(
log,
&format!(
"{access_label} mask check failed on {}{}: {}; continuing",
"{access_label} mask check failed on {}{}: {}; will attempt grant",
root.display(),
label_suffix,
e
@@ -676,15 +667,10 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
match path_mask_allows(root, &[psid], write_mask, /*require_all_bits*/ true) {
Ok(h) => h,
Err(e) => {
refresh_errors.push(format!(
"write mask check failed on {} for {label}: {}",
root.display(),
e
));
log_line(
log,
&format!(
"write mask check failed on {} for {label}: {}; continuing",
"write mask check failed on {} for {label}: {}; will attempt grant",
root.display(),
e
),