mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add per-turn multi-agent mode (#28685)
## Why Multi-agent v2 currently carries an explicit-request-only delegation rule in its static usage hint. That provides a safe default, but it prevents clients from selecting proactive delegation per turn without changing static guidance or rewriting prior model context. This change makes delegation mode a session selection that can be updated through `turn/start`, while deriving the effective model-visible mode separately for each turn. Eligible multi-agent v2 turns remain explicit-request-only unless proactive mode is both selected and enabled. ## What changed - Add the experimental `turn/start.multiAgentMode` parameter with `explicitRequestOnly` and `proactive` values. Omission retains the loaded session's current optional selection. - Add the default-off `features.multi_agent_mode` feature gate. Eligible multi-agent v2 turns use the selected mode when enabled; an unset selection or disabled gate resolves to `explicitRequestOnly`. - Treat mode prompting as inapplicable for multi-agent v1 and other unsupported session configurations, producing no multi-agent mode developer message rather than rejecting the turn. - Move the explicit-request-only rule out of the static v2 usage hint and into a bounded, tagged developer context fragment. - Emit the effective mode in initial context and only when that effective mode changes on later turns. - Persist the effective mode in `TurnContextItem` as the durable baseline for resume and context-update comparisons. Historical rollout items are not rewritten. Later mode developer messages establish the current rule incrementally. ## Not covered - Initial selection through `thread/start` and selected-mode reporting from thread lifecycle/settings APIs; those are isolated in the stacked #28792. - A TUI control or slash command for selecting the mode. - Persisting a preferred mode to `config.toml`; selection remains session/turn scoped. - Changes to multi-agent concurrency limits, tool availability, or model catalog capability declarations. - Rewriting historical rollout prompt items. Cold resume restores the latest persisted effective mode when available while leaving historical developer messages intact. ## Verification - `CARGO_INCREMENTAL=0 just test -p codex-core multi_agent_mode` - Focused app-server coverage verifies that `turn/start.multiAgentMode` produces proactive developer instructions for an eligible v2 turn. ## Stack Followed by #28792, which adds `thread/start` initialization and lifecycle/settings observability.
This commit is contained in:
committed by
GitHub
Unverified
parent
f886e33e5a
commit
fc8c6b7384
@@ -1760,6 +1760,14 @@
|
||||
"ModelProviderCapabilitiesReadParams": {
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
"restricted",
|
||||
|
||||
+8
@@ -12637,6 +12637,14 @@
|
||||
"title": "ModelVerificationNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
"restricted",
|
||||
|
||||
+8
@@ -9061,6 +9061,14 @@
|
||||
"title": "ModelVerificationNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
"restricted",
|
||||
|
||||
@@ -141,6 +141,14 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MultiAgentMode": {
|
||||
"description": "Controls whether the model should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help.",
|
||||
"enum": [
|
||||
"explicitRequestOnly",
|
||||
"proactive"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
"restricted",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Controls whether the model should only spawn sub-agents after an explicit
|
||||
* user request or may delegate proactively when doing so would help.
|
||||
*/
|
||||
export type MultiAgentMode = "explicitRequestOnly" | "proactive";
|
||||
@@ -49,6 +49,7 @@ export type { LocalShellStatus } from "./LocalShellStatus";
|
||||
export type { McpServerInfo } from "./McpServerInfo";
|
||||
export type { MessagePhase } from "./MessagePhase";
|
||||
export type { ModeKind } from "./ModeKind";
|
||||
export type { MultiAgentMode } from "./MultiAgentMode";
|
||||
export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
|
||||
export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction";
|
||||
export type { ParsedCommand } from "./ParsedCommand";
|
||||
|
||||
@@ -3774,6 +3774,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
|
||||
summary: None,
|
||||
output_schema: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_mode: None,
|
||||
personality: None,
|
||||
};
|
||||
let serialized_without_override =
|
||||
@@ -3781,6 +3782,29 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
|
||||
assert_eq!(serialized_without_override.get("serviceTier"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_start_params_round_trip_multi_agent_mode() {
|
||||
let params: TurnStartParams = serde_json::from_value(json!({
|
||||
"threadId": "thread_123",
|
||||
"input": [],
|
||||
"multiAgentMode": "proactive"
|
||||
}))
|
||||
.expect("params should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
params.multi_agent_mode,
|
||||
Some(codex_protocol::config_types::MultiAgentMode::Proactive)
|
||||
);
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(¶ms),
|
||||
Some("turn/start.multiAgentMode")
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(params).expect("params should serialize")["multiAgentMode"],
|
||||
"proactive"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_settings_update_params_preserve_explicit_null_service_tier() {
|
||||
let params: ThreadSettingsUpdateParams = serde_json::from_value(json!({
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::SandboxPolicy;
|
||||
use super::Turn;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::models::ImageDetail;
|
||||
@@ -149,6 +150,12 @@ pub struct TurnStartParams {
|
||||
#[experimental("turn/start.collaborationMode")]
|
||||
#[ts(optional = nullable)]
|
||||
pub collaboration_mode: Option<CollaborationMode>,
|
||||
|
||||
/// Controls whether multi-agent v2 delegation requires an explicit user request.
|
||||
/// Omitted keeps the loaded session's current mode.
|
||||
#[experimental("turn/start.multiAgentMode")]
|
||||
#[ts(optional = nullable)]
|
||||
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -168,7 +168,7 @@ Example with notification opt-out:
|
||||
- `thread/backgroundTerminals/list` — list running background terminals for a loaded thread (experimental; requires `capabilities.experimentalApi`); returns `data` with the running terminal ids.
|
||||
- `thread/backgroundTerminals/terminate` — terminate one running background terminal by app-server `processId` (experimental; requires `capabilities.experimentalApi`); returns whether a process was terminated.
|
||||
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Experimental `multiAgentMode` accepts `explicitRequestOnly` or `proactive`; omission keeps the loaded session's current mode. The requested mode is retained for the loaded session without rejecting unsupported configurations. Eligible multi-agent v2 turns use the requested mode when `features.multi_agent_mode` is enabled and otherwise use explicit-request-only developer instructions.
|
||||
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
|
||||
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`.
|
||||
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
|
||||
|
||||
@@ -673,6 +673,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
|
||||
personality: None,
|
||||
output_schema: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_mode: None,
|
||||
},
|
||||
},
|
||||
Some(remote_trace),
|
||||
|
||||
@@ -776,6 +776,7 @@ mod thread_processor_behavior_tests {
|
||||
developer_instructions: None,
|
||||
},
|
||||
},
|
||||
multi_agent_mode: Default::default(),
|
||||
session_source: SessionSource::Cli,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
|
||||
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
@@ -60,6 +61,7 @@ struct ThreadSettingsBuildParams {
|
||||
effort: Option<ReasoningEffort>,
|
||||
summary: Option<ReasoningSummary>,
|
||||
collaboration_mode: Option<CollaborationMode>,
|
||||
multi_agent_mode: Option<MultiAgentMode>,
|
||||
personality: Option<Personality>,
|
||||
}
|
||||
|
||||
@@ -454,6 +456,7 @@ impl TurnRequestProcessor {
|
||||
effort: params.effort,
|
||||
summary: params.summary,
|
||||
collaboration_mode: params.collaboration_mode,
|
||||
multi_agent_mode: params.multi_agent_mode,
|
||||
personality: params.personality,
|
||||
},
|
||||
)
|
||||
@@ -560,6 +563,7 @@ impl TurnRequestProcessor {
|
||||
effort,
|
||||
summary,
|
||||
collaboration_mode,
|
||||
multi_agent_mode,
|
||||
personality,
|
||||
} = params;
|
||||
|
||||
@@ -593,6 +597,7 @@ impl TurnRequestProcessor {
|
||||
|| effort.is_some()
|
||||
|| summary.is_some()
|
||||
|| collaboration_mode.is_some()
|
||||
|| multi_agent_mode.is_some()
|
||||
|| personality.is_some();
|
||||
|
||||
let runtime_workspace_roots =
|
||||
@@ -669,6 +674,7 @@ impl TurnRequestProcessor {
|
||||
summary,
|
||||
service_tier: service_tier.clone(),
|
||||
collaboration_mode: collaboration_mode.clone(),
|
||||
multi_agent_mode,
|
||||
personality,
|
||||
})
|
||||
.await
|
||||
@@ -692,6 +698,7 @@ impl TurnRequestProcessor {
|
||||
summary,
|
||||
service_tier,
|
||||
collaboration_mode,
|
||||
multi_agent_mode,
|
||||
personality,
|
||||
})
|
||||
}
|
||||
@@ -722,6 +729,7 @@ impl TurnRequestProcessor {
|
||||
effort: params.effort,
|
||||
summary: params.summary,
|
||||
collaboration_mode: params.collaboration_mode,
|
||||
multi_agent_mode: None,
|
||||
personality: params.personality,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -69,6 +69,7 @@ use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::Settings;
|
||||
@@ -1749,6 +1750,83 @@ async fn turn_start_accepts_personality_override_v2() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_accepts_multi_agent_mode_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let response_mock = responses::mount_sse_once(&server, body).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::from([
|
||||
(Feature::MultiAgentV2, true),
|
||||
(Feature::MultiAgentMode, true),
|
||||
]),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
multi_agent_mode: Some(MultiAgentMode::Proactive),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let _: TurnStartResponse = to_response(turn_resp)?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let developer_texts = response_mock
|
||||
.single_request()
|
||||
.message_input_texts("developer");
|
||||
assert!(developer_texts.iter().any(|text| {
|
||||
text.contains("<multi_agent_mode>")
|
||||
&& text.contains("Proactive multi-agent delegation is active.")
|
||||
}));
|
||||
assert!(!developer_texts.iter().any(|text| {
|
||||
text.contains("Do not spawn sub-agents unless the user explicitly asks for sub-agents")
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_change_personality_mid_thread_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -2367,6 +2445,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
|
||||
personality: None,
|
||||
output_schema: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_mode: None,
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
@@ -2406,6 +2485,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
|
||||
personality: None,
|
||||
output_schema: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_mode: None,
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
|
||||
@@ -557,6 +557,9 @@
|
||||
"multi_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"multi_agent_mode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"multi_agent_v2": {
|
||||
"$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml"
|
||||
},
|
||||
@@ -4793,6 +4796,9 @@
|
||||
"multi_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"multi_agent_mode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"multi_agent_v2": {
|
||||
"$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
@@ -71,6 +72,7 @@ pub struct ThreadConfigSnapshot {
|
||||
pub reasoning_summary: Option<ReasoningSummary>,
|
||||
pub personality: Option<Personality>,
|
||||
pub collaboration_mode: CollaborationMode,
|
||||
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||
pub session_source: SessionSource,
|
||||
pub forked_from_thread_id: Option<ThreadId>,
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
@@ -151,6 +153,7 @@ pub struct CodexThreadSettingsOverrides {
|
||||
pub summary: Option<ReasoningSummary>,
|
||||
pub service_tier: Option<Option<String>>,
|
||||
pub collaboration_mode: Option<CollaborationMode>,
|
||||
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||
pub personality: Option<Personality>,
|
||||
}
|
||||
|
||||
@@ -371,6 +374,7 @@ impl CodexThread {
|
||||
summary,
|
||||
service_tier,
|
||||
collaboration_mode,
|
||||
multi_agent_mode,
|
||||
personality,
|
||||
} = overrides;
|
||||
let collaboration_mode = if let Some(collaboration_mode) = collaboration_mode {
|
||||
@@ -394,6 +398,7 @@ impl CodexThread {
|
||||
active_permission_profile,
|
||||
windows_sandbox_level,
|
||||
collaboration_mode: Some(collaboration_mode),
|
||||
multi_agent_mode,
|
||||
reasoning_summary: summary,
|
||||
service_tier,
|
||||
personality,
|
||||
|
||||
@@ -10055,9 +10055,8 @@ max_concurrent_threads_per_session = 17
|
||||
|
||||
let config = resolve_multi_agent_v2_config(&config_toml);
|
||||
let concurrency_guidance = "There are 17 available concurrency slots, meaning that up to 17 agents can be active at once, including you.";
|
||||
let expected_suffix = format!(
|
||||
"{DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT}\n{concurrency_guidance}\n\n{DEFAULT_MULTI_AGENT_V2_NO_SPAWN_HINT_TEXT}"
|
||||
);
|
||||
let expected_suffix =
|
||||
format!("{DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT}\n{concurrency_guidance}");
|
||||
assert!(
|
||||
[
|
||||
config.root_agent_usage_hint_text,
|
||||
|
||||
@@ -247,11 +247,9 @@ All agents share the same directory. In detail:
|
||||
- All agents use the same current working directory.
|
||||
- As a result, edits made by one agent are immediately visible to all other agents.
|
||||
"#;
|
||||
const DEFAULT_MULTI_AGENT_V2_NO_SPAWN_HINT_TEXT: &str = "Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegation, or parallel agent work.";
|
||||
|
||||
fn default_multi_agent_v2_usage_hint_text(usage_hint_text: &str, max_concurrency: usize) -> String {
|
||||
format!(
|
||||
"{usage_hint_text}\n{DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT}\nThere are {max_concurrency} available concurrency slots, meaning that up to {max_concurrency} agents can be active at once, including you.\n\n{DEFAULT_MULTI_AGENT_V2_NO_SPAWN_HINT_TEXT}"
|
||||
"{usage_hint_text}\n{DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT}\nThere are {max_concurrency} available concurrency slots, meaning that up to {max_concurrency} agents can be active at once, including you."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() {
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: None,
|
||||
effort: None,
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
|
||||
@@ -17,6 +17,7 @@ mod legacy_apply_patch_exec_command_warning;
|
||||
mod legacy_model_mismatch_warning;
|
||||
mod legacy_unified_exec_process_limit_warning;
|
||||
mod model_switch_instructions;
|
||||
mod multi_agent_mode_instructions;
|
||||
mod network_rule_saved;
|
||||
mod permissions_instructions;
|
||||
mod personality_spec_instructions;
|
||||
@@ -59,6 +60,7 @@ pub(crate) use legacy_apply_patch_exec_command_warning::LegacyApplyPatchExecComm
|
||||
pub(crate) use legacy_model_mismatch_warning::LegacyModelMismatchWarning;
|
||||
pub(crate) use legacy_unified_exec_process_limit_warning::LegacyUnifiedExecProcessLimitWarning;
|
||||
pub(crate) use model_switch_instructions::ModelSwitchInstructions;
|
||||
pub(crate) use multi_agent_mode_instructions::MultiAgentModeInstructions;
|
||||
pub(crate) use network_rule_saved::NetworkRuleSaved;
|
||||
pub use permissions_instructions::PermissionsInstructions;
|
||||
pub(crate) use personality_spec_instructions::PersonalitySpecInstructions;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use super::ContextualUserFragment;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
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 explicitly asks for sub-agents, delegation, or parallel agent work.";
|
||||
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)]
|
||||
pub(crate) struct MultiAgentModeInstructions {
|
||||
multi_agent_mode: MultiAgentMode,
|
||||
}
|
||||
|
||||
impl MultiAgentModeInstructions {
|
||||
pub(crate) fn new(multi_agent_mode: MultiAgentMode) -> Self {
|
||||
Self { multi_agent_mode }
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for MultiAgentModeInstructions {
|
||||
fn role(&self) -> &'static str {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
(MULTI_AGENT_MODE_OPEN_TAG, MULTI_AGENT_MODE_CLOSE_TAG)
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
match self.multi_agent_mode {
|
||||
MultiAgentMode::ExplicitRequestOnly => {
|
||||
EXPLICIT_REQUEST_ONLY_MULTI_AGENT_MODE_TEXT.to_string()
|
||||
}
|
||||
MultiAgentMode::Proactive => PROACTIVE_MULTI_AGENT_MODE_TEXT.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +146,7 @@ fn reference_context_item() -> TurnContextItem {
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(false),
|
||||
effort: None,
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -935,6 +936,7 @@ fn drop_last_n_user_turns_trims_context_updates_above_rolled_back_turn() {
|
||||
assistant_msg("turn 1 assistant"),
|
||||
developer_msg("Generated images are saved to /tmp as /tmp/image-1.png by default."),
|
||||
developer_msg("<collaboration_mode>ROLLED_BACK_DEV_INSTRUCTIONS</collaboration_mode>"),
|
||||
developer_msg("<multi_agent_mode>ROLLED_BACK_MULTI_AGENT_MODE</multi_agent_mode>"),
|
||||
user_input_text_msg(
|
||||
"<environment_context><cwd>PRETURN_CONTEXT_DIFF_CWD</cwd></environment_context>",
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::context::CollaborationModeInstructions;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::EnvironmentContext;
|
||||
use crate::context::ModelSwitchInstructions;
|
||||
use crate::context::MultiAgentModeInstructions;
|
||||
use crate::context::PermissionsInstructions;
|
||||
use crate::context::PersonalitySpecInstructions;
|
||||
use crate::context::RealtimeEndInstructions;
|
||||
@@ -12,6 +13,7 @@ use crate::session::turn_context::TurnContext;
|
||||
use crate::shell::Shell;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -95,6 +97,31 @@ fn build_collaboration_mode_update_item(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_multi_agent_mode_update_item(
|
||||
previous: Option<&TurnContextItem>,
|
||||
next: &TurnContext,
|
||||
) -> Option<String> {
|
||||
let effective_multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode(
|
||||
next.multi_agent_version,
|
||||
&next.config.multi_agent_v2,
|
||||
&next.session_source,
|
||||
next.multi_agent_mode,
|
||||
next.features.enabled(Feature::MultiAgentMode),
|
||||
);
|
||||
let previous = previous?;
|
||||
if previous.multi_agent_mode == effective_multi_agent_mode {
|
||||
return None;
|
||||
}
|
||||
|
||||
match effective_multi_agent_mode {
|
||||
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())
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_realtime_update_item(
|
||||
previous: Option<&TurnContextItem>,
|
||||
previous_turn_settings: Option<&PreviousTurnSettings>,
|
||||
@@ -230,6 +257,7 @@ pub(crate) fn build_settings_update_items(
|
||||
build_model_instructions_update_item(previous_turn_settings, next),
|
||||
build_permissions_update_item(previous, next, exec_policy),
|
||||
build_collaboration_mode_update_item(previous, next),
|
||||
build_multi_agent_mode_update_item(previous, next),
|
||||
build_realtime_update_item(previous, previous_turn_settings, next),
|
||||
build_personality_update_item(previous, next, personality_feature_enabled),
|
||||
]
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_protocol::models::is_image_open_tag_text;
|
||||
use codex_protocol::models::is_local_image_close_tag_text;
|
||||
use codex_protocol::models::is_local_image_open_tag_text;
|
||||
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
|
||||
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
||||
use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
|
||||
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
@@ -29,6 +30,7 @@ const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
|
||||
"<permissions instructions>",
|
||||
"<model_switch>",
|
||||
COLLABORATION_MODE_OPEN_TAG,
|
||||
MULTI_AGENT_MODE_OPEN_TAG,
|
||||
REALTIME_CONVERSATION_OPEN_TAG,
|
||||
SKILLS_INSTRUCTIONS_OPEN_TAG,
|
||||
"<personality_spec>",
|
||||
|
||||
@@ -124,6 +124,7 @@ async fn thread_settings_update(
|
||||
summary,
|
||||
service_tier,
|
||||
collaboration_mode,
|
||||
multi_agent_mode,
|
||||
personality,
|
||||
} = thread_settings;
|
||||
let collaboration_mode = match collaboration_mode {
|
||||
@@ -149,6 +150,7 @@ async fn thread_settings_update(
|
||||
active_permission_profile,
|
||||
windows_sandbox_level,
|
||||
collaboration_mode: Some(collaboration_mode),
|
||||
multi_agent_mode,
|
||||
reasoning_summary: summary,
|
||||
service_tier,
|
||||
personality,
|
||||
|
||||
@@ -27,6 +27,7 @@ use crate::context::AvailablePluginsInstructions;
|
||||
use crate::context::AvailableSkillsInstructions;
|
||||
use crate::context::CollaborationModeInstructions;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::MultiAgentModeInstructions;
|
||||
use crate::context::NetworkRuleSaved;
|
||||
use crate::context::PermissionsInstructions;
|
||||
use crate::context::PersonalitySpecInstructions;
|
||||
@@ -92,6 +93,7 @@ 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;
|
||||
@@ -210,7 +212,7 @@ mod handlers;
|
||||
mod inject;
|
||||
mod input_queue;
|
||||
mod mcp;
|
||||
mod multi_agents;
|
||||
pub(crate) mod multi_agents;
|
||||
mod review;
|
||||
mod rollout_budget;
|
||||
mod rollout_reconstruction;
|
||||
@@ -581,6 +583,7 @@ impl Codex {
|
||||
.await;
|
||||
let multi_agent_version =
|
||||
resolve_multi_agent_version(&conversation_history, inherited_multi_agent_version);
|
||||
let multi_agent_mode = conversation_history.get_multi_agent_mode();
|
||||
config
|
||||
.validate_multi_agent_v2_config()
|
||||
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
||||
@@ -614,6 +617,7 @@ impl Codex {
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
service_tier,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
@@ -3240,6 +3244,17 @@ impl Session {
|
||||
{
|
||||
items.push(usage_hint_message);
|
||||
}
|
||||
if let Some(multi_agent_mode) = multi_agents::effective_multi_agent_mode(
|
||||
turn_context.multi_agent_version,
|
||||
&turn_context.config.multi_agent_v2,
|
||||
&session_source,
|
||||
turn_context.multi_agent_mode,
|
||||
turn_context.features.enabled(Feature::MultiAgentMode),
|
||||
) {
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::config::MultiAgentV2Config;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -16,6 +18,13 @@ pub(super) fn usage_hint_text<'a>(
|
||||
return None;
|
||||
}
|
||||
|
||||
configured_usage_hint_text_for_source(multi_agent_v2, session_source)
|
||||
}
|
||||
|
||||
fn configured_usage_hint_text_for_source<'a>(
|
||||
multi_agent_v2: &'a MultiAgentV2Config,
|
||||
session_source: &SessionSource,
|
||||
) -> Option<&'a str> {
|
||||
match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) => {
|
||||
multi_agent_v2.subagent_usage_hint_text.as_deref()
|
||||
@@ -29,3 +38,31 @@ pub(super) fn usage_hint_text<'a>(
|
||||
SessionSource::Internal(_) | SessionSource::SubAgent(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn multi_agent_mode_is_applicable(
|
||||
multi_agent_version: MultiAgentVersion,
|
||||
multi_agent_v2: &MultiAgentV2Config,
|
||||
session_source: &SessionSource,
|
||||
) -> bool {
|
||||
multi_agent_version == MultiAgentVersion::V2
|
||||
&& multi_agent_v2.usage_hint_enabled
|
||||
&& configured_usage_hint_text_for_source(multi_agent_v2, session_source).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn effective_multi_agent_mode(
|
||||
multi_agent_version: MultiAgentVersion,
|
||||
multi_agent_v2: &MultiAgentV2Config,
|
||||
session_source: &SessionSource,
|
||||
requested_multi_agent_mode: Option<MultiAgentMode>,
|
||||
multi_agent_mode_enabled: bool,
|
||||
) -> Option<MultiAgentMode> {
|
||||
if !multi_agent_mode_is_applicable(multi_agent_version, multi_agent_v2, session_source) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(if multi_agent_mode_enabled {
|
||||
requested_multi_agent_mode.unwrap_or_default()
|
||||
} else {
|
||||
MultiAgentMode::ExplicitRequestOnly
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ pub(super) async fn spawn_review_thread(
|
||||
developer_instructions: None,
|
||||
user_instructions: None,
|
||||
collaboration_mode: parent_turn_context.collaboration_mode.clone(),
|
||||
multi_agent_mode: parent_turn_context.multi_agent_mode,
|
||||
multi_agent_version: MultiAgentVersion::Disabled,
|
||||
personality: parent_turn_context.personality,
|
||||
approval_policy: parent_turn_context.approval_policy.clone(),
|
||||
|
||||
@@ -102,6 +102,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
|
||||
personality: turn_context.personality,
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -142,6 +143,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
personality: turn_context.personality,
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -1007,6 +1009,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
personality: turn_context.personality,
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -1090,6 +1093,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
personality: turn_context.personality,
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -1120,6 +1124,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
personality: turn_context.personality,
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -1243,6 +1248,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
personality: turn_context.personality,
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -1363,6 +1369,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
personality: turn_context.personality,
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -1527,6 +1534,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
personality: turn_context.personality,
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
|
||||
@@ -52,6 +52,7 @@ pub(crate) struct SessionConfiguration {
|
||||
pub(super) provider: ModelProviderInfo,
|
||||
|
||||
pub(super) collaboration_mode: CollaborationMode,
|
||||
pub(super) multi_agent_mode: Option<MultiAgentMode>,
|
||||
pub(super) model_reasoning_summary: Option<ReasoningSummaryConfig>,
|
||||
pub(super) service_tier: Option<String>,
|
||||
|
||||
@@ -192,6 +193,7 @@ impl SessionConfiguration {
|
||||
reasoning_summary: self.model_reasoning_summary,
|
||||
personality: self.personality,
|
||||
collaboration_mode: self.collaboration_mode.clone(),
|
||||
multi_agent_mode: self.multi_agent_mode,
|
||||
session_source: self.session_source.clone(),
|
||||
forked_from_thread_id: self.forked_from_thread_id,
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
@@ -228,6 +230,9 @@ impl SessionConfiguration {
|
||||
if let Some(collaboration_mode) = updates.collaboration_mode.clone() {
|
||||
next_configuration.collaboration_mode = collaboration_mode;
|
||||
}
|
||||
if let Some(multi_agent_mode) = updates.multi_agent_mode {
|
||||
next_configuration.multi_agent_mode = Some(multi_agent_mode);
|
||||
}
|
||||
if let Some(summary) = updates.reasoning_summary {
|
||||
next_configuration.model_reasoning_summary = Some(summary);
|
||||
}
|
||||
@@ -422,6 +427,7 @@ pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub(crate) windows_sandbox_level: Option<WindowsSandboxLevel>,
|
||||
pub(crate) collaboration_mode: Option<CollaborationMode>,
|
||||
pub(crate) multi_agent_mode: Option<MultiAgentMode>,
|
||||
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
|
||||
pub(crate) service_tier: Option<Option<String>>,
|
||||
pub(crate) final_output_json_schema: Option<Option<Value>>,
|
||||
|
||||
@@ -2732,6 +2732,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
personality: turn_context.personality,
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -3349,6 +3350,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode: Default::default(),
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
loaded_agents_md: None,
|
||||
@@ -3455,6 +3457,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode: Default::default(),
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
loaded_agents_md: None,
|
||||
@@ -3982,6 +3985,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode: Default::default(),
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
loaded_agents_md: None,
|
||||
@@ -4848,6 +4852,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode: Default::default(),
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
loaded_agents_md: None,
|
||||
@@ -4960,6 +4965,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode: Default::default(),
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
loaded_agents_md: None,
|
||||
@@ -5205,6 +5211,7 @@ async fn make_session_with_config_and_rx(
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode: Default::default(),
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
loaded_agents_md: None,
|
||||
@@ -5311,6 +5318,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode: Default::default(),
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
loaded_agents_md: None,
|
||||
@@ -7017,6 +7025,7 @@ where
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode: Default::default(),
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
loaded_agents_md: None,
|
||||
|
||||
@@ -125,6 +125,7 @@ pub struct TurnContext {
|
||||
pub(crate) developer_instructions: Option<String>,
|
||||
pub(crate) user_instructions: Option<String>,
|
||||
pub(crate) collaboration_mode: CollaborationMode,
|
||||
pub(crate) multi_agent_mode: Option<MultiAgentMode>,
|
||||
pub(crate) multi_agent_version: MultiAgentVersion,
|
||||
pub(crate) personality: Option<Personality>,
|
||||
pub(crate) approval_policy: Constrained<AskForApproval>,
|
||||
@@ -276,6 +277,7 @@ impl TurnContext {
|
||||
developer_instructions: self.developer_instructions.clone(),
|
||||
user_instructions: self.user_instructions.clone(),
|
||||
collaboration_mode,
|
||||
multi_agent_mode: self.multi_agent_mode,
|
||||
multi_agent_version: self.multi_agent_version,
|
||||
personality: self.personality,
|
||||
approval_policy: self.approval_policy.clone(),
|
||||
@@ -381,6 +383,13 @@ impl TurnContext {
|
||||
personality: self.personality,
|
||||
collaboration_mode: Some(self.collaboration_mode.clone()),
|
||||
multi_agent_version: Some(self.multi_agent_version),
|
||||
multi_agent_mode: super::multi_agents::effective_multi_agent_mode(
|
||||
self.multi_agent_version,
|
||||
&self.config.multi_agent_v2,
|
||||
&self.session_source,
|
||||
self.multi_agent_mode,
|
||||
self.features.enabled(Feature::MultiAgentMode),
|
||||
),
|
||||
realtime_active: Some(self.realtime_active),
|
||||
effort: self.reasoning_effort.clone(),
|
||||
summary: ReasoningSummaryConfig::Auto,
|
||||
@@ -562,6 +571,7 @@ impl Session {
|
||||
.as_ref()
|
||||
.map(LoadedAgentsMd::render),
|
||||
collaboration_mode: session_configuration.collaboration_mode.clone(),
|
||||
multi_agent_mode: session_configuration.multi_agent_mode,
|
||||
multi_agent_version,
|
||||
personality: session_configuration.personality,
|
||||
approval_policy: session_configuration.approval_policy.clone(),
|
||||
|
||||
@@ -73,6 +73,7 @@ mod model_switching;
|
||||
mod model_visible_layout;
|
||||
mod models_cache_ttl;
|
||||
mod models_etag_responses;
|
||||
mod multi_agent_mode;
|
||||
mod openai_file_mcp;
|
||||
mod otel;
|
||||
mod override_updates;
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
use anyhow::Result;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
|
||||
const NO_SPAWN_TEXT: &str = "Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegation, or parallel agent work.";
|
||||
const PROACTIVE_TEXT: &str = "Proactive multi-agent delegation is active.";
|
||||
|
||||
fn developer_texts(input: &[Value]) -> Vec<&str> {
|
||||
input
|
||||
.iter()
|
||||
.filter(|item| item.get("role").and_then(Value::as_str) == Some("developer"))
|
||||
.filter_map(|item| item.get("content")?.as_array())
|
||||
.flatten()
|
||||
.filter_map(|content| content.get("text")?.as_str())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn count_containing(texts: &[&str], target: &str) -> usize {
|
||||
texts.iter().filter(|text| text.contains(target)).count()
|
||||
}
|
||||
|
||||
async fn submit_turn(
|
||||
codex: &codex_core::CodexThread,
|
||||
prompt: &str,
|
||||
mode: Option<MultiAgentMode>,
|
||||
) -> Result<()> {
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: prompt.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: ThreadSettingsOverrides {
|
||||
multi_agent_mode: mode,
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multi_agent_mode_is_sticky_and_emits_only_on_change() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
(1..=3)
|
||||
.map(|index| {
|
||||
sse(vec![
|
||||
ev_response_created(&format!("resp-{index}")),
|
||||
ev_completed(&format!("resp-{index}")),
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentMode)
|
||||
.expect("test config should allow feature update");
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
submit_turn(&test.codex, "turn one", /*mode*/ None).await?;
|
||||
assert_eq!(test.codex.config_snapshot().await.multi_agent_mode, None);
|
||||
submit_turn(&test.codex, "turn two", Some(MultiAgentMode::Proactive)).await?;
|
||||
submit_turn(&test.codex, "turn three", /*mode*/ None).await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
let inputs = requests
|
||||
.iter()
|
||||
.map(core_test_support::responses::ResponsesRequest::input)
|
||||
.collect::<Vec<_>>();
|
||||
let first = developer_texts(&inputs[0]);
|
||||
let second = developer_texts(&inputs[1]);
|
||||
let third = developer_texts(&inputs[2]);
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
count_containing(&first, MULTI_AGENT_MODE_OPEN_TAG),
|
||||
count_containing(&first, NO_SPAWN_TEXT),
|
||||
count_containing(&first, PROACTIVE_TEXT),
|
||||
),
|
||||
(1, 1, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
count_containing(&second, MULTI_AGENT_MODE_OPEN_TAG),
|
||||
count_containing(&second, NO_SPAWN_TEXT),
|
||||
count_containing(&second, PROACTIVE_TEXT),
|
||||
),
|
||||
(2, 1, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
count_containing(&third, MULTI_AGENT_MODE_OPEN_TAG),
|
||||
count_containing(&third, NO_SPAWN_TEXT),
|
||||
count_containing(&third, PROACTIVE_TEXT),
|
||||
),
|
||||
(2, 1, 1)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multi_agent_mode_feature_uses_explicit_mode_when_disabled() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
submit_turn(&test.codex, "hello", /*mode*/ None).await?;
|
||||
|
||||
let input = responses.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, 1, 0)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn resume_compares_against_previous_effective_multi_agent_mode() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
(1..=4)
|
||||
.map(|index| {
|
||||
sse(vec![
|
||||
ev_response_created(&format!("resp-{index}")),
|
||||
ev_completed(&format!("resp-{index}")),
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
let initial = test_codex()
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
submit_turn(
|
||||
&initial.codex,
|
||||
"before resume",
|
||||
Some(MultiAgentMode::Proactive),
|
||||
)
|
||||
.await?;
|
||||
drop(initial);
|
||||
|
||||
let mut resume_builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentMode)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
submit_turn(
|
||||
&resumed.codex,
|
||||
"after resume",
|
||||
Some(MultiAgentMode::Proactive),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
let resumed_input = requests[1].input();
|
||||
let texts = developer_texts(&resumed_input);
|
||||
assert_eq!(
|
||||
(
|
||||
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
|
||||
count_containing(&texts, NO_SPAWN_TEXT),
|
||||
count_containing(&texts, PROACTIVE_TEXT),
|
||||
),
|
||||
(2, 1, 1)
|
||||
);
|
||||
|
||||
let resumed_rollout_path = resumed
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("resumed rollout path");
|
||||
let resumed_home = resumed.home.clone();
|
||||
drop(resumed);
|
||||
let mut same_mode_resume_builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentMode)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let resumed_same_mode = same_mode_resume_builder
|
||||
.resume(&server, resumed_home, resumed_rollout_path)
|
||||
.await?;
|
||||
submit_turn(
|
||||
&resumed_same_mode.codex,
|
||||
"after same-mode resume",
|
||||
/*mode*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
resumed_same_mode
|
||||
.codex
|
||||
.config_snapshot()
|
||||
.await
|
||||
.multi_agent_mode,
|
||||
Some(MultiAgentMode::Proactive)
|
||||
);
|
||||
let requests = responses.requests();
|
||||
let resumed_same_mode_input = requests[2].input();
|
||||
let texts = developer_texts(&resumed_same_mode_input);
|
||||
assert_eq!(
|
||||
(
|
||||
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
|
||||
count_containing(&texts, NO_SPAWN_TEXT),
|
||||
count_containing(&texts, PROACTIVE_TEXT),
|
||||
),
|
||||
(2, 1, 1)
|
||||
);
|
||||
|
||||
let resumed_same_mode_rollout_path = resumed_same_mode
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("same-mode resumed rollout path");
|
||||
let resumed_same_mode_home = resumed_same_mode.home.clone();
|
||||
drop(resumed_same_mode);
|
||||
let mut disabled_mode_resume_builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let resumed_disabled_mode = disabled_mode_resume_builder
|
||||
.resume(
|
||||
&server,
|
||||
resumed_same_mode_home,
|
||||
resumed_same_mode_rollout_path,
|
||||
)
|
||||
.await?;
|
||||
submit_turn(
|
||||
&resumed_disabled_mode.codex,
|
||||
"after disabled-mode resume",
|
||||
/*mode*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
resumed_disabled_mode
|
||||
.codex
|
||||
.config_snapshot()
|
||||
.await
|
||||
.multi_agent_mode,
|
||||
Some(MultiAgentMode::Proactive)
|
||||
);
|
||||
let requests = responses.requests();
|
||||
let resumed_disabled_mode_input = requests[3].input();
|
||||
let texts = developer_texts(&resumed_disabled_mode_input);
|
||||
assert_eq!(
|
||||
(
|
||||
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
|
||||
count_containing(&texts, NO_SPAWN_TEXT),
|
||||
count_containing(&texts, PROACTIVE_TEXT),
|
||||
),
|
||||
(3, 2, 1)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn explicit_multi_agent_mode_is_retained_without_multi_agent_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentMode)
|
||||
.expect("test config should allow feature update");
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
submit_turn(&test.codex, "hello", Some(MultiAgentMode::Proactive)).await?;
|
||||
|
||||
assert_eq!(
|
||||
test.codex.config_snapshot().await.multi_agent_mode,
|
||||
Some(MultiAgentMode::Proactive)
|
||||
);
|
||||
let input = responses.single_request().input();
|
||||
let texts = developer_texts(&input);
|
||||
assert_eq!(
|
||||
(
|
||||
count_containing(&texts, MULTI_AGENT_MODE_OPEN_TAG),
|
||||
count_containing(&texts, PROACTIVE_TEXT),
|
||||
),
|
||||
(0, 0)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -41,6 +41,7 @@ fn resume_history(
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: None,
|
||||
effort: config.model_reasoning_effort.clone(),
|
||||
summary: config
|
||||
|
||||
@@ -891,6 +891,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
personality: None,
|
||||
output_schema,
|
||||
collaboration_mode: None,
|
||||
multi_agent_mode: None,
|
||||
},
|
||||
},
|
||||
"turn/start",
|
||||
|
||||
@@ -144,6 +144,8 @@ pub enum Feature {
|
||||
Collab,
|
||||
/// Enable task-path-based multi-agent routing.
|
||||
MultiAgentV2,
|
||||
/// Enable per-turn multi-agent mode selection.
|
||||
MultiAgentMode,
|
||||
/// Enable CSV-backed agent job tools.
|
||||
SpawnCsv,
|
||||
/// Enable apps.
|
||||
@@ -1018,6 +1020,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::MultiAgentMode,
|
||||
key: "multi_agent_mode",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::SpawnCsv,
|
||||
key: "enable_fanout",
|
||||
|
||||
@@ -296,6 +296,20 @@ pub enum Personality {
|
||||
Pragmatic,
|
||||
}
|
||||
|
||||
/// Controls whether the model should only spawn sub-agents after an explicit
|
||||
/// user request or may delegate proactively when doing so would help.
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, Default,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum MultiAgentMode {
|
||||
#[default]
|
||||
ExplicitRequestOnly,
|
||||
Proactive,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, Default,
|
||||
)]
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::approvals::ElicitationRequestEvent;
|
||||
use crate::config_types::ApprovalsReviewer;
|
||||
use crate::config_types::CollaborationMode;
|
||||
use crate::config_types::ModeKind;
|
||||
use crate::config_types::MultiAgentMode;
|
||||
use crate::config_types::Personality;
|
||||
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use crate::config_types::WindowsSandboxLevel;
|
||||
@@ -104,6 +105,8 @@ pub const PLUGINS_INSTRUCTIONS_OPEN_TAG: &str = "<plugins_instructions>";
|
||||
pub const PLUGINS_INSTRUCTIONS_CLOSE_TAG: &str = "</plugins_instructions>";
|
||||
pub const COLLABORATION_MODE_OPEN_TAG: &str = "<collaboration_mode>";
|
||||
pub const COLLABORATION_MODE_CLOSE_TAG: &str = "</collaboration_mode>";
|
||||
pub const MULTI_AGENT_MODE_OPEN_TAG: &str = "<multi_agent_mode>";
|
||||
pub const MULTI_AGENT_MODE_CLOSE_TAG: &str = "</multi_agent_mode>";
|
||||
pub const REALTIME_CONVERSATION_OPEN_TAG: &str = "<realtime_conversation>";
|
||||
pub const REALTIME_CONVERSATION_CLOSE_TAG: &str = "</realtime_conversation>";
|
||||
pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
|
||||
@@ -479,6 +482,9 @@ pub struct ThreadSettingsOverrides {
|
||||
/// Takes precedence over model, effort, and developer instructions if set.
|
||||
pub collaboration_mode: Option<CollaborationMode>,
|
||||
|
||||
/// Updated multi-agent mode for this turn and subsequent turns.
|
||||
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||
|
||||
/// Updated personality preference.
|
||||
pub personality: Option<Personality>,
|
||||
}
|
||||
@@ -2548,6 +2554,14 @@ impl InitialHistory {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_multi_agent_mode(&self) -> Option<MultiAgentMode> {
|
||||
match self {
|
||||
InitialHistory::New | InitialHistory::Cleared => None,
|
||||
InitialHistory::Resumed(resumed) => multi_agent_mode_from_items(&resumed.history),
|
||||
InitialHistory::Forked(items) => multi_agent_mode_from_items(items),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_resumed_session_sources(&self) -> Option<(SessionSource, Option<ThreadSource>)> {
|
||||
let meta = self.get_resumed_session_meta()?;
|
||||
Some((meta.source.clone(), meta.thread_source.clone()))
|
||||
@@ -2860,6 +2874,17 @@ fn multi_agent_version_from_items(
|
||||
})
|
||||
}
|
||||
|
||||
fn multi_agent_mode_from_items(items: &[RolloutItem]) -> Option<MultiAgentMode> {
|
||||
items.iter().rev().find_map(|item| match item {
|
||||
RolloutItem::TurnContext(turn_context) => turn_context.multi_agent_mode,
|
||||
RolloutItem::SessionMeta(_)
|
||||
| RolloutItem::ResponseItem(_)
|
||||
| RolloutItem::InterAgentCommunication(_)
|
||||
| RolloutItem::Compacted(_)
|
||||
| RolloutItem::EventMsg(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
@@ -3027,6 +3052,9 @@ pub struct TurnContextItem {
|
||||
pub collaboration_mode: Option<CollaborationMode>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
/// Effective model-visible mode used as the durable context-diff baseline.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub realtime_active: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -5399,6 +5427,7 @@ mod tests {
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: None,
|
||||
effort: None,
|
||||
summary: ReasoningSummaryConfig::Auto,
|
||||
|
||||
@@ -1152,6 +1152,7 @@ async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Re
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: None,
|
||||
effort: None,
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
|
||||
@@ -368,6 +368,7 @@ mod tests {
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: None,
|
||||
effort: None,
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -412,6 +413,7 @@ mod tests {
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: None,
|
||||
effort: None,
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -452,6 +454,7 @@ mod tests {
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: None,
|
||||
effort: Some(ReasoningEffort::High),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
@@ -489,6 +492,7 @@ mod tests {
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
multi_agent_mode: None,
|
||||
realtime_active: None,
|
||||
effort: Some(ReasoningEffort::High),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
|
||||
@@ -786,6 +786,7 @@ impl AppServerSession {
|
||||
personality,
|
||||
output_schema,
|
||||
collaboration_mode,
|
||||
multi_agent_mode: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user