feat: fork thread multi agent

This commit is contained in:
jif-oai
2026-02-22 14:15:52 +00:00
parent 55fc075723
commit b6d6011edb
4 changed files with 94 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ use crate::agent::status::is_final;
use crate::error::CodexErr;
use crate::error::Result as CodexResult;
use crate::find_thread_path_by_id_str;
use crate::rollout::RolloutRecorder;
use crate::session_prefix::format_subagent_notification_message;
use crate::state_db;
use crate::thread_manager::ThreadManagerState;
@@ -19,6 +20,11 @@ use tokio::sync::watch;
const AGENT_NAMES: &str = include_str!("agent_names.txt");
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct SpawnAgentOptions {
pub(crate) fork_parent_thread: bool,
}
fn agent_nickname_list() -> Vec<&'static str> {
AGENT_NAMES
.lines()
@@ -57,6 +63,17 @@ impl AgentControl {
config: crate::config::Config,
items: Vec<UserInput>,
session_source: Option<SessionSource>,
) -> CodexResult<ThreadId> {
self.spawn_agent_with_options(config, items, session_source, SpawnAgentOptions::default())
.await
}
pub(crate) async fn spawn_agent_with_options(
&self,
config: crate::config::Config,
items: Vec<UserInput>,
session_source: Option<SessionSource>,
options: SpawnAgentOptions,
) -> CodexResult<ThreadId> {
let state = self.upgrade()?;
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
@@ -82,9 +99,47 @@ impl AgentControl {
// The same `AgentControl` is sent to spawn the thread.
let new_thread = match session_source {
Some(session_source) => {
state
.spawn_new_thread_with_source(config, self.clone(), session_source, false)
.await?
if options.fork_parent_thread {
let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
..
}) = session_source.clone()
else {
return Err(CodexErr::Fatal(
"spawn_agent fork requires a thread-spawn session source".to_string(),
));
};
let rollout_path = match state.get_thread(parent_thread_id).await {
Ok(parent_thread) => parent_thread.rollout_path(),
Err(_) => None,
}
.or(
find_thread_path_by_id_str(
config.codex_home.as_path(),
&parent_thread_id.to_string(),
)
.await?,
)
.ok_or_else(|| {
CodexErr::Fatal(format!(
"parent thread rollout unavailable for fork: {parent_thread_id}"
))
})?;
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
state
.fork_thread_with_source(
config,
initial_history,
self.clone(),
session_source,
false,
)
.await?
} else {
state
.spawn_new_thread_with_source(config, self.clone(), session_source, false)
.await?
}
}
None => state.spawn_new_thread(config, self.clone()).await?,
};

View File

@@ -464,6 +464,26 @@ impl ThreadManagerState {
.await
}
pub(crate) async fn fork_thread_with_source(
&self,
config: Config,
initial_history: InitialHistory,
agent_control: AgentControl,
session_source: SessionSource,
persist_extended_history: bool,
) -> CodexResult<NewThread> {
self.spawn_thread_with_source(
config,
initial_history,
Arc::clone(&self.auth_manager),
agent_control,
session_source,
Vec::new(),
persist_extended_history,
)
.await
}
/// Spawn a new thread with optional history and register it with the manager.
pub(crate) async fn spawn_thread(
&self,

View File

@@ -94,6 +94,7 @@ impl ToolHandler for MultiAgentHandler {
mod spawn {
use super::*;
use crate::agent::role::apply_role_to_config;
use crate::agent::control::SpawnAgentOptions;
use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::next_thread_spawn_depth;
@@ -104,6 +105,8 @@ mod spawn {
message: Option<String>,
items: Option<Vec<UserInput>>,
agent_type: Option<String>,
#[serde(default)]
fork_current_thread: bool,
}
#[derive(Debug, Serialize)]
@@ -156,7 +159,7 @@ mod spawn {
let result = session
.services
.agent_control
.spawn_agent(
.spawn_agent_with_options(
config,
input_items,
Some(thread_spawn_source(
@@ -164,6 +167,9 @@ mod spawn {
child_depth,
role_name,
)),
SpawnAgentOptions {
fork_parent_thread: args.fork_current_thread,
},
)
.await
.map_err(collab_spawn_error);

View File

@@ -552,6 +552,15 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec {
)),
},
),
(
"fork_current_thread".to_string(),
JsonSchema::Boolean {
description: Some(
"When true, fork the current thread history into the new agent before sending the initial prompt."
.to_string(),
),
},
),
]);
ToolSpec::Function(ResponsesApiTool {