mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Add host-reserved V2 child creation APIs (#57586)
## What changed - Add `CodexThread::create_child` to create an idle child from a live tool invocation using a host-reserved thread ID and controller. - Add `prepare_child` and `ThreadManager::create_prepared_child` to capture inherited settings and fresh or forked history, then start the child on another manager even after the parent shuts down. Reject detached preparation when live bindings cannot be transferred. - Add opt-in `HostToolInvocationLookup` to track live calls and reject duplicate active call IDs within a turn. Revoke invocation access when dispatch ends and clean up provisional children when startup fails or is cancelled. ## Testing Add tests for invocation lifetime and call ID reservation, inherited policy and live bindings, detached startup with fresh and full history, and cancellation cleanup. GitOrigin-RevId: 71f17ecb4cd7636517e675df59289a8e8c7105b8
This commit is contained in:
@@ -14,6 +14,7 @@ use crate::agent::types::MessageDeliveryMode;
|
||||
use crate::agent::types::SpawnAgentForkMode;
|
||||
use crate::agent::types::SpawnAgentOptions;
|
||||
use crate::codex_thread::ThreadConfigSnapshot;
|
||||
use crate::config::Config;
|
||||
use crate::session::multi_agents::resolve_usage_hints;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::step_context::StepContext;
|
||||
@@ -26,6 +27,15 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
|
||||
/// Child creation options; omitted overrides retain native MAv2 inheritance and role rules.
|
||||
pub struct ThreadCreateRequest {
|
||||
pub task_name: String,
|
||||
/// `None` starts fresh; `FullHistory` uses native fork preparation.
|
||||
pub fork_mode: Option<SpawnAgentForkMode>,
|
||||
pub agent_type: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
pub(crate) struct ThreadSpawnRequest {
|
||||
pub task_name: String,
|
||||
/// Explicit encoding: hosts normally provide plaintext; native tools can forward ciphertext.
|
||||
@@ -37,8 +47,39 @@ pub(crate) struct ThreadSpawnRequest {
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
/// Accepted child and its effective configuration. Acceptance does not mean the turn completed.
|
||||
pub(crate) struct ThreadSpawnResult {
|
||||
impl ThreadSpawnRequest {
|
||||
pub(crate) fn into_parts(
|
||||
self,
|
||||
) -> Result<(ThreadCreateRequest, AgentMessage), ThreadSpawnError> {
|
||||
let message_text = match &self.message {
|
||||
AgentMessage::Plaintext(text) | AgentMessage::Encrypted(text) => text,
|
||||
};
|
||||
if message_text.trim().is_empty() {
|
||||
return Err(ThreadSpawnError::Preparation(
|
||||
"Empty message can't be sent to an agent".to_string(),
|
||||
));
|
||||
}
|
||||
Ok((
|
||||
ThreadCreateRequest {
|
||||
task_name: self.task_name,
|
||||
fork_mode: self.fork_mode,
|
||||
agent_type: self.agent_type,
|
||||
model: self.model,
|
||||
reasoning_effort: self.reasoning_effort,
|
||||
},
|
||||
self.message,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ChildSpawnSettings {
|
||||
pub(crate) config: Config,
|
||||
pub(crate) source: SessionSource,
|
||||
pub(crate) options: SpawnAgentOptions,
|
||||
}
|
||||
|
||||
/// Child runtime and its effective configuration.
|
||||
pub struct ThreadSpawnResult {
|
||||
pub agent: LiveAgent,
|
||||
pub config: ThreadConfigSnapshot,
|
||||
pub task_name: AgentPath,
|
||||
@@ -46,7 +87,7 @@ pub(crate) struct ThreadSpawnResult {
|
||||
|
||||
/// Separates preparation errors from backend failures to preserve native tool error reporting.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ThreadSpawnError {
|
||||
pub enum ThreadSpawnError {
|
||||
#[error("{0}")]
|
||||
Preparation(String),
|
||||
#[error(transparent)]
|
||||
@@ -59,7 +100,18 @@ pub(crate) async fn spawn(
|
||||
call_id: &str,
|
||||
request: ThreadSpawnRequest,
|
||||
) -> Result<ThreadSpawnResult, ThreadSpawnError> {
|
||||
let (request, task_name) = prepare(session, step_context, call_id, request).await?;
|
||||
let (request, message) = request.into_parts()?;
|
||||
let (settings, task_name) = prepare(session, step_context, call_id, request).await?;
|
||||
let request = SpawnRequest {
|
||||
caller: session.thread_id,
|
||||
config: settings.config,
|
||||
input: AgentInput::Message {
|
||||
message,
|
||||
mode: MessageDeliveryMode::TriggerTurn,
|
||||
},
|
||||
source: settings.source,
|
||||
options: settings.options,
|
||||
};
|
||||
let (agent, config) = session.services.agent_control.spawn(request).await?;
|
||||
Ok(ThreadSpawnResult {
|
||||
agent,
|
||||
@@ -73,25 +125,16 @@ pub(crate) async fn prepare(
|
||||
session: &Session,
|
||||
step_context: &StepContext,
|
||||
call_id: &str,
|
||||
request: ThreadSpawnRequest,
|
||||
) -> Result<(SpawnRequest, AgentPath), ThreadSpawnError> {
|
||||
request: ThreadCreateRequest,
|
||||
) -> Result<(ChildSpawnSettings, AgentPath), ThreadSpawnError> {
|
||||
let turn = &step_context.turn;
|
||||
let ThreadSpawnRequest {
|
||||
let ThreadCreateRequest {
|
||||
task_name,
|
||||
message,
|
||||
fork_mode,
|
||||
agent_type,
|
||||
model,
|
||||
reasoning_effort,
|
||||
} = request;
|
||||
let message_text = match &message {
|
||||
AgentMessage::Plaintext(text) | AgentMessage::Encrypted(text) => text,
|
||||
};
|
||||
if message_text.trim().is_empty() {
|
||||
return Err(ThreadSpawnError::Preparation(
|
||||
"Empty message can't be sent to an agent".to_string(),
|
||||
));
|
||||
}
|
||||
let role_name = agent_type
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
@@ -149,13 +192,8 @@ pub(crate) async fn prepare(
|
||||
None
|
||||
};
|
||||
Ok((
|
||||
SpawnRequest {
|
||||
caller: session.thread_id,
|
||||
ChildSpawnSettings {
|
||||
config,
|
||||
input: AgentInput::Message {
|
||||
message,
|
||||
mode: MessageDeliveryMode::TriggerTurn,
|
||||
},
|
||||
source: spawn_source,
|
||||
options: SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.to_string()),
|
||||
|
||||
Reference in New Issue
Block a user