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:
rhan-oai
2026-10-27 23:02:27 +00:00
committed by copyberry
parent cb66dbecf2
commit 37f199dab1
29 changed files with 2203 additions and 209 deletions
+61
View File
@@ -8,11 +8,15 @@ use crate::elicitation::ElicitationRegistration;
use crate::session::SessionIo;
use crate::session::SessionSettingsUpdate;
use crate::session::Submission;
use crate::session::emit_subagent_session_started;
use crate::session::new_submission_id;
use crate::session::session::AppServerClientMetadata;
use crate::session::session::Session;
use crate::session::startup_prewarm::PrewarmInput;
use crate::session::step_settings::StepSettingsUpdate;
use crate::thread_startup_metadata::ThreadStartupMetadata;
use chrono::DateTime;
use chrono::Utc;
use codex_diagnostics::Gauge;
use codex_diagnostics::GaugeGuard;
use codex_exec_server::SelectedCapabilityRootsStatus;
@@ -45,6 +49,7 @@ use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::ThreadSettingsOverrides;
@@ -289,6 +294,39 @@ impl CodexThread {
self.session.services.analytics_events_client.is_enabled()
}
/// Looks up a live V2 tool invocation on a runtime with [`crate::HostToolInvocationLookup`].
pub fn tool_invocation(
&self,
turn_id: &str,
call_id: &str,
) -> CodexResult<crate::ToolInvocationContext> {
crate::agent::host::tool_invocation(self, turn_id, call_id)
}
/// Creates an idle V2 child here with the invoking step's environments and live application
/// bindings. Compatible children share the parent's exec policy.
/// Ending the invocation or dropping this future aborts pending startup.
/// [`crate::ThreadManager::create_prepared_child`] defines acceptance and cleanup.
pub async fn create_child(
&self,
invocation: &crate::ToolInvocationContext,
request: crate::ThreadCreateRequest,
reservation: crate::HostThreadSpawnReservation,
) -> Result<crate::ThreadSpawnResult, crate::ThreadSpawnError> {
crate::agent::host::create(self, invocation, request, reservation).await
}
/// Captures inputs from a live invocation for [`crate::ThreadManager::create_prepared_child`].
/// The host owns cancellation and snapshot freshness after this returns. Detached preparation
/// rejects managed application network, auth, and cloud config bindings.
pub async fn prepare_child(
&self,
invocation: &crate::ToolInvocationContext,
request: crate::ThreadCreateRequest,
) -> Result<crate::PreparedThreadSpawn, crate::ThreadSpawnError> {
crate::agent::host::prepare(self, invocation, request).await
}
/// Returns extension-owned data attached to this thread runtime.
pub fn thread_extension_data(&self) -> &codex_extension_api::ExtensionData {
&self.session.services.thread_extension_data
@@ -338,6 +376,29 @@ impl CodexThread {
self.io.session_loop_termination.clone().await;
}
pub(crate) async fn emit_subagent_session_started(
&self,
client_metadata: AppServerClientMetadata,
resumed_created_at: Option<DateTime<Utc>>,
) {
let SessionSource::SubAgent(source @ SubAgentSource::ThreadSpawn { .. }) =
&self.session_source
else {
return;
};
let config = self.config_snapshot().await;
emit_subagent_session_started(
&self.session.services.analytics_events_client,
client_metadata,
self.session.session_id(),
self.session.thread_id(),
config.parent_thread_id,
config,
source.clone(),
resumed_created_at,
);
}
pub(crate) async fn emit_thread_ready_lifecycle(&self) {
let contributors = self
.session