mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08: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:
committed by
GitHub
Unverified
parent
fc8c6b7384
commit
7abfcf220b
@@ -1992,6 +1992,8 @@ pub struct ThreadSettingsSnapshot {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub personality: Option<Personality>,
|
||||
pub collaboration_mode: CollaborationMode,
|
||||
#[serde(default)]
|
||||
pub multi_agent_mode: Option<MultiAgentMode>,
|
||||
}
|
||||
|
||||
#[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> {
|
||||
match self {
|
||||
InitialHistory::New | InitialHistory::Cleared => None,
|
||||
InitialHistory::Resumed(resumed) => multi_agent_mode_from_items(&resumed.history),
|
||||
InitialHistory::Forked(items) => multi_agent_mode_from_items(items),
|
||||
}
|
||||
pub fn get_latest_effective_multi_agent_mode(&self) -> Option<MultiAgentMode> {
|
||||
let items = match self {
|
||||
InitialHistory::New | InitialHistory::Cleared => return None,
|
||||
InitialHistory::Resumed(resumed) => &resumed.history,
|
||||
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>)> {
|
||||
@@ -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)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
@@ -5399,6 +5402,31 @@ mod tests {
|
||||
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]
|
||||
fn turn_context_item_serializes_network_when_present() -> Result<()> {
|
||||
let item = TurnContextItem {
|
||||
|
||||
Reference in New Issue
Block a user