diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index f97db6e2a0..36136595db 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1057,6 +1057,12 @@ impl Session { } } + /// Track a freshly emitted `ResponseItem` for the active turn. + /// + /// When the model stream produces an item (e.g. a tool call) we register it + /// immediately so abort paths can still flush it into history even if the + /// matching tool future has not completed yet. The returned index lets the + /// caller later patch the slot when the response arrives. pub async fn append_processed_item( &self, sub_id: &str, @@ -1072,6 +1078,14 @@ impl Session { } } + /// Patch a previously queued `ProcessedResponseItem` with the tool + /// response. + /// + /// Tool futures resolve outside of the stream loop. Once a result is + /// available we update the placeholder slot created by + /// [`append_processed_item`] so the turn state reflects the final + /// `ResponseInputItem`. If the turn has already ended (e.g. abort), we skip + /// the update—the abort handler will synthesize the appropriate output. pub async fn update_processed_item_response( &self, sub_id: &str, @@ -2023,6 +2037,132 @@ pub(crate) struct ProcessedResponseItem { pub(crate) response: Option, } +struct ProcessedItemsSummary { + history_items: Vec, + responses: Vec, +} + +fn summarize_processed_items(processed_items: Vec) -> ProcessedItemsSummary { + let mut summary = ProcessedItemsSummary { + history_items: Vec::new(), + responses: Vec::new(), + }; + + for processed_response_item in processed_items { + let ProcessedResponseItem { item, response } = processed_response_item; + match (&item, &response) { + (ResponseItem::Message { role, .. }, None) if role == "assistant" => { + summary.history_items.push(item.clone()); + } + ( + ResponseItem::LocalShellCall { .. }, + Some(ResponseInputItem::FunctionCallOutput { call_id, output }), + ) => { + summary.history_items.push(item.clone()); + summary + .history_items + .push(ResponseItem::FunctionCallOutput { + call_id: call_id.clone(), + output: output.clone(), + }); + } + ( + ResponseItem::FunctionCall { .. }, + Some(ResponseInputItem::FunctionCallOutput { call_id, output }), + ) => { + summary.history_items.push(item.clone()); + summary + .history_items + .push(ResponseItem::FunctionCallOutput { + call_id: call_id.clone(), + output: output.clone(), + }); + } + ( + ResponseItem::CustomToolCall { .. }, + Some(ResponseInputItem::CustomToolCallOutput { call_id, output }), + ) => { + summary.history_items.push(item.clone()); + summary + .history_items + .push(ResponseItem::CustomToolCallOutput { + call_id: call_id.clone(), + output: output.clone(), + }); + } + ( + ResponseItem::FunctionCall { .. }, + Some(ResponseInputItem::McpToolCallOutput { call_id, result }), + ) => { + summary.history_items.push(item.clone()); + let output = match result { + Ok(call_tool_result) => { + convert_call_tool_result_to_function_call_output_payload(call_tool_result) + } + Err(err) => FunctionCallOutputPayload { + content: err.clone(), + success: Some(false), + }, + }; + summary + .history_items + .push(ResponseItem::FunctionCallOutput { + call_id: call_id.clone(), + output, + }); + } + ( + ResponseItem::LocalShellCall { + call_id: Some(call_id), + .. + }, + None, + ) => { + summary.history_items.push(item.clone()); + summary + .history_items + .push(make_aborted_function_call_output(call_id.clone())); + } + (ResponseItem::FunctionCall { call_id, .. }, None) => { + summary.history_items.push(item.clone()); + summary + .history_items + .push(make_aborted_function_call_output(call_id.clone())); + } + (ResponseItem::CustomToolCall { call_id, .. }, None) => { + summary.history_items.push(item.clone()); + summary + .history_items + .push(make_aborted_custom_tool_call_output(call_id.clone())); + } + ( + ResponseItem::Reasoning { + id, + summary: reasoning_summary, + content, + encrypted_content, + }, + None, + ) => { + summary.history_items.push(ResponseItem::Reasoning { + id: id.clone(), + summary: reasoning_summary.clone(), + content: content.clone(), + encrypted_content: encrypted_content.clone(), + }); + } + _ => { + warn!("Unexpected response item: {item:?} with response: {response:?}"); + } + }; + if let Some(response) = response { + summary.responses.push(response); + } + } + + summary +} + #[derive(Debug)] struct TurnRunResult { processed_items: Vec, @@ -3488,128 +3628,3 @@ mod tests { assert!(exec_output.output.contains("hi")); } } -struct ProcessedItemsSummary { - history_items: Vec, - responses: Vec, -} - -fn summarize_processed_items(processed_items: Vec) -> ProcessedItemsSummary { - let mut summary = ProcessedItemsSummary { - history_items: Vec::new(), - responses: Vec::new(), - }; - - for processed_response_item in processed_items { - let ProcessedResponseItem { item, response } = processed_response_item; - match (&item, &response) { - (ResponseItem::Message { role, .. }, None) if role == "assistant" => { - summary.history_items.push(item.clone()); - } - ( - ResponseItem::LocalShellCall { .. }, - Some(ResponseInputItem::FunctionCallOutput { call_id, output }), - ) => { - summary.history_items.push(item.clone()); - summary - .history_items - .push(ResponseItem::FunctionCallOutput { - call_id: call_id.clone(), - output: output.clone(), - }); - } - ( - ResponseItem::FunctionCall { .. }, - Some(ResponseInputItem::FunctionCallOutput { call_id, output }), - ) => { - summary.history_items.push(item.clone()); - summary - .history_items - .push(ResponseItem::FunctionCallOutput { - call_id: call_id.clone(), - output: output.clone(), - }); - } - ( - ResponseItem::CustomToolCall { .. }, - Some(ResponseInputItem::CustomToolCallOutput { call_id, output }), - ) => { - summary.history_items.push(item.clone()); - summary - .history_items - .push(ResponseItem::CustomToolCallOutput { - call_id: call_id.clone(), - output: output.clone(), - }); - } - ( - ResponseItem::FunctionCall { .. }, - Some(ResponseInputItem::McpToolCallOutput { call_id, result }), - ) => { - summary.history_items.push(item.clone()); - let output = match result { - Ok(call_tool_result) => { - convert_call_tool_result_to_function_call_output_payload(call_tool_result) - } - Err(err) => FunctionCallOutputPayload { - content: err.clone(), - success: Some(false), - }, - }; - summary - .history_items - .push(ResponseItem::FunctionCallOutput { - call_id: call_id.clone(), - output, - }); - } - ( - ResponseItem::LocalShellCall { - call_id: Some(call_id), - .. - }, - None, - ) => { - summary.history_items.push(item.clone()); - summary - .history_items - .push(make_aborted_function_call_output(call_id.clone())); - } - (ResponseItem::FunctionCall { call_id, .. }, None) => { - summary.history_items.push(item.clone()); - summary - .history_items - .push(make_aborted_function_call_output(call_id.clone())); - } - (ResponseItem::CustomToolCall { call_id, .. }, None) => { - summary.history_items.push(item.clone()); - summary - .history_items - .push(make_aborted_custom_tool_call_output(call_id.clone())); - } - ( - ResponseItem::Reasoning { - id, - summary: reasoning_summary, - content, - encrypted_content, - }, - None, - ) => { - summary.history_items.push(ResponseItem::Reasoning { - id: id.clone(), - summary: reasoning_summary.clone(), - content: content.clone(), - encrypted_content: encrypted_content.clone(), - }); - } - _ => { - warn!("Unexpected response item: {item:?} with response: {response:?}"); - } - }; - if let Some(response) = response { - summary.responses.push(response); - } - } - - summary -} diff --git a/codex-rs/core/tests/suite/abort_tasks.rs b/codex-rs/core/tests/suite/abort_tasks.rs index cc5650fbb1..67d8659652 100644 --- a/codex-rs/core/tests/suite/abort_tasks.rs +++ b/codex-rs/core/tests/suite/abort_tasks.rs @@ -168,11 +168,8 @@ async fn interrupt_tool_records_history_entries() { if next.get("type").and_then(|v| v.as_str()) == Some("function_call_output") && next.get("call_id").and_then(|v| v.as_str()) == Some(call_id) { - let content_matches = next - .get("output") - .and_then(|o| o.as_str()) - .map(|s| s == "aborted") - .unwrap_or(false); + let content_matches = + next.get("output").and_then(serde_json::Value::as_str) == Some("aborted"); if content_matches { abort_seen = true; break;