mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
[codex] Add configurable multi-agent mode hint text (#30493)
## Why
Multi-agent V2 normally derives its mode instructions from reasoning
effort: Ultra enables proactive delegation, while other efforts require
an explicit request. Some deployments need to provide one configured
delegation policy that replaces those built-ins and remains stable when
reasoning effort changes.
## What changed
- Add `features.multi_agent_v2.multi_agent_mode_hint_text` alongside the
existing root and subagent hint settings.
- Treat any configured value, including an empty string, as
`MultiAgentMode::Custom(hint_text)`, so the configured text replaces the
built-in explicit-only and proactive policies.
- Persist the full custom variant and hint text in the turn-context
snapshot, so the durable comparison baseline detects both
reasoning-effort changes and configured policy-text changes.
- Preserve the existing explicit-only/proactive behavior when the
setting is absent.
- Replace the ambiguous `MultiAgentMode::None` variant with
`MultiAgentMode::Custom(String)` in new rollouts and API schemas. A
compatibility wire type maps legacy serialized `none` values to
`Custom("")` when resuming existing rollouts.
- Regenerate the config and app-server schemas.
## Configuration examples
The distinction is whether `multi_agent_mode_hint_text` is present. An
empty string is still a configured value and intentionally suppresses
the built-in mode instructions.
### Unset: preserve existing effort-derived behavior
```toml
[features.multi_agent_v2]
enabled = true
# multi_agent_mode_hint_text is omitted
```
- Ultra reasoning uses the built-in proactive delegation instructions.
- Other reasoning efforts use the built-in explicit-request-only
instructions.
### Empty: suppress all mode hint text
```toml
[features.multi_agent_v2]
enabled = true
multi_agent_mode_hint_text = ""
```
This selects effective mode `custom` at every reasoning effort and
injects an empty mode body, suppressing both built-in policies.
### Set: always use the configured text
```toml
[features.multi_agent_v2]
enabled = true
multi_agent_mode_hint_text = "Delegate to subagents when it will materially improve the result."
```
This selects effective mode `custom` at every reasoning effort and
injects the configured text verbatim instead of either built-in policy.
## Verification
- `just test -p codex-core multi_agent_mode`
- Covers a configured hint across High and Ultra reasoning efforts and
verifies the full custom hint is recorded for both turns.
- Covers an empty-string override suppressing both built-in instruction
bodies.
- `just test -p codex-protocol -p codex-app-server-protocol`
- Covers legacy `none` turn-context deserialization as `Custom("")` and
verifies the regenerated schemas.
This commit is contained in:
+23
-7
@@ -1786,13 +1786,29 @@
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
|
||||
@@ -2713,13 +2713,29 @@
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
|
||||
+23
-7
@@ -12860,13 +12860,29 @@
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
|
||||
+23
-7
@@ -9264,13 +9264,29 @@
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
|
||||
@@ -681,13 +681,29 @@
|
||||
]
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
|
||||
@@ -681,13 +681,29 @@
|
||||
]
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
|
||||
+23
-7
@@ -108,13 +108,29 @@
|
||||
"type": "string"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
|
||||
@@ -195,13 +195,29 @@
|
||||
"type": "string"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
|
||||
@@ -681,13 +681,29 @@
|
||||
]
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
|
||||
@@ -141,13 +141,29 @@
|
||||
"type": "string"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.",
|
||||
"enum": [
|
||||
"none",
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
"description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"custom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"custom"
|
||||
],
|
||||
"title": "CustomMultiAgentMode",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Controls whether the model receives multi-agent delegation instructions and,
|
||||
* when it does, whether it should only spawn sub-agents after an explicit user
|
||||
* request or may delegate proactively when doing so would help. `none` leaves
|
||||
* the multi-agent tools available without injecting delegation instructions.
|
||||
* Controls the effective multi-agent delegation instructions for a turn. `custom` means the
|
||||
* configured mode hint defines the policy instead of a built-in policy.
|
||||
*/
|
||||
export type MultiAgentMode = "none" | "explicitRequestOnly" | "proactive";
|
||||
export type MultiAgentMode = { "custom": string } | "explicitRequestOnly" | "proactive";
|
||||
|
||||
@@ -1750,6 +1750,9 @@
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"multi_agent_mode_hint_text": {
|
||||
"type": "string"
|
||||
},
|
||||
"non_code_mode_only": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -10307,6 +10307,7 @@ default_wait_timeout_ms = 30000
|
||||
usage_hint_text = "Custom delegation guidance."
|
||||
root_agent_usage_hint_text = "Root guidance."
|
||||
subagent_usage_hint_text = "Subagent guidance."
|
||||
multi_agent_mode_hint_text = "Custom mode guidance."
|
||||
tool_namespace = "agents"
|
||||
hide_spawn_agent_metadata = true
|
||||
non_code_mode_only = true
|
||||
@@ -10343,6 +10344,10 @@ non_code_mode_only = true
|
||||
config.multi_agent_v2.subagent_usage_hint_text.as_deref(),
|
||||
Some("Subagent guidance.")
|
||||
);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.multi_agent_mode_hint_text.as_deref(),
|
||||
Some("Custom mode guidance.")
|
||||
);
|
||||
assert_eq!(
|
||||
config.multi_agent_v2.tool_namespace.as_deref(),
|
||||
Some("agents")
|
||||
@@ -10405,6 +10410,22 @@ max_concurrent_threads_per_session = 17
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_agent_v2_preserves_empty_mode_hint_override() {
|
||||
let config_toml = toml::from_str(
|
||||
r#"[features.multi_agent_v2]
|
||||
multi_agent_mode_hint_text = ""
|
||||
"#,
|
||||
)
|
||||
.expect("multi-agent v2 config should parse");
|
||||
|
||||
let expected = MultiAgentV2Config {
|
||||
multi_agent_mode_hint_text: Some(String::new()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(resolve_multi_agent_v2_config(&config_toml), expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_empty_usage_hint_overrides_clear_default_hints() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -1144,6 +1144,7 @@ pub struct MultiAgentV2Config {
|
||||
pub usage_hint_text: Option<String>,
|
||||
pub root_agent_usage_hint_text: Option<String>,
|
||||
pub subagent_usage_hint_text: Option<String>,
|
||||
pub multi_agent_mode_hint_text: Option<String>,
|
||||
pub tool_namespace: Option<String>,
|
||||
pub hide_spawn_agent_metadata: bool,
|
||||
pub non_code_mode_only: bool,
|
||||
@@ -1165,6 +1166,7 @@ impl MultiAgentV2Config {
|
||||
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT,
|
||||
max_concurrent_threads_per_session,
|
||||
)),
|
||||
multi_agent_mode_hint_text: None,
|
||||
tool_namespace: Some(DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE.to_string()),
|
||||
hide_spawn_agent_metadata: true,
|
||||
non_code_mode_only: true,
|
||||
@@ -2501,6 +2503,10 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
|
||||
base.map(|config| &config.subagent_usage_hint_text),
|
||||
default.subagent_usage_hint_text,
|
||||
);
|
||||
let multi_agent_mode_hint_text = base
|
||||
.and_then(|config| config.multi_agent_mode_hint_text.as_ref())
|
||||
.cloned()
|
||||
.or(default.multi_agent_mode_hint_text);
|
||||
let tool_namespace = base
|
||||
.and_then(|config| config.tool_namespace.as_ref())
|
||||
.cloned()
|
||||
@@ -2520,6 +2526,7 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
|
||||
usage_hint_text,
|
||||
root_agent_usage_hint_text,
|
||||
subagent_usage_hint_text,
|
||||
multi_agent_mode_hint_text,
|
||||
tool_namespace,
|
||||
hide_spawn_agent_metadata,
|
||||
non_code_mode_only,
|
||||
|
||||
@@ -4,7 +4,6 @@ use codex_protocol::protocol::MULTI_AGENT_MODE_CLOSE_TAG;
|
||||
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
||||
|
||||
const EXPLICIT_REQUEST_ONLY_MULTI_AGENT_MODE_TEXT: &str = "Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.";
|
||||
const NO_MULTI_AGENT_MODE_TEXT: &str = "Multi-agent delegation mode instructions are inactive. Any earlier multi-agent mode developer message no longer applies.";
|
||||
const PROACTIVE_MULTI_AGENT_MODE_TEXT: &str = "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -32,8 +31,8 @@ impl ContextualUserFragment for MultiAgentModeInstructions {
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
match self.multi_agent_mode {
|
||||
MultiAgentMode::None => NO_MULTI_AGENT_MODE_TEXT.to_string(),
|
||||
match &self.multi_agent_mode {
|
||||
MultiAgentMode::Custom(hint_text) => hint_text.clone(),
|
||||
MultiAgentMode::ExplicitRequestOnly => {
|
||||
EXPLICIT_REQUEST_ONLY_MULTI_AGENT_MODE_TEXT.to_string()
|
||||
}
|
||||
|
||||
@@ -85,9 +85,6 @@ fn build_multi_agent_mode_update_item(
|
||||
}
|
||||
|
||||
match effective_multi_agent_mode {
|
||||
Some(MultiAgentMode::None) => {
|
||||
Some(MultiAgentModeInstructions::new(MultiAgentMode::None).render())
|
||||
}
|
||||
Some(multi_agent_mode) => Some(MultiAgentModeInstructions::new(multi_agent_mode).render()),
|
||||
None if previous.multi_agent_mode == Some(MultiAgentMode::Proactive) => {
|
||||
Some(MultiAgentModeInstructions::new(MultiAgentMode::ExplicitRequestOnly).render())
|
||||
|
||||
@@ -94,7 +94,6 @@ use codex_protocol::approvals::NetworkPolicyRuleAction;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
@@ -3430,16 +3429,10 @@ impl Session {
|
||||
{
|
||||
items.push(usage_hint_message);
|
||||
}
|
||||
match multi_agents::effective_multi_agent_mode(turn_context) {
|
||||
Some(
|
||||
multi_agent_mode
|
||||
@ (MultiAgentMode::ExplicitRequestOnly | MultiAgentMode::Proactive),
|
||||
) => {
|
||||
items.push(ContextualUserFragment::into(
|
||||
MultiAgentModeInstructions::new(multi_agent_mode),
|
||||
));
|
||||
}
|
||||
Some(MultiAgentMode::None) | None => {}
|
||||
if let Some(multi_agent_mode) = multi_agents::effective_multi_agent_mode(turn_context) {
|
||||
items.push(ContextualUserFragment::into(
|
||||
MultiAgentModeInstructions::new(multi_agent_mode),
|
||||
));
|
||||
}
|
||||
if let Some(contextual_user_message) =
|
||||
crate::context_manager::updates::build_contextual_user_message(contextual_user_sections)
|
||||
|
||||
@@ -41,9 +41,18 @@ pub(crate) fn effective_multi_agent_mode(turn_context: &TurnContext) -> Option<M
|
||||
return None;
|
||||
}
|
||||
|
||||
let multi_agent_mode = match turn_context.effective_reasoning_effort() {
|
||||
Some(ReasoningEffort::Ultra) => MultiAgentMode::Proactive,
|
||||
_ => MultiAgentMode::ExplicitRequestOnly,
|
||||
// A configured hint, including an empty string, defines a custom policy instead of an
|
||||
// effort-derived built-in policy.
|
||||
let multi_agent_mode = match &turn_context
|
||||
.config
|
||||
.multi_agent_v2
|
||||
.multi_agent_mode_hint_text
|
||||
{
|
||||
Some(hint_text) => MultiAgentMode::Custom(hint_text.clone()),
|
||||
None => match turn_context.effective_reasoning_effort() {
|
||||
Some(ReasoningEffort::Ultra) => MultiAgentMode::Proactive,
|
||||
_ => MultiAgentMode::ExplicitRequestOnly,
|
||||
},
|
||||
};
|
||||
|
||||
match &turn_context.session_source {
|
||||
|
||||
@@ -20,9 +20,11 @@ use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
|
||||
const NO_SPAWN_TEXT: &str = "Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work.";
|
||||
const PROACTIVE_TEXT: &str = "Proactive multi-agent delegation is active.";
|
||||
const CUSTOM_MODE_HINT_TEXT: &str = "Use the configured delegation policy.";
|
||||
|
||||
fn add_ultra_reasoning(model_info: &mut ModelInfo) {
|
||||
model_info.supports_reasoning_summaries = true;
|
||||
@@ -41,6 +43,12 @@ fn configure_multi_agent_v2(config: &mut Config) {
|
||||
.expect("test config should allow feature update");
|
||||
}
|
||||
|
||||
// Configuring a custom mode hint also enables multi-agent V2 for the test.
|
||||
fn configure_custom_mode_hint(config: &mut Config) {
|
||||
configure_multi_agent_v2(config);
|
||||
config.multi_agent_v2.multi_agent_mode_hint_text = Some(CUSTOM_MODE_HINT_TEXT.to_string());
|
||||
}
|
||||
|
||||
fn configure_ultra(config: &mut Config) {
|
||||
configure_multi_agent_v2(config);
|
||||
config.model_reasoning_effort = Some(ReasoningEffort::Ultra);
|
||||
@@ -120,6 +128,105 @@ async fn ultra_reasoning_uses_max_and_proactive_mode() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn configured_mode_hint_uses_custom_mode_across_reasoning_efforts() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
(1..=2)
|
||||
.map(|index| {
|
||||
sse(vec![
|
||||
ev_response_created(&format!("resp-{index}")),
|
||||
ev_completed(&format!("resp-{index}")),
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_model_info_override("gpt-5.4", add_ultra_reasoning)
|
||||
.with_config(configure_custom_mode_hint)
|
||||
.build(&server)
|
||||
.await?;
|
||||
let rollout_path = test
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
submit_turn(&test.codex, "explicit", Some(ReasoningEffort::High)).await?;
|
||||
submit_turn(&test.codex, "proactive", Some(ReasoningEffort::Ultra)).await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
let first_input = requests[0].input();
|
||||
let first_texts = developer_texts(&first_input);
|
||||
let second_input = requests[1].input();
|
||||
let second_texts = developer_texts(&second_input);
|
||||
let instruction_counts = |texts: &[&str]| {
|
||||
(
|
||||
count_containing(texts, CUSTOM_MODE_HINT_TEXT),
|
||||
count_containing(texts, NO_SPAWN_TEXT),
|
||||
count_containing(texts, PROACTIVE_TEXT),
|
||||
)
|
||||
};
|
||||
assert_eq!(instruction_counts(&first_texts), (1, 0, 0));
|
||||
assert_eq!(instruction_counts(&second_texts), (1, 0, 0));
|
||||
let rollout_values = std::fs::read_to_string(rollout_path)?
|
||||
.lines()
|
||||
.map(serde_json::from_str::<Value>)
|
||||
.collect::<serde_json::Result<Vec<_>>>()?;
|
||||
let recorded_modes = rollout_values
|
||||
.iter()
|
||||
.filter(|value| value.get("type").and_then(Value::as_str) == Some("turn_context"))
|
||||
.filter_map(|value| value.pointer("/payload/multi_agent_mode").cloned())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
recorded_modes,
|
||||
[
|
||||
json!({"custom": CUSTOM_MODE_HINT_TEXT}),
|
||||
json!({"custom": CUSTOM_MODE_HINT_TEXT}),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn empty_configured_mode_hint_suppresses_builtin_text() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let response = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| {
|
||||
configure_multi_agent_v2(config);
|
||||
config.multi_agent_v2.multi_agent_mode_hint_text = Some(String::new());
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
submit_turn(&test.codex, "hello", Some(ReasoningEffort::High)).await?;
|
||||
|
||||
let input = response.single_request().input();
|
||||
let texts = developer_texts(&input);
|
||||
assert_eq!(
|
||||
(
|
||||
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
|
||||
count_containing(&texts, NO_SPAWN_TEXT),
|
||||
count_containing(&texts, PROACTIVE_TEXT),
|
||||
),
|
||||
(1, 0, 0)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn leaving_ultra_after_cold_resume_emits_explicit_mode() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -56,6 +56,8 @@ pub struct MultiAgentV2ConfigToml {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subagent_usage_hint_text: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub multi_agent_mode_hint_text: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(length(min = 1, max = 64), regex(pattern = r"^[a-zA-Z0-9_-]+$"))]
|
||||
pub tool_namespace: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -610,6 +610,7 @@ usage_hint_enabled = false
|
||||
usage_hint_text = "Custom delegation guidance."
|
||||
root_agent_usage_hint_text = "Root guidance."
|
||||
subagent_usage_hint_text = "Subagent guidance."
|
||||
multi_agent_mode_hint_text = "Custom mode guidance."
|
||||
tool_namespace = "agents"
|
||||
hide_spawn_agent_metadata = true
|
||||
non_code_mode_only = true
|
||||
@@ -633,6 +634,7 @@ non_code_mode_only = true
|
||||
usage_hint_text: Some("Custom delegation guidance.".to_string()),
|
||||
root_agent_usage_hint_text: Some("Root guidance.".to_string()),
|
||||
subagent_usage_hint_text: Some("Subagent guidance.".to_string()),
|
||||
multi_agent_mode_hint_text: Some("Custom mode guidance.".to_string()),
|
||||
tool_namespace: Some("agents".to_string()),
|
||||
hide_spawn_agent_metadata: Some(true),
|
||||
non_code_mode_only: Some(true),
|
||||
|
||||
@@ -296,23 +296,39 @@ pub enum Personality {
|
||||
Pragmatic,
|
||||
}
|
||||
|
||||
/// Controls whether the model receives multi-agent delegation instructions and,
|
||||
/// when it does, whether it should only spawn sub-agents after an explicit user
|
||||
/// request or may delegate proactively when doing so would help. `none` leaves
|
||||
/// the multi-agent tools available without injecting delegation instructions.
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, Default,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/// Controls the effective multi-agent delegation instructions for a turn. `custom` means the
|
||||
/// configured mode hint defines the policy instead of a built-in policy.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Display, JsonSchema, TS, Default)]
|
||||
#[serde(rename_all = "camelCase", from = "MultiAgentModeWire")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum MultiAgentMode {
|
||||
None,
|
||||
Custom(String),
|
||||
#[default]
|
||||
ExplicitRequestOnly,
|
||||
Proactive,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum MultiAgentModeWire {
|
||||
None,
|
||||
Custom(String),
|
||||
ExplicitRequestOnly,
|
||||
Proactive,
|
||||
}
|
||||
|
||||
impl From<MultiAgentModeWire> for MultiAgentMode {
|
||||
fn from(value: MultiAgentModeWire) -> Self {
|
||||
match value {
|
||||
MultiAgentModeWire::None => Self::Custom(String::new()),
|
||||
MultiAgentModeWire::Custom(hint_text) => Self::Custom(hint_text),
|
||||
MultiAgentModeWire::ExplicitRequestOnly => Self::ExplicitRequestOnly,
|
||||
MultiAgentModeWire::Proactive => Self::Proactive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, Default,
|
||||
)]
|
||||
|
||||
@@ -2668,7 +2668,7 @@ impl InitialHistory {
|
||||
| RolloutItem::WorldState(_)
|
||||
| RolloutItem::EventMsg(_) => None,
|
||||
})
|
||||
.and_then(|turn_context| turn_context.multi_agent_mode)
|
||||
.and_then(|turn_context| turn_context.multi_agent_mode.clone())
|
||||
}
|
||||
|
||||
pub fn get_resumed_session_sources(&self) -> Option<(SessionSource, Option<ThreadSource>)> {
|
||||
@@ -5741,6 +5741,25 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_effective_multi_agent_mode_maps_legacy_none_to_empty_custom() -> Result<()> {
|
||||
let value = json!({
|
||||
"cwd": test_path_buf("/tmp"),
|
||||
"approval_policy": "never",
|
||||
"sandbox_policy": { "type": "danger-full-access" },
|
||||
"model": "gpt-5",
|
||||
"multi_agent_mode": "none",
|
||||
"summary": "auto",
|
||||
});
|
||||
let item = RolloutItem::TurnContext(serde_json::from_value(value)?);
|
||||
|
||||
assert_eq!(
|
||||
InitialHistory::Forked(vec![item]).get_latest_effective_multi_agent_mode(),
|
||||
Some(MultiAgentMode::Custom(String::new()))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_context_item_serializes_network_when_present() -> Result<()> {
|
||||
let item = TurnContextItem {
|
||||
|
||||
Reference in New Issue
Block a user