diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 2bd379252..69e8f66b5 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2870,7 +2870,6 @@ dependencies = [ "codex-plugin", "codex-protocol", "codex-rmcp-client", - "codex-utils-absolute-path", "codex-utils-plugins", "futures", "pretty_assertions", diff --git a/codex-rs/cli/src/responses_cmd.rs b/codex-rs/cli/src/responses_cmd.rs index 6974198ef..012c70d94 100644 --- a/codex-rs/cli/src/responses_cmd.rs +++ b/codex-rs/cli/src/responses_cmd.rs @@ -78,8 +78,9 @@ fn response_event_to_json(event: codex_api::ResponseEvent) -> serde_json::Value codex_api::ResponseEvent::Completed { response_id, token_usage, + end_turn, } => { - let response = match token_usage { + let mut response = match token_usage { Some(token_usage) => json!({ "id": response_id, "usage": { @@ -96,6 +97,9 @@ fn response_event_to_json(event: codex_api::ResponseEvent) -> serde_json::Value }), None => json!({ "id": response_id }), }; + if let Some(end_turn) = end_turn { + response["end_turn"] = json!(end_turn); + } json!({ "type": "response.completed", "response": response }) } codex_api::ResponseEvent::OutputTextDelta(delta) => { @@ -165,6 +169,7 @@ mod tests { reasoning_output_tokens: 3, total_tokens: 17, }), + end_turn: Some(true), }); assert_eq!( completed, @@ -183,6 +188,7 @@ mod tests { }, "total_tokens": 17, }, + "end_turn": true, }, }) ); @@ -190,10 +196,22 @@ mod tests { let completed_without_usage = response_event_to_json(codex_api::ResponseEvent::Completed { response_id: "resp-2".to_string(), token_usage: None, + end_turn: Some(false), }); assert_eq!( completed_without_usage, - json!({"type": "response.completed", "response": {"id": "resp-2"}}) + json!({"type": "response.completed", "response": {"id": "resp-2", "end_turn": false}}) + ); + + let completed_without_usage_or_end_turn = + response_event_to_json(codex_api::ResponseEvent::Completed { + response_id: "resp-3".to_string(), + token_usage: None, + end_turn: None, + }); + assert_eq!( + completed_without_usage_or_end_turn, + json!({"type": "response.completed", "response": {"id": "resp-3"}}) ); } diff --git a/codex-rs/codex-api/src/common.rs b/codex-rs/codex-api/src/common.rs index 6f118d103..4b150b55f 100644 --- a/codex-rs/codex-api/src/common.rs +++ b/codex-rs/codex-api/src/common.rs @@ -81,6 +81,9 @@ pub enum ResponseEvent { Completed { response_id: String, token_usage: Option, + /// Did the model affirmatively end its turn? Some providers do not set this, + /// so we rely on fallback logic when this is `None`. + end_turn: Option, }, OutputTextDelta(String), ToolCallInputDelta { diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index 7b4a4ceab..fb1742463 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -123,6 +123,8 @@ struct ResponseCompleted { id: String, #[serde(default)] usage: Option, + #[serde(default)] + end_turn: Option, } #[derive(Debug, Deserialize)] @@ -382,6 +384,7 @@ pub fn process_responses_event( return Ok(Some(ResponseEvent::Completed { response_id: resp.id, token_usage: resp.usage.map(Into::into), + end_turn: resp.end_turn, })); } Err(err) => { @@ -704,9 +707,11 @@ mod tests { Ok(ResponseEvent::Completed { response_id, token_usage, + end_turn, }) => { assert_eq!(response_id, "resp1"); assert!(token_usage.is_none()); + assert!(end_turn.is_none()); } other => panic!("unexpected third event: {other:?}"), } @@ -843,9 +848,11 @@ mod tests { Ok(ResponseEvent::Completed { response_id, token_usage, + end_turn, }) => { assert_eq!(response_id, "resp1"); assert!(token_usage.is_none()); + assert!(end_turn.is_none()); } other => panic!("unexpected event: {other:?}"), } @@ -1148,7 +1155,8 @@ mod tests { &events[1], ResponseEvent::Completed { response_id, - token_usage: None + token_usage: None, + end_turn: None, } if response_id == "resp-1" ); } @@ -1184,7 +1192,8 @@ mod tests { &events[2], ResponseEvent::Completed { response_id, - token_usage: None + token_usage: None, + end_turn: None, } if response_id == "resp-1" ); } @@ -1218,7 +1227,8 @@ mod tests { &events[1], ResponseEvent::Completed { response_id, - token_usage: None + token_usage: None, + end_turn: None, } if response_id == "resp-1" ); } diff --git a/codex-rs/codex-api/tests/sse_end_to_end.rs b/codex-rs/codex-api/tests/sse_end_to_end.rs index 107c10172..bf880fefc 100644 --- a/codex-rs/codex-api/tests/sse_end_to_end.rs +++ b/codex-rs/codex-api/tests/sse_end_to_end.rs @@ -158,9 +158,11 @@ async fn responses_stream_parses_items_and_completed_end_to_end() -> Result<()> ResponseEvent::Completed { response_id, token_usage, + end_turn, } => { assert_eq!(response_id, "resp1"); assert!(token_usage.is_none()); + assert!(end_turn.is_none()); } other => panic!("unexpected third event: {other:?}"), } diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index cb63ca455..c49e28f20 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -1655,6 +1655,7 @@ where Ok(ResponseEvent::Completed { response_id, token_usage, + end_turn, }) => { if let Some(usage) = &token_usage { session_telemetry.sse_event_completed( @@ -1680,6 +1681,7 @@ where .send(Ok(ResponseEvent::Completed { response_id, token_usage, + end_turn, })) .await .is_err() diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index fe9320b12..2577ec47d 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -2132,6 +2132,7 @@ async fn try_run_sampling_request( ResponseEvent::Completed { response_id: _, token_usage, + end_turn, } => { flush_assistant_text_segments_all( &sess, @@ -2143,7 +2144,9 @@ async fn try_run_sampling_request( sess.update_token_usage_info(&turn_context, token_usage.as_ref()) .await; should_emit_turn_diff = true; - + if let Some(false) = end_turn { + needs_follow_up = true; + } break Ok(SamplingRequestResult { needs_follow_up, last_agent_message,