mirror of
https://github.com/openai/codex.git
synced 2026-11-03 08:22:30 +00:00
Expose thread-level multi-agent mode (#28792)
## Why Once multi-agent mode can be selected per turn, clients also need to choose the initial selection when creating a thread and observe that selection through lifecycle and settings APIs. The selected value is intentionally distinct from the effective model-visible value: no client selection is represented as `null`, even though an eligible multi-agent v2 turn derives `explicitRequestOnly` as its effective default. ## What changed - Add the optional experimental `thread/start.multiAgentMode` parameter and pass it through thread creation. - Preserve an omitted initial value as an unset selection rather than eagerly storing `explicitRequestOnly`. - Apply an explicit `thread/start` selection to the first turn through the session configuration established at thread creation. - Restore the latest persisted effective mode as the selected baseline on cold resume when rollout history contains one. - Inherit the optional selected mode from a loaded parent when creating related runtime threads. - Return the current selected `multiAgentMode` from `thread/start`, `thread/resume`, `thread/fork`, and thread settings, using `null` when no mode is selected. - Keep lifecycle reporting independent from model capability and feature eligibility; core turn construction remains responsible for calculating and persisting the effective mode. ## Not covered - Clearing an existing loaded-session selection back to unset through `turn/start`; omitted or `null` currently retains the session's selection. - A TUI control, slash command, or `config.toml` preference. ## Verification - `CARGO_INCREMENTAL=0 just test -p codex-app-server-protocol` - `CARGO_INCREMENTAL=0 just test -p codex-app-server multi_agent_mode` The focused app-server coverage verifies explicit `thread/start` initialization, first-turn prompting, nullable reporting for an omitted selection, and retention of selections that are not currently runtime-eligible. ## Stack Stacked on #28685. This PR contains only the thread initialization and lifecycle/settings API layer.
This commit is contained in:
1 parent
fc8c6b7384
commit
7abfcf220b
46 files changed
+620
-36
No files matched your search
@@ -224,6 +224,7 @@ fn sample_thread_start_response(
|
|||||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||||
active_permission_profile: None,
|
active_permission_profile: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +289,7 @@ fn sample_thread_resume_response_with_source(
|
|||||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||||
active_permission_profile: None,
|
active_permission_profile: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
initial_turns_page: None,
|
initial_turns_page: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ fn sample_thread_start_response() -> ClientResponsePayload {
|
|||||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||||
active_permission_profile: None,
|
active_permission_profile: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +332,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload {
|
|||||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||||
active_permission_profile: None,
|
active_permission_profile: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
initial_turns_page: None,
|
initial_turns_page: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -349,6 +351,7 @@ fn sample_thread_fork_response() -> ClientResponsePayload {
|
|||||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||||
active_permission_profile: None,
|
active_permission_profile: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2611,6 +2611,14 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"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": {
|
"NetworkAccess": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"restricted",
|
"restricted",
|
||||||
|
|||||||
@@ -662,6 +662,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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": {
|
"NetworkAccess": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"restricted",
|
"restricted",
|
||||||
|
|||||||
@@ -662,6 +662,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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": {
|
"NetworkAccess": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"restricted",
|
"restricted",
|
||||||
|
|||||||
+8
@@ -108,6 +108,14 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"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": {
|
"NetworkAccess": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"restricted",
|
"restricted",
|
||||||
|
|||||||
@@ -194,6 +194,14 @@
|
|||||||
"LegacyAppPathString": {
|
"LegacyAppPathString": {
|
||||||
"type": "string"
|
"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"
|
||||||
|
},
|
||||||
"Personality": {
|
"Personality": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"none",
|
"none",
|
||||||
|
|||||||
@@ -662,6 +662,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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": {
|
"NetworkAccess": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"restricted",
|
"restricted",
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
|||||||
import type { AskForApproval } from "./AskForApproval";
|
import type { AskForApproval } from "./AskForApproval";
|
||||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||||
|
|
||||||
export type ThreadSettings = { cwd: AbsolutePathBuf, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, activePermissionProfile: ActivePermissionProfile | null, model: string, modelProvider: string, serviceTier: string | null, effort: ReasoningEffort | null, summary: ReasoningSummary | null, collaborationMode: CollaborationMode, personality: Personality | null, };
|
export type ThreadSettings = {cwd: AbsolutePathBuf, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, activePermissionProfile: ActivePermissionProfile | null, model: string, modelProvider: string, serviceTier: string | null, effort: ReasoningEffort | null, summary: ReasoningSummary | null, collaborationMode: CollaborationMode, personality: Personality | null};
|
||||||
@@ -2583,6 +2583,7 @@ mod tests {
|
|||||||
sandbox: v2::SandboxPolicy::DangerFullAccess,
|
sandbox: v2::SandboxPolicy::DangerFullAccess,
|
||||||
active_permission_profile: None,
|
active_permission_profile: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2630,7 +2631,8 @@ mod tests {
|
|||||||
"type": "dangerFullAccess"
|
"type": "dangerFullAccess"
|
||||||
},
|
},
|
||||||
"activePermissionProfile": null,
|
"activePermissionProfile": null,
|
||||||
"reasoningEffort": null
|
"reasoningEffort": null,
|
||||||
|
"multiAgentMode": null
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
serde_json::to_value(&response)?,
|
serde_json::to_value(&response)?,
|
||||||
@@ -3545,6 +3547,7 @@ mod tests {
|
|||||||
developer_instructions: None,
|
developer_instructions: None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
personality: None,
|
personality: None,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
|||||||
sandbox: SandboxPolicy::DangerFullAccess,
|
sandbox: SandboxPolicy::DangerFullAccess,
|
||||||
active_permission_profile: None,
|
active_permission_profile: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
initial_turns_page: Some(TurnsPage {
|
initial_turns_page: Some(TurnsPage {
|
||||||
data: Vec::new(),
|
data: Vec::new(),
|
||||||
next_cursor: Some("cursor_next".to_string()),
|
next_cursor: Some("cursor_next".to_string()),
|
||||||
@@ -3701,6 +3702,14 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
|
|||||||
assert_eq!(resume.active_permission_profile, None);
|
assert_eq!(resume.active_permission_profile, None);
|
||||||
assert_eq!(resume.initial_turns_page, None);
|
assert_eq!(resume.initial_turns_page, None);
|
||||||
assert_eq!(fork.active_permission_profile, None);
|
assert_eq!(fork.active_permission_profile, None);
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
start.multi_agent_mode,
|
||||||
|
resume.multi_agent_mode,
|
||||||
|
fork.multi_agent_mode,
|
||||||
|
),
|
||||||
|
(None, None, None)
|
||||||
|
);
|
||||||
|
|
||||||
let foreign_source: LegacyAppPathString =
|
let foreign_source: LegacyAppPathString =
|
||||||
serde_json::from_value(json!(r"C:\workspace\AGENTS.md")).expect("foreign source");
|
serde_json::from_value(json!(r"C:\workspace\AGENTS.md")).expect("foreign source");
|
||||||
@@ -3805,6 +3814,27 @@ fn turn_start_params_round_trip_multi_agent_mode() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn thread_start_params_round_trip_multi_agent_mode() {
|
||||||
|
let params: ThreadStartParams = serde_json::from_value(json!({
|
||||||
|
"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("thread/start.multiAgentMode")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(params).expect("params should serialize")["multiAgentMode"],
|
||||||
|
"proactive"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn thread_settings_update_params_preserve_explicit_null_service_tier() {
|
fn thread_settings_update_params_preserve_explicit_null_service_tier() {
|
||||||
let params: ThreadSettingsUpdateParams = serde_json::from_value(json!({
|
let params: ThreadSettingsUpdateParams = serde_json::from_value(json!({
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use codex_experimental_api_macros::ExperimentalApi;
|
|||||||
pub use codex_protocol::capabilities::CapabilityRootLocation;
|
pub use codex_protocol::capabilities::CapabilityRootLocation;
|
||||||
pub use codex_protocol::capabilities::SelectedCapabilityRoot;
|
pub use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||||
use codex_protocol::config_types::CollaborationMode;
|
use codex_protocol::config_types::CollaborationMode;
|
||||||
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
use codex_protocol::config_types::Personality;
|
use codex_protocol::config_types::Personality;
|
||||||
use codex_protocol::config_types::ReasoningSummary;
|
use codex_protocol::config_types::ReasoningSummary;
|
||||||
pub use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
|
pub use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
|
||||||
@@ -93,6 +94,12 @@ pub struct ThreadStartParams {
|
|||||||
pub developer_instructions: Option<String>,
|
pub developer_instructions: Option<String>,
|
||||||
#[ts(optional = nullable)]
|
#[ts(optional = nullable)]
|
||||||
pub personality: Option<Personality>,
|
pub personality: Option<Personality>,
|
||||||
|
/// Set the initial multi-agent mode for this thread.
|
||||||
|
/// Omitted leaves the thread without a selected mode. Eligible multi-agent
|
||||||
|
/// v2 turns still default to `explicitRequestOnly`.
|
||||||
|
#[experimental("thread/start.multiAgentMode")]
|
||||||
|
#[ts(optional = nullable)]
|
||||||
|
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||||
#[ts(optional = nullable)]
|
#[ts(optional = nullable)]
|
||||||
pub ephemeral: Option<bool>,
|
pub ephemeral: Option<bool>,
|
||||||
#[ts(optional = nullable)]
|
#[ts(optional = nullable)]
|
||||||
@@ -179,6 +186,10 @@ pub struct ThreadStartResponse {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||||
pub reasoning_effort: Option<ReasoningEffort>,
|
pub reasoning_effort: Option<ReasoningEffort>,
|
||||||
|
/// Current selected multi-agent mode for this thread, if one was selected.
|
||||||
|
#[experimental("thread/start.multiAgentMode")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThreadStartResponse {
|
impl ThreadStartResponse {
|
||||||
@@ -239,6 +250,10 @@ pub struct ThreadSettingsUpdateParams {
|
|||||||
#[experimental("thread/settings/update.collaborationMode")]
|
#[experimental("thread/settings/update.collaborationMode")]
|
||||||
#[ts(optional = nullable)]
|
#[ts(optional = nullable)]
|
||||||
pub collaboration_mode: Option<CollaborationMode>,
|
pub collaboration_mode: Option<CollaborationMode>,
|
||||||
|
/// Select the multi-agent mode for subsequent turns.
|
||||||
|
#[experimental("thread/settings/update.multiAgentMode")]
|
||||||
|
#[ts(optional = nullable)]
|
||||||
|
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||||
/// Override the personality for subsequent turns.
|
/// Override the personality for subsequent turns.
|
||||||
#[ts(optional = nullable)]
|
#[ts(optional = nullable)]
|
||||||
pub personality: Option<Personality>,
|
pub personality: Option<Personality>,
|
||||||
@@ -249,7 +264,7 @@ pub struct ThreadSettingsUpdateParams {
|
|||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
pub struct ThreadSettingsUpdateResponse {}
|
pub struct ThreadSettingsUpdateResponse {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
pub struct ThreadSettings {
|
pub struct ThreadSettings {
|
||||||
@@ -264,6 +279,10 @@ pub struct ThreadSettings {
|
|||||||
pub effort: Option<ReasoningEffort>,
|
pub effort: Option<ReasoningEffort>,
|
||||||
pub summary: Option<ReasoningSummary>,
|
pub summary: Option<ReasoningSummary>,
|
||||||
pub collaboration_mode: CollaborationMode,
|
pub collaboration_mode: CollaborationMode,
|
||||||
|
/// Current selected multi-agent mode for this thread, if one was selected.
|
||||||
|
#[experimental("thread/settings.multiAgentMode")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||||
pub personality: Option<Personality>,
|
pub personality: Option<Personality>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,6 +419,10 @@ pub struct ThreadResumeResponse {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||||
pub reasoning_effort: Option<ReasoningEffort>,
|
pub reasoning_effort: Option<ReasoningEffort>,
|
||||||
|
/// Current selected multi-agent mode for this thread, if one was selected.
|
||||||
|
#[experimental("thread/resume.multiAgentMode")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||||
/// `thread/turns/list` page returned when requested by `initialTurnsPage`.
|
/// `thread/turns/list` page returned when requested by `initialTurnsPage`.
|
||||||
#[experimental("thread/resume.initialTurnsPage")]
|
#[experimental("thread/resume.initialTurnsPage")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -555,6 +578,10 @@ pub struct ThreadForkResponse {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||||
pub reasoning_effort: Option<ReasoningEffort>,
|
pub reasoning_effort: Option<ReasoningEffort>,
|
||||||
|
/// Current selected multi-agent mode for this thread, if one was selected.
|
||||||
|
#[experimental("thread/fork.multiAgentMode")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThreadForkResponse {
|
impl ThreadForkResponse {
|
||||||
|
|||||||
@@ -137,17 +137,17 @@ Example with notification opt-out:
|
|||||||
|
|
||||||
## API Overview
|
## API Overview
|
||||||
|
|
||||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are also started in that environment; HTTP MCP declarations remain inactive.
|
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `multiAgentMode` selects the initial thread mode; omission leaves the selected mode unset, while eligible multi-agent v2 turns still default to `explicitRequestOnly`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are also started in that environment; HTTP MCP declarations remain inactive.
|
||||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
|
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. Multi-agent mode restores the last effective mode from rollout history when available; clients can select another mode on the first `turn/start`.
|
||||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
|
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
|
||||||
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
|
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their experimental `multiAgentMode` field, and the corresponding thread setting, report the thread's current selected mode or `null` when no mode was selected. Turn construction separately determines whether that mode is applicable to the selected model and runtime configuration.
|
||||||
- `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Experimental clients can use `parentThreadId` to filter direct spawned children represented by persisted spawn-edge state. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known.
|
- `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Experimental clients can use `parentThreadId` to filter direct spawned children represented by persisted spawn-edge state. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known.
|
||||||
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
||||||
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||||
- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
|
- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
|
||||||
- `thread/turns/items/list` — experimental; reserved for paging full items for one turn. The API shape is present, but app-server currently returns an unsupported-method JSON-RPC error.
|
- `thread/turns/items/list` — experimental; reserved for paging full items for one turn. The API shape is present, but app-server currently returns an unsupported-method JSON-RPC error.
|
||||||
- `thread/metadata/update` — patch stored thread metadata in sqlite; currently supports updating persisted `gitInfo` fields and returns the refreshed `thread`.
|
- `thread/metadata/update` — patch stored thread metadata in sqlite; currently supports updating persisted `gitInfo` fields and returns the refreshed `thread`.
|
||||||
- `thread/settings/update` — experimental; queue a partial update to a loaded thread’s next-turn settings without starting a turn or adding transcript items. Omitted fields leave settings unchanged; `serviceTier: null` clears the tier; `sandboxPolicy` and `permissions` cannot be combined. Returns `{}` when the update is accepted and emits `thread/settings/updated` with the full effective settings only if they actually change. `turn/start` settings overrides emit the same notification when they change the stored settings.
|
- `thread/settings/update` — experimental; queue a partial update to a loaded thread’s next-turn settings without starting a turn or adding transcript items. Omitted fields leave settings unchanged; `serviceTier: null` clears the tier; `multiAgentMode` selects `explicitRequestOnly` or `proactive` for subsequent turns; `sandboxPolicy` and `permissions` cannot be combined. Returns `{}` when the update is accepted and emits `thread/settings/updated` with the full effective settings only if they actually change. `turn/start` settings overrides emit the same notification when they change the stored settings.
|
||||||
- `thread/memoryMode/set` — experimental; set a thread’s persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success.
|
- `thread/memoryMode/set` — experimental; set a thread’s persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success.
|
||||||
- `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success.
|
- `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success.
|
||||||
- `thread/goal/set` — create or update the single persisted goal for a materialized thread; returns the current goal and emits `thread/goal/updated`.
|
- `thread/goal/set` — create or update the single persisted goal for a materialized thread; returns the current goal and emits `thread/goal/updated`.
|
||||||
@@ -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/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/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.
|
- `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". 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.
|
- `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 selected mode, including an unset mode. The requested mode is retained for the loaded session without rejecting unsupported configurations. Eligible multi-agent v2 turns default an unset mode to explicit-request-only, use the selected 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.
|
- `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/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"`.
|
- `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"`.
|
||||||
|
|||||||
@@ -639,6 +639,7 @@ pub(super) async fn handle_pending_thread_resume_request(
|
|||||||
active_permission_profile,
|
active_permission_profile,
|
||||||
workspace_roots,
|
workspace_roots,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
|
multi_agent_mode,
|
||||||
..
|
..
|
||||||
} = config_snapshot;
|
} = config_snapshot;
|
||||||
let instruction_sources = pending.instruction_sources;
|
let instruction_sources = pending.instruction_sources;
|
||||||
@@ -661,6 +662,7 @@ pub(super) async fn handle_pending_thread_resume_request(
|
|||||||
sandbox,
|
sandbox,
|
||||||
active_permission_profile,
|
active_permission_profile,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
|
multi_agent_mode,
|
||||||
initial_turns_page,
|
initial_turns_page,
|
||||||
};
|
};
|
||||||
outgoing.send_response(request_id, response).await;
|
outgoing.send_response(request_id, response).await;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::*;
|
|||||||
use crate::error_code::method_not_found;
|
use crate::error_code::method_not_found;
|
||||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||||
use codex_extension_api::ExtensionDataInit;
|
use codex_extension_api::ExtensionDataInit;
|
||||||
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||||
|
|
||||||
@@ -902,6 +903,7 @@ impl ThreadRequestProcessor {
|
|||||||
mock_experimental_field: _mock_experimental_field,
|
mock_experimental_field: _mock_experimental_field,
|
||||||
experimental_raw_events,
|
experimental_raw_events,
|
||||||
personality,
|
personality,
|
||||||
|
multi_agent_mode,
|
||||||
ephemeral,
|
ephemeral,
|
||||||
session_start_source,
|
session_start_source,
|
||||||
thread_source,
|
thread_source,
|
||||||
@@ -955,6 +957,7 @@ impl ThreadRequestProcessor {
|
|||||||
supports_openai_form_elicitation,
|
supports_openai_form_elicitation,
|
||||||
config,
|
config,
|
||||||
typesafe_overrides,
|
typesafe_overrides,
|
||||||
|
multi_agent_mode,
|
||||||
dynamic_tools,
|
dynamic_tools,
|
||||||
selected_capability_roots.unwrap_or_default(),
|
selected_capability_roots.unwrap_or_default(),
|
||||||
session_start_source,
|
session_start_source,
|
||||||
@@ -1029,6 +1032,7 @@ impl ThreadRequestProcessor {
|
|||||||
supports_openai_form_elicitation: bool,
|
supports_openai_form_elicitation: bool,
|
||||||
config_overrides: Option<HashMap<String, serde_json::Value>>,
|
config_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||||
typesafe_overrides: ConfigOverrides,
|
typesafe_overrides: ConfigOverrides,
|
||||||
|
multi_agent_mode: Option<MultiAgentMode>,
|
||||||
dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||||
selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
||||||
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
|
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
|
||||||
@@ -1152,6 +1156,7 @@ impl ThreadRequestProcessor {
|
|||||||
thread_source,
|
thread_source,
|
||||||
dynamic_tools,
|
dynamic_tools,
|
||||||
metrics_service_name: service_name,
|
metrics_service_name: service_name,
|
||||||
|
multi_agent_mode,
|
||||||
parent_trace: request_trace,
|
parent_trace: request_trace,
|
||||||
environments,
|
environments,
|
||||||
thread_extension_init,
|
thread_extension_init,
|
||||||
@@ -1257,6 +1262,7 @@ impl ThreadRequestProcessor {
|
|||||||
sandbox,
|
sandbox,
|
||||||
active_permission_profile,
|
active_permission_profile,
|
||||||
reasoning_effort: config_snapshot.reasoning_effort,
|
reasoning_effort: config_snapshot.reasoning_effort,
|
||||||
|
multi_agent_mode: config_snapshot.multi_agent_mode,
|
||||||
};
|
};
|
||||||
let notif = thread_started_notification(thread);
|
let notif = thread_started_notification(thread);
|
||||||
listener_task_context
|
listener_task_context
|
||||||
@@ -2787,6 +2793,7 @@ impl ThreadRequestProcessor {
|
|||||||
sandbox,
|
sandbox,
|
||||||
active_permission_profile,
|
active_permission_profile,
|
||||||
reasoning_effort: session_configured.reasoning_effort,
|
reasoning_effort: session_configured.reasoning_effort,
|
||||||
|
multi_agent_mode: config_snapshot.multi_agent_mode,
|
||||||
initial_turns_page,
|
initial_turns_page,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3508,6 +3515,7 @@ impl ThreadRequestProcessor {
|
|||||||
sandbox,
|
sandbox,
|
||||||
active_permission_profile,
|
active_permission_profile,
|
||||||
reasoning_effort: session_configured.reasoning_effort,
|
reasoning_effort: session_configured.reasoning_effort,
|
||||||
|
multi_agent_mode: config_snapshot.multi_agent_mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
let notif = thread_started_notification(thread);
|
let notif = thread_started_notification(thread);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -206,6 +205,7 @@ pub(crate) fn thread_settings_from_config_snapshot(
|
|||||||
effort: config_snapshot.reasoning_effort.clone(),
|
effort: config_snapshot.reasoning_effort.clone(),
|
||||||
summary: config_snapshot.reasoning_summary,
|
summary: config_snapshot.reasoning_summary,
|
||||||
collaboration_mode: config_snapshot.collaboration_mode.clone(),
|
collaboration_mode: config_snapshot.collaboration_mode.clone(),
|
||||||
|
multi_agent_mode: config_snapshot.multi_agent_mode,
|
||||||
personality: config_snapshot.personality,
|
personality: config_snapshot.personality,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,6 +226,7 @@ pub(crate) fn thread_settings_from_core_snapshot(
|
|||||||
reasoning_summary,
|
reasoning_summary,
|
||||||
personality,
|
personality,
|
||||||
collaboration_mode,
|
collaboration_mode,
|
||||||
|
multi_agent_mode,
|
||||||
} = snapshot;
|
} = snapshot;
|
||||||
let sandbox_policy = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
|
let sandbox_policy = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
|
||||||
ThreadSettings {
|
ThreadSettings {
|
||||||
@@ -242,6 +243,7 @@ pub(crate) fn thread_settings_from_core_snapshot(
|
|||||||
effort: reasoning_effort,
|
effort: reasoning_effort,
|
||||||
summary: reasoning_summary,
|
summary: reasoning_summary,
|
||||||
collaboration_mode,
|
collaboration_mode,
|
||||||
|
multi_agent_mode,
|
||||||
personality,
|
personality,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -729,7 +729,7 @@ impl TurnRequestProcessor {
|
|||||||
effort: params.effort,
|
effort: params.effort,
|
||||||
summary: params.summary,
|
summary: params.summary,
|
||||||
collaboration_mode: params.collaboration_mode,
|
collaboration_mode: params.collaboration_mode,
|
||||||
multi_agent_mode: None,
|
multi_agent_mode: params.multi_agent_mode,
|
||||||
personality: params.personality,
|
personality: params.personality,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ mod tests {
|
|||||||
developer_instructions: None,
|
developer_instructions: None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
personality: None,
|
personality: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -789,6 +789,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
|
|||||||
base_instructions: None,
|
base_instructions: None,
|
||||||
developer_instructions: None,
|
developer_instructions: None,
|
||||||
personality: None,
|
personality: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
ephemeral: None,
|
ephemeral: None,
|
||||||
session_start_source: None,
|
session_start_source: None,
|
||||||
thread_source: None,
|
thread_source: None,
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ use codex_app_server_protocol::TurnStartParams;
|
|||||||
use codex_app_server_protocol::TurnStartResponse;
|
use codex_app_server_protocol::TurnStartResponse;
|
||||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||||
use codex_core::test_support::all_model_presets;
|
use codex_core::test_support::all_model_presets;
|
||||||
|
use codex_features::Feature;
|
||||||
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||||
|
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
||||||
use core_test_support::responses;
|
use core_test_support::responses;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -94,6 +97,112 @@ async fn thread_settings_update_emits_notification_and_updates_future_turns() ->
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_settings_update_multi_agent_mode_applies_to_future_turns() -> Result<()> {
|
||||||
|
let server = responses::start_mock_server().await;
|
||||||
|
let response_mock = responses::mount_sse_sequence(
|
||||||
|
&server,
|
||||||
|
(1..=2)
|
||||||
|
.map(|index| {
|
||||||
|
responses::sse(vec![
|
||||||
|
responses::ev_response_created(&format!("resp-{index}")),
|
||||||
|
responses::ev_assistant_message(&format!("msg-{index}"), "done"),
|
||||||
|
responses::ev_completed(&format!("resp-{index}")),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
write_mock_responses_config_toml(
|
||||||
|
codex_home.path(),
|
||||||
|
&server.uri(),
|
||||||
|
&BTreeMap::from([
|
||||||
|
(Feature::MultiAgentV2, true),
|
||||||
|
(Feature::MultiAgentMode, true),
|
||||||
|
]),
|
||||||
|
/*auto_compact_limit*/ 200_000,
|
||||||
|
/*requires_openai_auth*/ None,
|
||||||
|
"mock_provider",
|
||||||
|
"compact",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||||
|
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
let thread = start_thread(&mut mcp).await?.thread;
|
||||||
|
|
||||||
|
start_text_turn(&mut mcp, thread.id.clone()).await?;
|
||||||
|
timeout(
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
|
mcp.read_stream_until_notification_message("turn/completed"),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
assert_eq!(response_mock.requests().len(), 1);
|
||||||
|
|
||||||
|
send_thread_settings_update(
|
||||||
|
&mut mcp,
|
||||||
|
ThreadSettingsUpdateParams {
|
||||||
|
thread_id: thread.id.clone(),
|
||||||
|
multi_agent_mode: Some(MultiAgentMode::Proactive),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
response_mock.requests().len(),
|
||||||
|
1,
|
||||||
|
"settings-only update should not start a model request"
|
||||||
|
);
|
||||||
|
|
||||||
|
let updated = read_thread_settings_updated(&mut mcp).await?;
|
||||||
|
assert_eq!(updated.thread_id, thread.id);
|
||||||
|
assert_eq!(
|
||||||
|
updated.thread_settings.multi_agent_mode,
|
||||||
|
Some(MultiAgentMode::Proactive)
|
||||||
|
);
|
||||||
|
|
||||||
|
start_text_turn(&mut mcp, thread.id).await?;
|
||||||
|
timeout(
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
|
mcp.read_stream_until_notification_message("turn/completed"),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let requests = response_mock.requests();
|
||||||
|
let first_developer_texts = requests[0].message_input_texts("developer");
|
||||||
|
let second_developer_texts = requests[1].message_input_texts("developer");
|
||||||
|
assert_eq!(
|
||||||
|
first_developer_texts
|
||||||
|
.iter()
|
||||||
|
.filter(|text| text.contains(MULTI_AGENT_MODE_OPEN_TAG))
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
second_developer_texts
|
||||||
|
.iter()
|
||||||
|
.filter(|text| text.contains(MULTI_AGENT_MODE_OPEN_TAG))
|
||||||
|
.count(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
second_developer_texts
|
||||||
|
.iter()
|
||||||
|
.filter(|text| text.contains("Proactive multi-agent delegation is active."))
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
second_developer_texts
|
||||||
|
.iter()
|
||||||
|
.filter(|text| text
|
||||||
|
.contains("Do not spawn sub-agents unless the user explicitly asks for sub-agents"))
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn thread_settings_update_cwd_retargets_default_environment() -> Result<()> {
|
async fn thread_settings_update_cwd_retargets_default_environment() -> Result<()> {
|
||||||
let server = responses::start_mock_server().await;
|
let server = responses::start_mock_server().await;
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ use codex_protocol::config_types::Settings;
|
|||||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||||
use codex_protocol::models::ImageDetail;
|
use codex_protocol::models::ImageDetail;
|
||||||
use codex_protocol::openai_models::ReasoningEffort;
|
use codex_protocol::openai_models::ReasoningEffort;
|
||||||
|
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
||||||
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||||
use codex_utils_absolute_path::test_support::PathExt;
|
use codex_utils_absolute_path::test_support::PathExt;
|
||||||
use core_test_support::responses;
|
use core_test_support::responses;
|
||||||
@@ -1827,6 +1828,140 @@ async fn turn_start_accepts_multi_agent_mode_v2() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_start_multi_agent_mode_initializes_first_turn() -> 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()),
|
||||||
|
multi_agent_mode: Some(MultiAgentMode::Proactive),
|
||||||
|
..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,
|
||||||
|
multi_agent_mode,
|
||||||
|
..
|
||||||
|
} = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||||
|
assert_eq!(multi_agent_mode, Some(MultiAgentMode::Proactive));
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}],
|
||||||
|
..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_OPEN_TAG)
|
||||||
|
&& text.contains("Proactive multi-agent delegation is active.")
|
||||||
|
}),
|
||||||
|
"expected proactive multi-agent mode instructions in developer input, got {developer_texts:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_start_reports_selected_multi_agent_mode() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
BTreeMap::from([(Feature::MultiAgentV2, true)]),
|
||||||
|
Some(MultiAgentMode::Proactive),
|
||||||
|
Some(MultiAgentMode::Proactive),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
BTreeMap::new(),
|
||||||
|
Some(MultiAgentMode::Proactive),
|
||||||
|
Some(MultiAgentMode::Proactive),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
BTreeMap::from([
|
||||||
|
(Feature::MultiAgentV2, true),
|
||||||
|
(Feature::MultiAgentMode, true),
|
||||||
|
]),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (features, requested_multi_agent_mode, expected_multi_agent_mode) in cases {
|
||||||
|
let server = responses::start_mock_server().await;
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
create_config_toml(codex_home.path(), &server.uri(), "never", &features)?;
|
||||||
|
|
||||||
|
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()),
|
||||||
|
multi_agent_mode: requested_multi_agent_mode,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let thread_resp: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let response = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||||
|
|
||||||
|
assert_eq!(response.multi_agent_mode, expected_multi_agent_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn turn_start_change_personality_mid_thread_v2() -> Result<()> {
|
async fn turn_start_change_personality_mid_thread_v2() -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use crate::thread_rollout_truncation::truncate_rollout_to_last_n_fork_turns;
|
|||||||
use codex_protocol::AgentPath;
|
use codex_protocol::AgentPath;
|
||||||
use codex_protocol::SessionId;
|
use codex_protocol::SessionId;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
use codex_protocol::error::CodexErr;
|
use codex_protocol::error::CodexErr;
|
||||||
use codex_protocol::error::Result as CodexResult;
|
use codex_protocol::error::Result as CodexResult;
|
||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
@@ -68,6 +69,7 @@ pub(crate) struct SpawnAgentOptions {
|
|||||||
pub(crate) fork_mode: Option<SpawnAgentForkMode>,
|
pub(crate) fork_mode: Option<SpawnAgentForkMode>,
|
||||||
pub(crate) parent_thread_id: Option<ThreadId>,
|
pub(crate) parent_thread_id: Option<ThreadId>,
|
||||||
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
|
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||||
|
pub(crate) initial_multi_agent_mode: Option<MultiAgentMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ async fn spawn_v2_subagent(
|
|||||||
/*forked_from_thread_id*/ None,
|
/*forked_from_thread_id*/ None,
|
||||||
Some(ThreadSource::Subagent),
|
Some(ThreadSource::Subagent),
|
||||||
/*metrics_service_name*/ None,
|
/*metrics_service_name*/ None,
|
||||||
|
/*initial_multi_agent_mode*/ None,
|
||||||
/*inherited_environments*/ None,
|
/*inherited_environments*/ None,
|
||||||
/*inherited_exec_policy*/ None,
|
/*inherited_exec_policy*/ None,
|
||||||
/*environments*/ None,
|
/*environments*/ None,
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use super::residency::is_v2_resident_session_source;
|
use super::residency::is_v2_resident_session_source;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
|
|
||||||
const AGENT_NAMES: &str = include_str!("../agent_names.txt");
|
const AGENT_NAMES: &str = include_str!("../agent_names.txt");
|
||||||
|
|
||||||
struct SpawnAgentThreadInheritance {
|
struct SpawnAgentThreadInheritance {
|
||||||
environments: Option<TurnEnvironmentSnapshot>,
|
environments: Option<TurnEnvironmentSnapshot>,
|
||||||
exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||||
|
inherited_multi_agent_mode: Option<MultiAgentMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_agent_nickname_list() -> Vec<&'static str> {
|
fn default_agent_nickname_list() -> Vec<&'static str> {
|
||||||
@@ -237,6 +239,7 @@ impl AgentControl {
|
|||||||
exec_policy: self
|
exec_policy: self
|
||||||
.inherited_exec_policy_for_source(&state, session_source.as_ref(), &config)
|
.inherited_exec_policy_for_source(&state, session_source.as_ref(), &config)
|
||||||
.await,
|
.await,
|
||||||
|
inherited_multi_agent_mode: options.initial_multi_agent_mode,
|
||||||
};
|
};
|
||||||
let (session_source, mut agent_metadata) = match session_source {
|
let (session_source, mut agent_metadata) = match session_source {
|
||||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||||
@@ -283,6 +286,7 @@ impl AgentControl {
|
|||||||
/*forked_from_thread_id*/ None,
|
/*forked_from_thread_id*/ None,
|
||||||
/*thread_source*/ Some(ThreadSource::Subagent),
|
/*thread_source*/ Some(ThreadSource::Subagent),
|
||||||
/*metrics_service_name*/ None,
|
/*metrics_service_name*/ None,
|
||||||
|
inheritance.inherited_multi_agent_mode,
|
||||||
inheritance.environments,
|
inheritance.environments,
|
||||||
inheritance.exec_policy,
|
inheritance.exec_policy,
|
||||||
options.environments.clone(),
|
options.environments.clone(),
|
||||||
@@ -388,6 +392,7 @@ impl AgentControl {
|
|||||||
let SpawnAgentThreadInheritance {
|
let SpawnAgentThreadInheritance {
|
||||||
environments: inherited_environments,
|
environments: inherited_environments,
|
||||||
exec_policy: inherited_exec_policy,
|
exec_policy: inherited_exec_policy,
|
||||||
|
inherited_multi_agent_mode,
|
||||||
} = inheritance;
|
} = inheritance;
|
||||||
if options.fork_parent_spawn_call_id.is_none() {
|
if options.fork_parent_spawn_call_id.is_none() {
|
||||||
return Err(CodexErr::Fatal(
|
return Err(CodexErr::Fatal(
|
||||||
@@ -513,6 +518,7 @@ impl AgentControl {
|
|||||||
/*thread_source*/ Some(ThreadSource::Subagent),
|
/*thread_source*/ Some(ThreadSource::Subagent),
|
||||||
/*parent_thread_id*/ Some(parent_thread_id),
|
/*parent_thread_id*/ Some(parent_thread_id),
|
||||||
/*forked_from_thread_id*/ Some(parent_thread_id),
|
/*forked_from_thread_id*/ Some(parent_thread_id),
|
||||||
|
inherited_multi_agent_mode,
|
||||||
inherited_environments,
|
inherited_environments,
|
||||||
inherited_exec_policy,
|
inherited_exec_policy,
|
||||||
options.environments.clone(),
|
options.environments.clone(),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use codex_features::Feature;
|
|||||||
use codex_login::CodexAuth;
|
use codex_login::CodexAuth;
|
||||||
use codex_protocol::AgentPath;
|
use codex_protocol::AgentPath;
|
||||||
use codex_protocol::config_types::ModeKind;
|
use codex_protocol::config_types::ModeKind;
|
||||||
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::MessagePhase;
|
use codex_protocol::models::MessagePhase;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
@@ -816,6 +817,45 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
|
|||||||
assert_eq!(captured, Some(expected));
|
assert_eq!(captured, Some(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn spawn_thread_subagent_uses_supplied_initial_multi_agent_mode_without_history() {
|
||||||
|
let harness = AgentControlHarness::new().await;
|
||||||
|
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
|
||||||
|
|
||||||
|
let child_thread_id = harness
|
||||||
|
.control
|
||||||
|
.spawn_agent_with_metadata(
|
||||||
|
harness.config.clone(),
|
||||||
|
text_input("child task"),
|
||||||
|
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||||
|
parent_thread_id,
|
||||||
|
depth: 1,
|
||||||
|
agent_path: None,
|
||||||
|
agent_nickname: None,
|
||||||
|
agent_role: None,
|
||||||
|
})),
|
||||||
|
SpawnAgentOptions {
|
||||||
|
initial_multi_agent_mode: Some(MultiAgentMode::Proactive),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("spawn child without parent history")
|
||||||
|
.thread_id;
|
||||||
|
let child_snapshot = harness
|
||||||
|
.manager
|
||||||
|
.get_thread(child_thread_id)
|
||||||
|
.await
|
||||||
|
.expect("child thread should be registered")
|
||||||
|
.config_snapshot()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
child_snapshot.multi_agent_mode,
|
||||||
|
Some(MultiAgentMode::Proactive)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||||
let harness = AgentControlHarness::new().await;
|
let harness = AgentControlHarness::new().await;
|
||||||
@@ -838,6 +878,15 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
|||||||
.expect("start parent thread");
|
.expect("start parent thread");
|
||||||
let parent_thread_id = new_thread.thread_id;
|
let parent_thread_id = new_thread.thread_id;
|
||||||
let parent_thread = new_thread.thread;
|
let parent_thread = new_thread.thread;
|
||||||
|
parent_thread
|
||||||
|
.codex
|
||||||
|
.session
|
||||||
|
.update_settings(crate::session::SessionSettingsUpdate {
|
||||||
|
multi_agent_mode: Some(MultiAgentMode::Proactive),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("update parent multi-agent mode");
|
||||||
parent_thread
|
parent_thread
|
||||||
.inject_user_message_without_turn("parent seed context".to_string())
|
.inject_user_message_without_turn("parent seed context".to_string())
|
||||||
.await;
|
.await;
|
||||||
@@ -923,6 +972,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
|||||||
SpawnAgentOptions {
|
SpawnAgentOptions {
|
||||||
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
|
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
|
||||||
fork_mode: Some(SpawnAgentForkMode::FullHistory),
|
fork_mode: Some(SpawnAgentForkMode::FullHistory),
|
||||||
|
initial_multi_agent_mode: Some(MultiAgentMode::Proactive),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -935,6 +985,10 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
|||||||
.get_thread(child_thread_id)
|
.get_thread(child_thread_id)
|
||||||
.await
|
.await
|
||||||
.expect("child thread should be registered");
|
.expect("child thread should be registered");
|
||||||
|
assert_eq!(
|
||||||
|
child_thread.config_snapshot().await.multi_agent_mode,
|
||||||
|
Some(MultiAgentMode::Proactive)
|
||||||
|
);
|
||||||
assert_ne!(child_thread_id, parent_thread_id);
|
assert_ne!(child_thread_id, parent_thread_id);
|
||||||
let history = child_thread.codex.session.clone_history().await;
|
let history = child_thread.codex.session.clone_history().await;
|
||||||
let expected_history = [
|
let expected_history = [
|
||||||
@@ -1242,6 +1296,15 @@ async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() {
|
|||||||
async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
|
async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
|
||||||
let harness = AgentControlHarness::new().await;
|
let harness = AgentControlHarness::new().await;
|
||||||
let (parent_thread_id, parent_thread) = harness.start_thread().await;
|
let (parent_thread_id, parent_thread) = harness.start_thread().await;
|
||||||
|
parent_thread
|
||||||
|
.codex
|
||||||
|
.session
|
||||||
|
.update_settings(crate::session::SessionSettingsUpdate {
|
||||||
|
multi_agent_mode: Some(MultiAgentMode::Proactive),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("update parent multi-agent mode");
|
||||||
|
|
||||||
parent_thread
|
parent_thread
|
||||||
.inject_user_message_without_turn("old parent context".to_string())
|
.inject_user_message_without_turn("old parent context".to_string())
|
||||||
@@ -1326,6 +1389,7 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
|
|||||||
SpawnAgentOptions {
|
SpawnAgentOptions {
|
||||||
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
|
fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()),
|
||||||
fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)),
|
fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)),
|
||||||
|
initial_multi_agent_mode: Some(MultiAgentMode::Proactive),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1338,6 +1402,10 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() {
|
|||||||
.get_thread(child_thread_id)
|
.get_thread(child_thread_id)
|
||||||
.await
|
.await
|
||||||
.expect("child thread should be registered");
|
.expect("child thread should be registered");
|
||||||
|
assert_eq!(
|
||||||
|
child_thread.config_snapshot().await.multi_agent_mode,
|
||||||
|
Some(MultiAgentMode::Proactive)
|
||||||
|
);
|
||||||
let history = child_thread.codex.session.clone_history().await;
|
let history = child_thread.codex.session.clone_history().await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -2185,7 +2253,7 @@ async fn spawn_thread_subagent_uses_role_specific_nickname_candidates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
async fn resume_thread_subagent_restores_stored_metadata_and_effective_multi_agent_mode() {
|
||||||
let (home, mut config) = test_config().await;
|
let (home, mut config) = test_config().await;
|
||||||
config
|
config
|
||||||
.features
|
.features
|
||||||
@@ -2207,7 +2275,7 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
|||||||
manager,
|
manager,
|
||||||
control,
|
control,
|
||||||
};
|
};
|
||||||
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
|
let (parent_thread_id, parent_thread) = harness.start_thread().await;
|
||||||
let agent_path = AgentPath::from_string("/root/explorer".to_string())
|
let agent_path = AgentPath::from_string("/root/explorer".to_string())
|
||||||
.expect("test agent path should be valid");
|
.expect("test agent path should be valid");
|
||||||
|
|
||||||
@@ -2232,6 +2300,38 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
|||||||
.get_thread(child_thread_id)
|
.get_thread(child_thread_id)
|
||||||
.await
|
.await
|
||||||
.expect("child thread should exist");
|
.expect("child thread should exist");
|
||||||
|
let mut child_turn_context = child_thread
|
||||||
|
.codex
|
||||||
|
.session
|
||||||
|
.new_default_turn()
|
||||||
|
.await
|
||||||
|
.to_turn_context_item();
|
||||||
|
child_turn_context.multi_agent_mode = Some(MultiAgentMode::Proactive);
|
||||||
|
child_thread
|
||||||
|
.codex
|
||||||
|
.session
|
||||||
|
.persist_rollout_items(&[RolloutItem::TurnContext(child_turn_context)])
|
||||||
|
.await;
|
||||||
|
child_thread
|
||||||
|
.codex
|
||||||
|
.session
|
||||||
|
.ensure_rollout_materialized()
|
||||||
|
.await;
|
||||||
|
child_thread
|
||||||
|
.codex
|
||||||
|
.session
|
||||||
|
.flush_rollout()
|
||||||
|
.await
|
||||||
|
.expect("flush child effective multi-agent mode");
|
||||||
|
parent_thread
|
||||||
|
.codex
|
||||||
|
.session
|
||||||
|
.update_settings(crate::session::SessionSettingsUpdate {
|
||||||
|
multi_agent_mode: Some(MultiAgentMode::ExplicitRequestOnly),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("change parent multi-agent mode before child resume");
|
||||||
let mut status_rx = harness
|
let mut status_rx = harness
|
||||||
.control
|
.control
|
||||||
.subscribe_status(child_thread_id)
|
.subscribe_status(child_thread_id)
|
||||||
@@ -2320,6 +2420,10 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
|||||||
assert_eq!(resumed_agent_path, Some(agent_path));
|
assert_eq!(resumed_agent_path, Some(agent_path));
|
||||||
assert_eq!(resumed_nickname, Some(original_nickname));
|
assert_eq!(resumed_nickname, Some(original_nickname));
|
||||||
assert_eq!(resumed_role, Some("explorer".to_string()));
|
assert_eq!(resumed_role, Some("explorer".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
resumed_snapshot.multi_agent_mode,
|
||||||
|
Some(MultiAgentMode::Proactive)
|
||||||
|
);
|
||||||
|
|
||||||
let _ = harness
|
let _ = harness
|
||||||
.control
|
.control
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
|||||||
attestation_provider: parent_session.services.attestation_provider.clone(),
|
attestation_provider: parent_session.services.attestation_provider.clone(),
|
||||||
external_time_provider: Some(Arc::clone(&parent_session.services.time_provider)),
|
external_time_provider: Some(Arc::clone(&parent_session.services.time_provider)),
|
||||||
inherited_multi_agent_version: Some(MultiAgentVersion::Disabled),
|
inherited_multi_agent_version: Some(MultiAgentVersion::Disabled),
|
||||||
|
initial_multi_agent_mode: None,
|
||||||
}))
|
}))
|
||||||
.or_cancel(&cancel_token)
|
.or_cancel(&cancel_token)
|
||||||
.await??;
|
.await??;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ fn build_multi_agent_mode_update_item(
|
|||||||
&next.config.multi_agent_v2,
|
&next.config.multi_agent_v2,
|
||||||
&next.session_source,
|
&next.session_source,
|
||||||
next.multi_agent_mode,
|
next.multi_agent_mode,
|
||||||
next.features.enabled(Feature::MultiAgentMode),
|
next.config.features.enabled(Feature::MultiAgentMode),
|
||||||
);
|
);
|
||||||
let previous = previous?;
|
let previous = previous?;
|
||||||
if previous.multi_agent_mode == effective_multi_agent_mode {
|
if previous.multi_agent_mode == effective_multi_agent_mode {
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
|
|||||||
reasoning_summary: snapshot.reasoning_summary,
|
reasoning_summary: snapshot.reasoning_summary,
|
||||||
personality: snapshot.personality,
|
personality: snapshot.personality,
|
||||||
collaboration_mode: snapshot.collaboration_mode,
|
collaboration_mode: snapshot.collaboration_mode,
|
||||||
|
multi_agent_mode: snapshot.multi_agent_mode,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,6 +445,7 @@ pub(crate) struct CodexSpawnArgs {
|
|||||||
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||||
pub(crate) external_time_provider: Option<Arc<dyn TimeProvider>>,
|
pub(crate) external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||||
pub(crate) inherited_multi_agent_version: Option<MultiAgentVersion>,
|
pub(crate) inherited_multi_agent_version: Option<MultiAgentVersion>,
|
||||||
|
pub(crate) initial_multi_agent_mode: Option<MultiAgentMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_multi_agent_version(
|
pub(crate) fn resolve_multi_agent_version(
|
||||||
@@ -529,6 +530,7 @@ impl Codex {
|
|||||||
attestation_provider,
|
attestation_provider,
|
||||||
external_time_provider,
|
external_time_provider,
|
||||||
inherited_multi_agent_version,
|
inherited_multi_agent_version,
|
||||||
|
initial_multi_agent_mode,
|
||||||
} = args;
|
} = args;
|
||||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||||
let (tx_event, rx_event) = async_channel::unbounded();
|
let (tx_event, rx_event) = async_channel::unbounded();
|
||||||
@@ -583,7 +585,7 @@ impl Codex {
|
|||||||
.await;
|
.await;
|
||||||
let multi_agent_version =
|
let multi_agent_version =
|
||||||
resolve_multi_agent_version(&conversation_history, inherited_multi_agent_version);
|
resolve_multi_agent_version(&conversation_history, inherited_multi_agent_version);
|
||||||
let multi_agent_mode = conversation_history.get_multi_agent_mode();
|
let multi_agent_mode = initial_multi_agent_mode;
|
||||||
config
|
config
|
||||||
.validate_multi_agent_v2_config()
|
.validate_multi_agent_v2_config()
|
||||||
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
||||||
@@ -3249,7 +3251,10 @@ impl Session {
|
|||||||
&turn_context.config.multi_agent_v2,
|
&turn_context.config.multi_agent_v2,
|
||||||
&session_source,
|
&session_source,
|
||||||
turn_context.multi_agent_mode,
|
turn_context.multi_agent_mode,
|
||||||
turn_context.features.enabled(Feature::MultiAgentMode),
|
turn_context
|
||||||
|
.config
|
||||||
|
.features
|
||||||
|
.enabled(Feature::MultiAgentMode),
|
||||||
) {
|
) {
|
||||||
items.push(ContextualUserFragment::into(
|
items.push(ContextualUserFragment::into(
|
||||||
MultiAgentModeInstructions::new(multi_agent_mode),
|
MultiAgentModeInstructions::new(multi_agent_mode),
|
||||||
|
|||||||
@@ -738,6 +738,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
|||||||
attestation_provider: None,
|
attestation_provider: None,
|
||||||
external_time_provider: None,
|
external_time_provider: None,
|
||||||
inherited_multi_agent_version: None,
|
inherited_multi_agent_version: None,
|
||||||
|
initial_multi_agent_mode: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("spawn guardian subagent");
|
.expect("spawn guardian subagent");
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ impl TurnContext {
|
|||||||
&self.config.multi_agent_v2,
|
&self.config.multi_agent_v2,
|
||||||
&self.session_source,
|
&self.session_source,
|
||||||
self.multi_agent_mode,
|
self.multi_agent_mode,
|
||||||
self.features.enabled(Feature::MultiAgentMode),
|
self.config.features.enabled(Feature::MultiAgentMode),
|
||||||
),
|
),
|
||||||
realtime_active: Some(self.realtime_active),
|
realtime_active: Some(self.realtime_active),
|
||||||
effort: self.reasoning_effort.clone(),
|
effort: self.reasoning_effort.clone(),
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ use codex_models_manager::manager::RefreshStrategy;
|
|||||||
use codex_models_manager::manager::SharedModelsManager;
|
use codex_models_manager::manager::SharedModelsManager;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::config_types::CollaborationModeMask;
|
use codex_protocol::config_types::CollaborationModeMask;
|
||||||
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
use codex_protocol::error::CodexErr;
|
use codex_protocol::error::CodexErr;
|
||||||
use codex_protocol::error::Result as CodexResult;
|
use codex_protocol::error::Result as CodexResult;
|
||||||
use codex_protocol::openai_models::ModelPreset;
|
use codex_protocol::openai_models::ModelPreset;
|
||||||
@@ -184,6 +185,7 @@ pub struct StartThreadOptions {
|
|||||||
pub thread_source: Option<ThreadSource>,
|
pub thread_source: Option<ThreadSource>,
|
||||||
pub dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
pub dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
||||||
pub metrics_service_name: Option<String>,
|
pub metrics_service_name: Option<String>,
|
||||||
|
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||||
pub parent_trace: Option<W3cTraceContext>,
|
pub parent_trace: Option<W3cTraceContext>,
|
||||||
pub environments: Vec<TurnEnvironmentSelection>,
|
pub environments: Vec<TurnEnvironmentSelection>,
|
||||||
pub thread_extension_init: ExtensionDataInit,
|
pub thread_extension_init: ExtensionDataInit,
|
||||||
@@ -607,6 +609,7 @@ impl ThreadManager {
|
|||||||
thread_source: None,
|
thread_source: None,
|
||||||
dynamic_tools,
|
dynamic_tools,
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments,
|
environments,
|
||||||
thread_extension_init: ExtensionDataInit::default(),
|
thread_extension_init: ExtensionDataInit::default(),
|
||||||
@@ -646,6 +649,7 @@ impl ThreadManager {
|
|||||||
thread_source,
|
thread_source,
|
||||||
options.dynamic_tools,
|
options.dynamic_tools,
|
||||||
options.metrics_service_name,
|
options.metrics_service_name,
|
||||||
|
options.multi_agent_mode,
|
||||||
/*inherited_environments*/ None,
|
/*inherited_environments*/ None,
|
||||||
/*inherited_exec_policy*/ None,
|
/*inherited_exec_policy*/ None,
|
||||||
options.parent_trace,
|
options.parent_trace,
|
||||||
@@ -730,6 +734,7 @@ impl ThreadManager {
|
|||||||
let (session_source, thread_source) = initial_history
|
let (session_source, thread_source) = initial_history
|
||||||
.get_resumed_session_sources()
|
.get_resumed_session_sources()
|
||||||
.unwrap_or_else(|| (self.state.session_source.clone(), None));
|
.unwrap_or_else(|| (self.state.session_source.clone(), None));
|
||||||
|
let initial_multi_agent_mode = initial_history.get_latest_effective_multi_agent_mode();
|
||||||
Box::pin(self.state.spawn_thread_with_source(
|
Box::pin(self.state.spawn_thread_with_source(
|
||||||
config,
|
config,
|
||||||
initial_history,
|
initial_history,
|
||||||
@@ -741,6 +746,7 @@ impl ThreadManager {
|
|||||||
thread_source,
|
thread_source,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
/*metrics_service_name*/ None,
|
/*metrics_service_name*/ None,
|
||||||
|
initial_multi_agent_mode,
|
||||||
/*inherited_environments*/ None,
|
/*inherited_environments*/ None,
|
||||||
/*inherited_exec_policy*/ None,
|
/*inherited_exec_policy*/ None,
|
||||||
parent_trace,
|
parent_trace,
|
||||||
@@ -773,6 +779,7 @@ impl ThreadManager {
|
|||||||
/*thread_source*/ None,
|
/*thread_source*/ None,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
/*metrics_service_name*/ None,
|
/*metrics_service_name*/ None,
|
||||||
|
/*initial_multi_agent_mode*/ None,
|
||||||
/*parent_trace*/ None,
|
/*parent_trace*/ None,
|
||||||
environments,
|
environments,
|
||||||
/*thread_extension_init*/ ExtensionDataInit::default(),
|
/*thread_extension_init*/ ExtensionDataInit::default(),
|
||||||
@@ -799,6 +806,7 @@ impl ThreadManager {
|
|||||||
let (session_source, thread_source) = initial_history
|
let (session_source, thread_source) = initial_history
|
||||||
.get_resumed_session_sources()
|
.get_resumed_session_sources()
|
||||||
.unwrap_or_else(|| (self.state.session_source.clone(), None));
|
.unwrap_or_else(|| (self.state.session_source.clone(), None));
|
||||||
|
let initial_multi_agent_mode = initial_history.get_latest_effective_multi_agent_mode();
|
||||||
Box::pin(self.state.spawn_thread_with_source(
|
Box::pin(self.state.spawn_thread_with_source(
|
||||||
config,
|
config,
|
||||||
initial_history,
|
initial_history,
|
||||||
@@ -810,6 +818,7 @@ impl ThreadManager {
|
|||||||
thread_source,
|
thread_source,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
/*metrics_service_name*/ None,
|
/*metrics_service_name*/ None,
|
||||||
|
initial_multi_agent_mode,
|
||||||
/*inherited_environments*/ None,
|
/*inherited_environments*/ None,
|
||||||
/*inherited_exec_policy*/ None,
|
/*inherited_exec_policy*/ None,
|
||||||
/*parent_trace*/ None,
|
/*parent_trace*/ None,
|
||||||
@@ -960,18 +969,25 @@ impl ThreadManager {
|
|||||||
) -> CodexResult<NewThread> {
|
) -> CodexResult<NewThread> {
|
||||||
// `forked_from_id()` describes this history's existing lineage. When
|
// `forked_from_id()` describes this history's existing lineage. When
|
||||||
// forking a resumed thread, the child copies the resumed thread itself.
|
// forking a resumed thread, the child copies the resumed thread itself.
|
||||||
let forked_from_thread_id = match &history {
|
let source_thread_id = match &history {
|
||||||
InitialHistory::Resumed(resumed) => Some(resumed.conversation_id),
|
InitialHistory::Resumed(resumed) => Some(resumed.conversation_id),
|
||||||
InitialHistory::Forked(_) => history.forked_from_id(),
|
InitialHistory::Forked(_) => history.forked_from_id(),
|
||||||
InitialHistory::New | InitialHistory::Cleared => None,
|
InitialHistory::New | InitialHistory::Cleared => None,
|
||||||
};
|
};
|
||||||
|
let initial_multi_agent_mode = match source_thread_id {
|
||||||
|
Some(thread_id) => match self.get_thread(thread_id).await {
|
||||||
|
Ok(thread) => thread.config_snapshot().await.multi_agent_mode,
|
||||||
|
Err(_) => history.get_latest_effective_multi_agent_mode(),
|
||||||
|
},
|
||||||
|
None => history.get_latest_effective_multi_agent_mode(),
|
||||||
|
};
|
||||||
let multi_agent_version = self
|
let multi_agent_version = self
|
||||||
.state
|
.state
|
||||||
.effective_multi_agent_version_for_spawn(
|
.effective_multi_agent_version_for_spawn(
|
||||||
&history,
|
&history,
|
||||||
/*session_source*/ None,
|
/*session_source*/ None,
|
||||||
/*parent_thread_id*/ None,
|
/*parent_thread_id*/ None,
|
||||||
forked_from_thread_id,
|
source_thread_id,
|
||||||
&config,
|
&config,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -989,10 +1005,11 @@ impl ThreadManager {
|
|||||||
Arc::clone(&self.state.auth_manager),
|
Arc::clone(&self.state.auth_manager),
|
||||||
agent_control,
|
agent_control,
|
||||||
/*parent_thread_id*/ None,
|
/*parent_thread_id*/ None,
|
||||||
forked_from_thread_id,
|
source_thread_id,
|
||||||
thread_source,
|
thread_source,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
/*metrics_service_name*/ None,
|
/*metrics_service_name*/ None,
|
||||||
|
initial_multi_agent_mode,
|
||||||
parent_trace,
|
parent_trace,
|
||||||
environments,
|
environments,
|
||||||
/*thread_extension_init*/ ExtensionDataInit::default(),
|
/*thread_extension_init*/ ExtensionDataInit::default(),
|
||||||
@@ -1217,6 +1234,7 @@ impl ThreadManagerState {
|
|||||||
/*forked_from_thread_id*/ None,
|
/*forked_from_thread_id*/ None,
|
||||||
/*thread_source*/ None,
|
/*thread_source*/ None,
|
||||||
/*metrics_service_name*/ None,
|
/*metrics_service_name*/ None,
|
||||||
|
/*initial_multi_agent_mode*/ None,
|
||||||
/*inherited_environments*/ None,
|
/*inherited_environments*/ None,
|
||||||
/*inherited_exec_policy*/ None,
|
/*inherited_exec_policy*/ None,
|
||||||
/*environments*/ None,
|
/*environments*/ None,
|
||||||
@@ -1234,6 +1252,7 @@ impl ThreadManagerState {
|
|||||||
forked_from_thread_id: Option<ThreadId>,
|
forked_from_thread_id: Option<ThreadId>,
|
||||||
thread_source: Option<ThreadSource>,
|
thread_source: Option<ThreadSource>,
|
||||||
metrics_service_name: Option<String>,
|
metrics_service_name: Option<String>,
|
||||||
|
initial_multi_agent_mode: Option<MultiAgentMode>,
|
||||||
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
||||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||||
environments: Option<Vec<TurnEnvironmentSelection>>,
|
environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||||
@@ -1252,6 +1271,7 @@ impl ThreadManagerState {
|
|||||||
thread_source,
|
thread_source,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
metrics_service_name,
|
metrics_service_name,
|
||||||
|
initial_multi_agent_mode,
|
||||||
inherited_environments,
|
inherited_environments,
|
||||||
inherited_exec_policy,
|
inherited_exec_policy,
|
||||||
/*parent_trace*/ None,
|
/*parent_trace*/ None,
|
||||||
@@ -1279,6 +1299,7 @@ impl ThreadManagerState {
|
|||||||
let environments =
|
let environments =
|
||||||
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd);
|
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd);
|
||||||
let thread_source = initial_history.get_resumed_thread_source();
|
let thread_source = initial_history.get_resumed_thread_source();
|
||||||
|
let initial_multi_agent_mode = initial_history.get_latest_effective_multi_agent_mode();
|
||||||
Box::pin(self.spawn_thread_with_source(
|
Box::pin(self.spawn_thread_with_source(
|
||||||
config,
|
config,
|
||||||
initial_history,
|
initial_history,
|
||||||
@@ -1290,6 +1311,7 @@ impl ThreadManagerState {
|
|||||||
thread_source,
|
thread_source,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
/*metrics_service_name*/ None,
|
/*metrics_service_name*/ None,
|
||||||
|
initial_multi_agent_mode,
|
||||||
inherited_environments,
|
inherited_environments,
|
||||||
inherited_exec_policy,
|
inherited_exec_policy,
|
||||||
/*parent_trace*/ None,
|
/*parent_trace*/ None,
|
||||||
@@ -1311,6 +1333,7 @@ impl ThreadManagerState {
|
|||||||
thread_source: Option<ThreadSource>,
|
thread_source: Option<ThreadSource>,
|
||||||
parent_thread_id: Option<ThreadId>,
|
parent_thread_id: Option<ThreadId>,
|
||||||
forked_from_thread_id: Option<ThreadId>,
|
forked_from_thread_id: Option<ThreadId>,
|
||||||
|
initial_multi_agent_mode: Option<MultiAgentMode>,
|
||||||
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
||||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||||
environments: Option<Vec<TurnEnvironmentSelection>>,
|
environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||||
@@ -1329,6 +1352,7 @@ impl ThreadManagerState {
|
|||||||
thread_source,
|
thread_source,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
/*metrics_service_name*/ None,
|
/*metrics_service_name*/ None,
|
||||||
|
initial_multi_agent_mode,
|
||||||
inherited_environments,
|
inherited_environments,
|
||||||
inherited_exec_policy,
|
inherited_exec_policy,
|
||||||
/*parent_trace*/ None,
|
/*parent_trace*/ None,
|
||||||
@@ -1353,6 +1377,7 @@ impl ThreadManagerState {
|
|||||||
thread_source: Option<ThreadSource>,
|
thread_source: Option<ThreadSource>,
|
||||||
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
||||||
metrics_service_name: Option<String>,
|
metrics_service_name: Option<String>,
|
||||||
|
initial_multi_agent_mode: Option<MultiAgentMode>,
|
||||||
parent_trace: Option<W3cTraceContext>,
|
parent_trace: Option<W3cTraceContext>,
|
||||||
environments: Vec<TurnEnvironmentSelection>,
|
environments: Vec<TurnEnvironmentSelection>,
|
||||||
thread_extension_init: ExtensionDataInit,
|
thread_extension_init: ExtensionDataInit,
|
||||||
@@ -1370,6 +1395,7 @@ impl ThreadManagerState {
|
|||||||
thread_source,
|
thread_source,
|
||||||
dynamic_tools,
|
dynamic_tools,
|
||||||
metrics_service_name,
|
metrics_service_name,
|
||||||
|
initial_multi_agent_mode,
|
||||||
/*inherited_environments*/ None,
|
/*inherited_environments*/ None,
|
||||||
/*inherited_exec_policy*/ None,
|
/*inherited_exec_policy*/ None,
|
||||||
parent_trace,
|
parent_trace,
|
||||||
@@ -1394,6 +1420,7 @@ impl ThreadManagerState {
|
|||||||
thread_source: Option<ThreadSource>,
|
thread_source: Option<ThreadSource>,
|
||||||
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
|
||||||
metrics_service_name: Option<String>,
|
metrics_service_name: Option<String>,
|
||||||
|
initial_multi_agent_mode: Option<MultiAgentMode>,
|
||||||
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
||||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||||
parent_trace: Option<W3cTraceContext>,
|
parent_trace: Option<W3cTraceContext>,
|
||||||
@@ -1473,6 +1500,7 @@ impl ThreadManagerState {
|
|||||||
attestation_provider: self.attestation_provider.clone(),
|
attestation_provider: self.attestation_provider.clone(),
|
||||||
external_time_provider: self.external_time_provider.clone(),
|
external_time_provider: self.external_time_provider.clone(),
|
||||||
inherited_multi_agent_version: multi_agent_version,
|
inherited_multi_agent_version: multi_agent_version,
|
||||||
|
initial_multi_agent_mode,
|
||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
let new_thread = self
|
let new_thread = self
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
|
|||||||
thread_source: None,
|
thread_source: None,
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments: Vec::new(),
|
environments: Vec::new(),
|
||||||
thread_extension_init: Default::default(),
|
thread_extension_init: Default::default(),
|
||||||
@@ -462,6 +463,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
|||||||
thread_source: None,
|
thread_source: None,
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments: Vec::new(),
|
environments: Vec::new(),
|
||||||
thread_extension_init: selected_root_init("selected-a", "env-a"),
|
thread_extension_init: selected_root_init("selected-a", "env-a"),
|
||||||
@@ -477,6 +479,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
|||||||
thread_source: None,
|
thread_source: None,
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments: Vec::new(),
|
environments: Vec::new(),
|
||||||
thread_extension_init: selected_root_init("selected-b", "env-b"),
|
thread_extension_init: selected_root_init("selected-b", "env-b"),
|
||||||
@@ -569,6 +572,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
|||||||
thread_source: None,
|
thread_source: None,
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments: environments.clone(),
|
environments: environments.clone(),
|
||||||
thread_extension_init: Default::default(),
|
thread_extension_init: Default::default(),
|
||||||
@@ -852,6 +856,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
|
|||||||
thread_source: Some(ThreadSource::User),
|
thread_source: Some(ThreadSource::User),
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments: Vec::new(),
|
environments: Vec::new(),
|
||||||
thread_extension_init: Default::default(),
|
thread_extension_init: Default::default(),
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ async fn handle_spawn_agent(
|
|||||||
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
|
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
|
||||||
parent_thread_id: Some(session.thread_id),
|
parent_thread_id: Some(session.thread_id),
|
||||||
environments: Some(turn.environments.to_selections()),
|
environments: Some(turn.environments.to_selections()),
|
||||||
|
initial_multi_agent_mode: None,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ use codex_model_provider_info::built_in_model_providers;
|
|||||||
use codex_protocol::AgentPath;
|
use codex_protocol::AgentPath;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::config_types::ApprovalsReviewer;
|
use codex_protocol::config_types::ApprovalsReviewer;
|
||||||
|
use codex_protocol::config_types::MultiAgentMode;
|
||||||
use codex_protocol::config_types::ServiceTier;
|
use codex_protocol::config_types::ServiceTier;
|
||||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||||
use codex_protocol::models::BaseInstructions;
|
use codex_protocol::models::BaseInstructions;
|
||||||
@@ -1147,6 +1148,11 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
|||||||
.features
|
.features
|
||||||
.enable(Feature::MultiAgentV2)
|
.enable(Feature::MultiAgentV2)
|
||||||
.expect("test config should allow feature update");
|
.expect("test config should allow feature update");
|
||||||
|
config
|
||||||
|
.features
|
||||||
|
.enable(Feature::MultiAgentMode)
|
||||||
|
.expect("test config should allow feature update");
|
||||||
|
turn.multi_agent_mode = Some(MultiAgentMode::Proactive);
|
||||||
set_turn_config(&mut turn, config);
|
set_turn_config(&mut turn, config);
|
||||||
|
|
||||||
let session = Arc::new(session);
|
let session = Arc::new(session);
|
||||||
@@ -1185,6 +1191,10 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
|
|||||||
child_snapshot.session_source.get_agent_path().as_deref(),
|
child_snapshot.session_source.get_agent_path().as_deref(),
|
||||||
Some("/root/test_process")
|
Some("/root/test_process")
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
child_snapshot.multi_agent_mode,
|
||||||
|
Some(MultiAgentMode::Proactive)
|
||||||
|
);
|
||||||
assert!(manager.captured_ops().iter().any(|(id, op)| {
|
assert!(manager.captured_ops().iter().any(|(id, op)| {
|
||||||
*id == child_thread_id
|
*id == child_thread_id
|
||||||
&& matches!(
|
&& matches!(
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ async fn handle_spawn_agent(
|
|||||||
let arguments = function_arguments(payload)?;
|
let arguments = function_arguments(payload)?;
|
||||||
let args: SpawnAgentArgs = parse_arguments(&arguments)?;
|
let args: SpawnAgentArgs = parse_arguments(&arguments)?;
|
||||||
let fork_mode = args.fork_mode()?;
|
let fork_mode = args.fork_mode()?;
|
||||||
|
let multi_agent_mode = crate::session::multi_agents::effective_multi_agent_mode(
|
||||||
|
turn.multi_agent_version,
|
||||||
|
&turn.config.multi_agent_v2,
|
||||||
|
&turn.session_source,
|
||||||
|
turn.multi_agent_mode,
|
||||||
|
turn.config
|
||||||
|
.features
|
||||||
|
.enabled(codex_features::Feature::MultiAgentMode),
|
||||||
|
);
|
||||||
let role_name = args
|
let role_name = args
|
||||||
.agent_type
|
.agent_type
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -134,6 +143,7 @@ async fn handle_spawn_agent(
|
|||||||
fork_mode,
|
fork_mode,
|
||||||
parent_thread_id: Some(session.thread_id),
|
parent_thread_id: Some(session.thread_id),
|
||||||
environments: Some(turn.environments.to_selections()),
|
environments: Some(turn.environments.to_selections()),
|
||||||
|
initial_multi_agent_mode: multi_agent_mode,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -641,6 +641,7 @@ impl TestCodexBuilder {
|
|||||||
thread_source: None,
|
thread_source: None,
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments,
|
environments,
|
||||||
thread_extension_init: Default::default(),
|
thread_extension_init: Default::default(),
|
||||||
|
|||||||
@@ -441,6 +441,7 @@ async fn loads_user_instructions_without_a_primary_environment() -> Result<()> {
|
|||||||
thread_source: None,
|
thread_source: None,
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments: Vec::new(),
|
environments: Vec::new(),
|
||||||
thread_extension_init: Default::default(),
|
thread_extension_init: Default::default(),
|
||||||
@@ -648,6 +649,7 @@ async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapsho
|
|||||||
thread_source: None,
|
thread_source: None,
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments: vec![
|
environments: vec![
|
||||||
TurnEnvironmentSelection {
|
TurnEnvironmentSelection {
|
||||||
|
|||||||
@@ -751,6 +751,7 @@ async fn subagent_stop_replaces_stop_and_skips_internal_subagents() -> Result<()
|
|||||||
thread_source: None,
|
thread_source: None,
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments: Vec::new(),
|
environments: Vec::new(),
|
||||||
thread_extension_init: Default::default(),
|
thread_extension_init: Default::default(),
|
||||||
|
|||||||
@@ -790,5 +790,6 @@ fn sample_thread_start_response() -> ThreadStartResponse {
|
|||||||
},
|
},
|
||||||
active_permission_profile: None,
|
active_permission_profile: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,6 +311,7 @@ impl MemoryStartupContext {
|
|||||||
thread_source: Some(ThreadSource::MemoryConsolidation),
|
thread_source: Some(ThreadSource::MemoryConsolidation),
|
||||||
dynamic_tools: Vec::new(),
|
dynamic_tools: Vec::new(),
|
||||||
metrics_service_name: None,
|
metrics_service_name: None,
|
||||||
|
multi_agent_mode: None,
|
||||||
parent_trace: None,
|
parent_trace: None,
|
||||||
environments,
|
environments,
|
||||||
thread_extension_init: Default::default(),
|
thread_extension_init: Default::default(),
|
||||||
|
|||||||
@@ -1992,6 +1992,8 @@ pub struct ThreadSettingsSnapshot {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub personality: Option<Personality>,
|
pub personality: Option<Personality>,
|
||||||
pub collaboration_mode: CollaborationMode,
|
pub collaboration_mode: CollaborationMode,
|
||||||
|
#[serde(default)]
|
||||||
|
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||||
@@ -2554,12 +2556,24 @@ impl InitialHistory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_multi_agent_mode(&self) -> Option<MultiAgentMode> {
|
pub fn get_latest_effective_multi_agent_mode(&self) -> Option<MultiAgentMode> {
|
||||||
match self {
|
let items = match self {
|
||||||
InitialHistory::New | InitialHistory::Cleared => None,
|
InitialHistory::New | InitialHistory::Cleared => return None,
|
||||||
InitialHistory::Resumed(resumed) => multi_agent_mode_from_items(&resumed.history),
|
InitialHistory::Resumed(resumed) => &resumed.history,
|
||||||
InitialHistory::Forked(items) => multi_agent_mode_from_items(items),
|
InitialHistory::Forked(items) => items,
|
||||||
}
|
};
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find_map(|item| match item {
|
||||||
|
RolloutItem::TurnContext(turn_context) => Some(turn_context),
|
||||||
|
RolloutItem::SessionMeta(_)
|
||||||
|
| RolloutItem::ResponseItem(_)
|
||||||
|
| RolloutItem::InterAgentCommunication(_)
|
||||||
|
| RolloutItem::Compacted(_)
|
||||||
|
| RolloutItem::EventMsg(_) => None,
|
||||||
|
})
|
||||||
|
.and_then(|turn_context| turn_context.multi_agent_mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_resumed_session_sources(&self) -> Option<(SessionSource, Option<ThreadSource>)> {
|
pub fn get_resumed_session_sources(&self) -> Option<(SessionSource, Option<ThreadSource>)> {
|
||||||
@@ -2874,17 +2888,6 @@ 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)]
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[ts(rename_all = "snake_case")]
|
#[ts(rename_all = "snake_case")]
|
||||||
@@ -5399,6 +5402,31 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn latest_effective_multi_agent_mode_uses_latest_turn_context_even_when_unset() -> Result<()> {
|
||||||
|
let turn_context_item = |multi_agent_mode| -> Result<RolloutItem> {
|
||||||
|
let mut value = json!({
|
||||||
|
"cwd": test_path_buf("/tmp"),
|
||||||
|
"approval_policy": "never",
|
||||||
|
"sandbox_policy": { "type": "danger-full-access" },
|
||||||
|
"model": "gpt-5",
|
||||||
|
"summary": "auto",
|
||||||
|
});
|
||||||
|
value["multi_agent_mode"] = serde_json::to_value(multi_agent_mode)?;
|
||||||
|
Ok(RolloutItem::TurnContext(serde_json::from_value(value)?))
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
InitialHistory::Forked(vec![
|
||||||
|
turn_context_item(Some(MultiAgentMode::Proactive))?,
|
||||||
|
turn_context_item(/*multi_agent_mode*/ None)?,
|
||||||
|
])
|
||||||
|
.get_latest_effective_multi_agent_mode(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn turn_context_item_serializes_network_when_present() -> Result<()> {
|
fn turn_context_item_serializes_network_when_present() -> Result<()> {
|
||||||
let item = TurnContextItem {
|
let item = TurnContextItem {
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ mod tests {
|
|||||||
developer_instructions: None,
|
developer_instructions: None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
personality: None,
|
personality: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6038,6 +6038,7 @@ async fn inactive_thread_settings_notification_updates_cached_collaboration_mode
|
|||||||
effort: collaboration_mode.settings.reasoning_effort.clone(),
|
effort: collaboration_mode.settings.reasoning_effort.clone(),
|
||||||
summary: None,
|
summary: None,
|
||||||
collaboration_mode: collaboration_mode.clone(),
|
collaboration_mode: collaboration_mode.clone(),
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
personality: Some(Personality::Pragmatic),
|
personality: Some(Personality::Pragmatic),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2381,6 +2381,7 @@ mod tests {
|
|||||||
.into(),
|
.into(),
|
||||||
active_permission_profile: None,
|
active_permission_profile: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
initial_turns_page: None,
|
initial_turns_page: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ fn thread_settings_for_test(
|
|||||||
developer_instructions: None,
|
developer_instructions: None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
multi_agent_mode: Default::default(),
|
||||||
personality: Some(Personality::Pragmatic),
|
personality: Some(Personality::Pragmatic),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user