mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +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:
@@ -48,6 +48,7 @@ pub type SharedTurnDiffTracker = Arc<Mutex<TurnDiffTracker>>;
|
||||
pub(crate) struct ToolCallState {
|
||||
pub(crate) terminal_outcome_reached: AtomicBool,
|
||||
pub(crate) delivered_assistant_message: OnceLock<String>,
|
||||
pub(crate) invocation: crate::agent::host::InvocationReservation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -160,6 +160,13 @@ impl ToolCallRuntime {
|
||||
cancellation_token: CancellationToken,
|
||||
call_state: Arc<ToolCallState>,
|
||||
) -> impl std::future::Future<Output = Result<AnyToolResult, FunctionCallError>> {
|
||||
if let Err(err) =
|
||||
call_state
|
||||
.invocation
|
||||
.reserve(&self.session, &step_context.turn, &call.call_id)
|
||||
{
|
||||
return Either::Left(std::future::ready(Err(err)));
|
||||
}
|
||||
let message_admission =
|
||||
super::user_messaging::admit_code_mode_send(&self.session, &source, &call.tool_name);
|
||||
self.session
|
||||
@@ -320,7 +327,7 @@ impl ToolCallRuntime {
|
||||
tracker,
|
||||
dispatch_call,
|
||||
source,
|
||||
dispatch_call_state,
|
||||
Arc::clone(&dispatch_call_state),
|
||||
)
|
||||
.await;
|
||||
drop(_admitted_timing);
|
||||
@@ -339,22 +346,34 @@ impl ToolCallRuntime {
|
||||
started.elapsed(),
|
||||
);
|
||||
}
|
||||
// Finished calls release their IDs even if their results have not been collected.
|
||||
// Otherwise the outer task may still need the ID for its abort callback.
|
||||
if dispatch_call_state
|
||||
.terminal_outcome_reached
|
||||
.load(Ordering::Acquire)
|
||||
{
|
||||
drop(dispatch_call_state.invocation.take());
|
||||
}
|
||||
result
|
||||
}
|
||||
.instrument(dispatch_span.clone().or_current()),
|
||||
));
|
||||
|
||||
async move {
|
||||
let completion = async move {
|
||||
let mut tool_call_timing_guard = tool_call_timing_guard;
|
||||
let join_error = |err| {
|
||||
drop(call_state.invocation.take());
|
||||
Self::tool_task_join_error(err)
|
||||
};
|
||||
let mut result = tokio::select! {
|
||||
res = &mut dispatch_handle => res.map_err(Self::tool_task_join_error)?,
|
||||
res = &mut dispatch_handle => res.map_err(join_error)?,
|
||||
_ = cancellation_token.cancelled() => {
|
||||
// Dynamic handlers observe the token and publish their terminal item themselves.
|
||||
if finishes_on_cancellation
|
||||
|| call_state.terminal_outcome_reached.load(Ordering::Acquire)
|
||||
|| dispatch_handle.is_finished()
|
||||
{
|
||||
dispatch_handle.await.map_err(Self::tool_task_join_error)?
|
||||
dispatch_handle.await.map_err(join_error)?
|
||||
} else {
|
||||
let secs = started.elapsed().as_secs_f32().max(0.1);
|
||||
abort_dispatch_span.record("aborted", true);
|
||||
@@ -362,6 +381,7 @@ impl ToolCallRuntime {
|
||||
match dispatch_handle.await {
|
||||
Ok(result) => result,
|
||||
Err(err) if err.is_cancelled() => {
|
||||
let _reservation = call_state.invocation.take();
|
||||
let response = Self::aborted_response(&call, Some(secs));
|
||||
call_trace::result_ready(
|
||||
thread_id,
|
||||
@@ -387,6 +407,7 @@ impl ToolCallRuntime {
|
||||
}
|
||||
},
|
||||
};
|
||||
let _reservation = call_state.invocation.take();
|
||||
// Cancellation outside the handler can return before the registry reports an outcome.
|
||||
if cancellation_token.is_cancelled()
|
||||
&& !call_state
|
||||
@@ -416,7 +437,8 @@ impl ToolCallRuntime {
|
||||
}
|
||||
result
|
||||
}
|
||||
.instrument(dispatch_span.or_current())
|
||||
.instrument(dispatch_span.or_current());
|
||||
Either::Right(completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -711,6 +711,15 @@ impl ToolRegistry {
|
||||
mut invocation: ToolInvocation,
|
||||
call_state: Option<Arc<ToolCallState>>,
|
||||
) -> Result<AnyToolResult, FunctionCallError> {
|
||||
// Standalone dispatch has no outer task to reserve the ID for it.
|
||||
let standalone;
|
||||
let reservation = if let Some(state) = &call_state {
|
||||
&state.invocation
|
||||
} else {
|
||||
standalone = crate::agent::host::InvocationReservation::default();
|
||||
standalone.reserve(&invocation.session, &invocation.turn, &invocation.call_id)?;
|
||||
&standalone
|
||||
};
|
||||
let cancellation_token = invocation.cancellation_token.clone();
|
||||
let tool_name = invocation.tool_name.clone();
|
||||
let call_id_owned = invocation.call_id.clone();
|
||||
@@ -879,6 +888,7 @@ impl ToolRegistry {
|
||||
return Err(fail_before_execution(&invocation, err).await);
|
||||
}
|
||||
|
||||
let invocation_guard = reservation.activate(&invocation);
|
||||
if tool.mcp_server_name().is_none() {
|
||||
notify_tool_start(&invocation, /*mcp_tool*/ None)
|
||||
.or_cancel_tool(&cancellation_token)
|
||||
@@ -978,6 +988,7 @@ impl ToolRegistry {
|
||||
handler_executed: true,
|
||||
},
|
||||
};
|
||||
drop(invocation_guard);
|
||||
notify_tool_finish_if_unclaimed(&invocation, call_state.as_deref(), lifecycle_outcome)
|
||||
.await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user