Track Code Mode tool call metadata completeness (#41058)

## Why

Executed-tool metadata for a Code Mode cell can span its `exec` and `wait`
outputs. Consumers need to know whether that metadata contains the cell's full
tool call inventory or only a partial record.

## What changed

- Associate recorded tool calls with the originating Code Mode cell across
  `exec` and `wait` outputs.
- Add a host-owned `tool_calls_complete` marker when recording finishes without
  dropped or truncated calls. The marker describes inventory completeness, not
  tool success.
- Preserve the cell metadata through replay and pruning, and clear the
  completeness marker when prompt budgeting makes the record incomplete.

## Testing

Add coverage for multi-wait cells, retries, interruptions, recording limits,
argument truncation, prompt budgeting, and rejection of input-forged metadata.

GitOrigin-RevId: 377014edec06aab42aadab78b8b5dccc0a1c83ba
This commit is contained in:
ningyi-oai authored and copyberry committed 2026-08-27 06:03:29 +00:00
1 parent f1433fc71f
commit d5caceccb1
12 files changed
+808 -93

No files matched your search

+42 -11
View File
@@ -18,8 +18,9 @@ use tracing::Instrument;
use super::ExecContext;
use super::PUBLIC_TOOL_NAME;
use super::call_nested_tool;
use super::submit_nested_tool;
use crate::session::step_context::StepContext;
use crate::tools::ExecutedToolCallRecorder;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::parallel::ToolCallRuntime;
@@ -27,6 +28,7 @@ pub(super) struct CodeModeDispatchBroker {
dispatch_tx: async_channel::Sender<DispatchMessage>,
dispatch_rx: async_channel::Receiver<DispatchMessage>,
dispatch_gates: Arc<Mutex<HashMap<CellId, CellDispatchGate>>>,
executed_tool_calls: Option<Arc<ExecutedToolCallRecorder>>,
}
struct CellDispatchGate {
@@ -36,12 +38,13 @@ struct CellDispatchGate {
}
impl CodeModeDispatchBroker {
pub(super) fn new() -> Self {
pub(super) fn new(executed_tool_calls: Option<Arc<ExecutedToolCallRecorder>>) -> Self {
let (dispatch_tx, dispatch_rx) = async_channel::unbounded();
Self {
dispatch_tx,
dispatch_rx,
dispatch_gates: Arc::new(Mutex::new(HashMap::new())),
executed_tool_calls,
}
}
@@ -76,7 +79,14 @@ impl CodeModeDispatchBroker {
}
pub(super) fn close_cell(&self, cell_id: &CellId) {
remove_dispatch_gate(&self.dispatch_gates, cell_id);
let mut dispatch_gates = self
.dispatch_gates
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
dispatch_gates.remove(cell_id);
if let Some(recorder) = &self.executed_tool_calls {
recorder.finish_cell_recording(cell_id);
}
}
pub(super) fn active_cell_ids(&self) -> Vec<CellId> {
@@ -94,6 +104,11 @@ impl CodeModeDispatchBroker {
step_context: Arc<StepContext>,
tracker: SharedTurnDiffTracker,
) -> CodeModeDispatchWorker {
let track_completeness = exec
.turn
.config
.features
.enabled(codex_features::Feature::ExecutedToolCallMetadata);
let tool_runtime = ToolCallRuntime::new(Arc::clone(&exec.session), step_context, tracker);
let host = Arc::new(CoreTurnHost { exec, tool_runtime });
let dispatch_rx = self.dispatch_rx.clone();
@@ -148,10 +163,26 @@ impl CodeModeDispatchBroker {
continue;
}
let host = Arc::clone(&host);
let dispatch_gates = Arc::clone(&dispatch_gates);
tokio::spawn(async move {
let invocation = host
.invoke_tool(invocation, cancellation_token.clone())
.instrument(span);
let invocation = {
let dispatch_gate = track_completeness.then(|| {
dispatch_gates
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
});
if dispatch_gate.as_ref().is_some_and(|gates| {
cancellation_token.is_cancelled()
|| !gates.contains_key(&cell_id)
}) {
return;
}
// Submission and cell closure share this gate.
span.in_scope(|| {
host.submit_tool(invocation, cancellation_token.clone())
})
.instrument(span)
};
tokio::pin!(invocation);
let response = tokio::select! {
biased;
@@ -324,19 +355,19 @@ struct CoreTurnHost {
}
impl CoreTurnHost {
async fn invoke_tool(
fn submit_tool(
&self,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> Result<JsonValue, String> {
call_nested_tool(
) -> impl std::future::Future<Output = Result<JsonValue, String>> + Send + 'static {
let invocation = submit_nested_tool(
self.exec.clone(),
self.tool_runtime.clone(),
invocation,
cancellation_token,
)
.await
.map_err(|error| error.to_string())
.map_err(|error| error.to_string());
async move { invocation?.await.map_err(|error| error.to_string()) }
}
async fn notify(&self, call_id: String, cell_id: CellId, text: String) -> Result<(), String> {
@@ -88,7 +88,7 @@ impl CodeModeExecuteHandler {
cell_id: cell_id.to_string(),
});
if let Some(executed_tool_calls) = exec.session.services.executed_tool_calls.as_ref() {
executed_tool_calls.register_cell(&cell_id, &call_id);
executed_tool_calls.start_cell(&cell_id, &call_id);
}
let runtime_cell_id = cell_id.to_string();
let code_cell_trace = exec
+18 -14
View File
@@ -30,6 +30,7 @@ use crate::original_image_detail::sanitize_original_image_detail as sanitize_ima
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use crate::tools::ExecutedToolCallRecorder;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolPayload;
@@ -80,8 +81,9 @@ impl CodeModeService {
pub(crate) fn new(
session_provider: Arc<dyn CodeModeSessionProvider>,
config: &CodeModeConfig,
executed_tool_calls: Option<Arc<ExecutedToolCallRecorder>>,
) -> Self {
let dispatch_broker = Arc::new(CodeModeDispatchBroker::new());
let dispatch_broker = Arc::new(CodeModeDispatchBroker::new(executed_tool_calls));
let availability = session_provider.availability();
Self {
session: OnceCell::new(),
@@ -341,12 +343,16 @@ fn truncate_code_mode_result(
truncate_function_output_items_with_policy(&items, policy, estimate_audio_token_count)
}
async fn call_nested_tool(
// Submit synchronously so the recorder sees the call before the cell's dispatch gate closes.
fn submit_nested_tool(
exec: ExecContext,
tool_runtime: ToolCallRuntime,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> Result<JsonValue, FunctionCallError> {
) -> Result<
impl std::future::Future<Output = Result<JsonValue, FunctionCallError>> + Send + 'static,
FunctionCallError,
> {
let CodeModeNestedToolCall {
cell_id,
runtime_tool_call_id,
@@ -380,17 +386,15 @@ async fn call_nested_tool(
call_id: call.call_id.clone(),
cell_id: cell_id.to_string(),
});
let result = tool_runtime
.handle_tool_call_with_source(
call,
ToolCallSource::CodeMode {
cell_id: cell_id.to_string(),
runtime_tool_call_id,
},
cancellation_token,
)
.await?;
Ok(result.code_mode_result())
let result = tool_runtime.handle_tool_call_with_source(
call,
ToolCallSource::CodeMode {
cell_id: cell_id.to_string(),
runtime_tool_call_id,
},
cancellation_token,
);
Ok(async move { Ok(result.await?.code_mode_result()) })
}
fn build_nested_tool_payload(