add turn completion model identity fields

This commit is contained in:
GLENNA ZHANG
2026-06-12 12:26:21 -04:00
parent 387adc6c4b
commit 4b491fd068
35 changed files with 305 additions and 4 deletions
@@ -356,6 +356,8 @@ fn sample_turn_completed_notification(
codex_error_info: Option<codex_app_server_protocol::CodexErrorInfo>,
) -> ServerNotification {
ServerNotification::TurnCompleted(TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: thread_id.to_string(),
turn: Turn {
id: turn_id.to_string(),
+4
View File
@@ -1196,6 +1196,8 @@ mod tests {
fn turn_completed_notification() -> ServerNotification {
ServerNotification::TurnCompleted(codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread".to_string(),
turn: codex_app_server_protocol::Turn {
id: "turn".to_string(),
@@ -2096,6 +2098,8 @@ mod tests {
&InProcessServerEvent::ServerNotification(
codex_app_server_protocol::ServerNotification::TurnCompleted(
codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread".to_string(),
turn: codex_app_server_protocol::Turn {
id: "turn".to_string(),
@@ -4882,6 +4882,20 @@
},
"TurnCompletedNotification": {
"properties": {
"finalModel": {
"description": "Final model used after server-side routing or fallback.",
"type": [
"string",
"null"
]
},
"modelSnapshot": {
"description": "Concrete resolved model snapshot used to execute the turn.",
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
@@ -18777,6 +18777,20 @@
"TurnCompletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"finalModel": {
"description": "Final model used after server-side routing or fallback.",
"type": [
"string",
"null"
]
},
"modelSnapshot": {
"description": "Concrete resolved model snapshot used to execute the turn.",
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
@@ -16594,6 +16594,20 @@
"TurnCompletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"finalModel": {
"description": "Final model used after server-side routing or fallback.",
"type": [
"string",
"null"
]
},
"modelSnapshot": {
"description": "Concrete resolved model snapshot used to execute the turn.",
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
@@ -1719,6 +1719,20 @@
}
},
"properties": {
"finalModel": {
"description": "Final model used after server-side routing or fallback.",
"type": [
"string",
"null"
]
},
"modelSnapshot": {
"description": "Concrete resolved model snapshot used to execute the turn.",
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
@@ -3,4 +3,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Turn } from "./Turn";
export type TurnCompletedNotification = { threadId: string, turn: Turn, };
export type TurnCompletedNotification = { threadId: string, turn: Turn,
/**
* Final model used after server-side routing or fallback.
*/
finalModel: string | null,
/**
* Concrete resolved model snapshot used to execute the turn.
*/
modelSnapshot: string | null, };
@@ -1442,6 +1442,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1504,6 +1506,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1582,6 +1586,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-image".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1933,6 +1939,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2482,6 +2490,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2524,6 +2534,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2580,6 +2592,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2622,6 +2636,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2810,6 +2826,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2832,6 +2850,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2844,6 +2864,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2882,6 +2904,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2944,6 +2968,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-compact".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -3206,6 +3232,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -3271,6 +3299,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -3326,6 +3356,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -3375,6 +3407,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -373,6 +373,10 @@ pub struct Usage {
pub struct TurnCompletedNotification {
pub thread_id: String,
pub turn: Turn,
/// Final model used after server-side routing or fallback.
pub final_model: Option<String>,
/// Concrete resolved model snapshot used to execute the turn.
pub model_snapshot: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+1 -1
View File
@@ -1293,7 +1293,7 @@ Because audio is intentionally separate from `ThreadItem`, clients can opt out o
The app-server streams JSON-RPC notifications while a turn is running. Each turn emits `turn/started` when it begins running and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`.
- `turn/started` — `{ turn }` with the turn id, empty `items`, and `status: "inProgress"`.
- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`.
- `turn/completed` — `{ turn, finalModel, modelSnapshot }` where `turn.status` is `completed`, `interrupted`, or `failed`; execution identity fields are `null` when the Responses API does not report them, and failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`.
- `turn/diff/updated` — `{ threadId, turnId, diff }` represents the up-to-date snapshot of the turn-level unified diff, emitted after every FileChange item. `diff` is the latest aggregated unified diff across every file change in the turn. UIs can render this to show the full "what changed" view without stitching individual `fileChange` items.
- `turn/plan/updated` — `{ turnId, explanation?, plan }` whenever the agent shares or changes its plan; each `plan` entry is `{ step, status }` with `status` in `pending`, `inProgress`, or `completed`.
- `model/rerouted` — `{ threadId, turnId, fromModel, toModel, reason }` when the backend reroutes a request to a different model (for example, due to high-risk cyber safety checks).
@@ -1318,6 +1318,8 @@ struct TurnCompletionMetadata {
started_at: Option<i64>,
completed_at: Option<i64>,
duration_ms: Option<i64>,
final_model: Option<String>,
model_snapshot: Option<String>,
}
async fn emit_turn_completed_with_status(
@@ -1338,6 +1340,8 @@ async fn emit_turn_completed_with_status(
completed_at: turn_completion_metadata.completed_at,
duration_ms: turn_completion_metadata.duration_ms,
},
final_model: turn_completion_metadata.final_model,
model_snapshot: turn_completion_metadata.model_snapshot,
};
outgoing
.send_server_notification(ServerNotification::TurnCompleted(notification))
@@ -1521,6 +1525,8 @@ async fn handle_turn_complete(
started_at: turn_summary.started_at,
completed_at: turn_complete_event.completed_at,
duration_ms: turn_complete_event.duration_ms,
final_model: turn_complete_event.final_model,
model_snapshot: turn_complete_event.model_snapshot,
},
outgoing,
)
@@ -1545,6 +1551,8 @@ async fn handle_turn_interrupted(
started_at: turn_summary.started_at,
completed_at: turn_aborted_event.completed_at,
duration_ms: turn_aborted_event.duration_ms,
final_model: None,
model_snapshot: None,
},
outgoing,
)
@@ -2256,6 +2264,8 @@ mod tests {
TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: Some(TEST_TURN_COMPLETED_AT),
duration_ms: Some(TEST_TURN_DURATION_MS),
time_to_first_token_ms: None,
@@ -3423,6 +3433,11 @@ mod tests {
ThreadId::new(),
);
let thread_state = new_thread_state();
let turn_complete = TurnCompleteEvent {
final_model: Some("test-final-model".to_string()),
model_snapshot: Some("test-model-snapshot".to_string()),
..turn_complete_event(&event_turn_id)
};
{
let mut state = thread_state.lock().await;
state.track_current_turn_event(
@@ -3437,14 +3452,14 @@ mod tests {
);
state.track_current_turn_event(
&event_turn_id,
&EventMsg::TurnComplete(turn_complete_event(&event_turn_id)),
&EventMsg::TurnComplete(turn_complete.clone()),
);
}
handle_turn_complete(
conversation_id,
event_turn_id.clone(),
turn_complete_event(&event_turn_id),
turn_complete,
&outgoing,
&thread_state,
)
@@ -3461,6 +3476,8 @@ mod tests {
assert_eq!(n.turn.started_at, Some(42));
assert_eq!(n.turn.completed_at, Some(TEST_TURN_COMPLETED_AT));
assert_eq!(n.turn.duration_ms, Some(TEST_TURN_DURATION_MS));
assert_eq!(n.final_model.as_deref(), Some("test-final-model"));
assert_eq!(n.model_snapshot.as_deref(), Some("test-model-snapshot"));
}
other => bail!("unexpected message: {other:?}"),
}
@@ -3511,6 +3528,8 @@ mod tests {
assert_eq!(n.turn.error, None);
assert_eq!(n.turn.completed_at, Some(TEST_TURN_COMPLETED_AT));
assert_eq!(n.turn.duration_ms, Some(TEST_TURN_DURATION_MS));
assert_eq!(n.final_model, None);
assert_eq!(n.model_snapshot, None);
}
other => bail!("unexpected message: {other:?}"),
}
+2
View File
@@ -894,6 +894,8 @@ mod tests {
completed_at: Some(0),
duration_ms: None,
},
final_model: None,
model_snapshot: None,
})
));
assert!(server_notification_requires_delivery(
@@ -160,6 +160,8 @@ async fn mark_thread_completed(thread: &CodexThread) {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn.sub_id.clone(),
last_agent_message: Some("done".to_string()),
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
+6
View File
@@ -305,6 +305,8 @@ async fn on_event_updates_status_from_task_complete() {
let status = agent_status_from_event(&EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("done".to_string()),
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1896,6 +1898,8 @@ async fn multi_agent_v2_completion_ignores_dead_direct_parent() {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: tester_turn.sub_id.clone(),
last_agent_message: Some("done".to_string()),
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1983,6 +1987,8 @@ async fn multi_agent_v2_completion_queues_message_for_direct_parent() {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: tester_turn.sub_id.clone(),
last_agent_message: Some("done".to_string()),
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1111,6 +1111,8 @@ mod tests {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: last_agent_message.map(str::to_string),
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms,
+2
View File
@@ -1980,6 +1980,8 @@ async fn guardian_reused_trunk_ignores_stale_prior_turn_completion() -> anyhow::
"{\"risk_level\":\"high\",\"user_authorization\":\"low\",\"outcome\":\"deny\",\"rationale\":\"stale guardian rationale\"}"
.to_string(),
),
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: Some(1),
@@ -147,6 +147,8 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
codex_protocol::protocol::TurnCompleteEvent {
turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -218,6 +220,8 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
codex_protocol::protocol::TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -249,6 +253,8 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
codex_protocol::protocol::TurnCompleteEvent {
turn_id: rolled_back_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -322,6 +328,8 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc
codex_protocol::protocol::TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -418,6 +426,8 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
codex_protocol::protocol::TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -448,6 +458,8 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
codex_protocol::protocol::TurnCompleteEvent {
turn_id: second_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -467,6 +479,8 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
codex_protocol::protocol::TurnCompleteEvent {
turn_id: standalone_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -543,6 +557,8 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -564,6 +580,8 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id: assistant_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -635,6 +653,8 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding
codex_protocol::protocol::TurnCompleteEvent {
turn_id: only_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -688,6 +708,8 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id: user_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -707,6 +729,8 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id: standalone_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -764,6 +788,8 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -931,6 +957,8 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
codex_protocol::protocol::TurnCompleteEvent {
turn_id: current_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1005,6 +1033,8 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1110,6 +1140,8 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1225,6 +1257,8 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1262,6 +1296,8 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
codex_protocol::protocol::TurnCompleteEvent {
turn_id: current_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1348,6 +1384,8 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -1509,6 +1547,8 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
+18
View File
@@ -2595,6 +2595,8 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2793,6 +2795,8 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2822,6 +2826,8 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: rolled_back_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2906,6 +2912,8 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2927,6 +2935,8 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: compact_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2958,6 +2968,8 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: rolled_back_turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -3016,6 +3028,8 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -3043,6 +3057,8 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-2".to_string(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -3070,6 +3086,8 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-3".to_string(),
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
+2
View File
@@ -742,6 +742,8 @@ impl Session {
let event = EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_context.sub_id.clone(),
last_agent_message,
final_model: None,
model_snapshot: None,
completed_at,
duration_ms,
time_to_first_token_ms,
@@ -1557,6 +1557,8 @@ async fn multi_agent_v2_list_agents_returns_completed_status_without_encrypted_s
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: child_turn.sub_id.clone(),
last_agent_message: Some("done".to_string()),
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2008,6 +2010,8 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: first_turn.sub_id.clone(),
last_agent_message: Some("first done".to_string()),
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -2037,6 +2041,8 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: second_turn.sub_id.clone(),
last_agent_message: Some("second done".to_string()),
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -69,6 +69,8 @@ fn resume_history(
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at: None,
duration_ms: None,
time_to_first_token_ms: None,
@@ -307,6 +307,8 @@ fn turn_completed_recovers_final_message_from_turn_items() {
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -355,6 +357,8 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() {
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -404,6 +408,8 @@ fn turn_completed_preserves_streamed_final_message_when_turn_items_are_empty() {
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -448,6 +454,8 @@ fn turn_failed_clears_stale_final_message() {
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -493,6 +501,8 @@ fn turn_interrupted_clears_stale_final_message() {
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -30,6 +30,8 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
let status = processor.process_server_notification(ServerNotification::TurnCompleted(
codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: codex_app_server_protocol::Turn {
id: "turn-1".to_string(),
+2
View File
@@ -371,6 +371,8 @@ fn turn_items_for_thread_returns_matching_turn_items() {
fn should_backfill_turn_completed_items_skips_ephemeral_threads() {
let notification =
ServerNotification::TurnCompleted(codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: codex_app_server_protocol::Turn {
id: "turn-1".to_string(),
@@ -1103,6 +1103,8 @@ fn plan_update_emits_started_then_updated_then_completed() {
let completed = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -1163,6 +1165,8 @@ fn plan_update_after_completion_starts_new_todo_list_with_new_id() {
));
let _ = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -1246,6 +1250,8 @@ fn token_usage_update_is_emitted_on_turn_completion() {
let completed = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -1281,6 +1287,8 @@ fn turn_completion_recovers_final_message_from_turn_items() {
let completed = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -1354,6 +1362,8 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
let completed = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -1422,6 +1432,8 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
let completed = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -1472,6 +1484,8 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
let completed = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -1521,6 +1535,8 @@ fn failed_turn_clears_stale_final_message() {
let collected = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -1549,6 +1565,8 @@ fn turn_completion_falls_back_to_final_plan_text() {
let completed = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -1604,6 +1622,8 @@ fn turn_failure_prefers_structured_error_message() {
let failed = processor.collect_thread_events(ServerNotification::TurnCompleted(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: "turn-1".to_string(),
@@ -167,6 +167,8 @@ fn turn_complete_item(turn_id: String, completed_at: Option<i64>) -> RolloutItem
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id,
last_agent_message: None,
final_model: None,
model_snapshot: None,
completed_at,
duration_ms: None,
time_to_first_token_ms: None,
+36
View File
@@ -1876,6 +1876,16 @@ pub struct ContextCompactedEvent;
pub struct TurnCompleteEvent {
pub turn_id: String,
pub last_agent_message: Option<String>,
/// Final model used by the terminal successful sampling request, after routing or fallback.
#[serde(default, skip_serializing, skip_deserializing)]
#[schemars(skip)]
#[ts(skip)]
pub final_model: Option<String>,
/// Concrete model snapshot used by the terminal successful sampling request.
#[serde(default, skip_serializing, skip_deserializing)]
#[schemars(skip)]
#[ts(skip)]
pub model_snapshot: Option<String>,
/// Unix timestamp (in seconds) when the turn completed.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null", optional)]
@@ -5307,6 +5317,32 @@ mod tests {
Ok(())
}
#[test]
fn turn_complete_execution_identity_is_not_serialized() -> Result<()> {
let event = EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("done".to_string()),
final_model: Some("test-final-model".to_string()),
model_snapshot: Some("test-model-snapshot".to_string()),
completed_at: Some(1_717_171_717),
duration_ms: Some(1_234),
time_to_first_token_ms: Some(321),
});
assert_eq!(
serde_json::to_value(event)?,
json!({
"type": "task_complete",
"turn_id": "turn-1",
"last_agent_message": "done",
"completed_at": 1_717_171_717,
"duration_ms": 1_234,
"time_to_first_token_ms": 321,
})
);
Ok(())
}
#[test]
fn deserialize_legacy_session_configured_event_uses_sandbox_policy() -> Result<()> {
let cwd = test_path_buf("/home/user/project");
@@ -664,6 +664,8 @@ mod tests {
fn turn_completed(turn_id: &str) -> ServerNotification {
ServerNotification::TurnCompleted(TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: Turn {
id: turn_id.to_string(),
+2
View File
@@ -4403,6 +4403,8 @@ fn turn_completed_notification(
duration_ms: Some(1),
..test_turn(turn_id, status, Vec::new())
},
final_model: None,
model_snapshot: None,
})
}
+2
View File
@@ -416,6 +416,8 @@ mod tests {
duration_ms: Some(1),
..test_turn(turn_id, status, Vec::new())
},
final_model: None,
model_snapshot: None,
})
}
+2
View File
@@ -36,6 +36,8 @@ impl ChatWidget {
) {
self.handle_turn_completed_notification(
TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: self.thread_id.map(|id| id.to_string()).unwrap_or_default(),
turn: Turn {
id: turn_id,
@@ -346,6 +346,8 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
chat.handle_server_notification(
ServerNotification::TurnCompleted(TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
@@ -766,6 +768,8 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() {
chat.handle_server_notification(
ServerNotification::TurnCompleted(TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
@@ -996,6 +996,8 @@ pub(super) fn handle_turn_completed(
) {
chat.handle_server_notification(
ServerNotification::TurnCompleted(TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
turn: app_server_turn(
turn_id,
@@ -1011,6 +1013,8 @@ pub(super) fn handle_turn_completed(
pub(super) fn handle_turn_interrupted(chat: &mut ChatWidget, turn_id: &str) {
chat.handle_server_notification(
ServerNotification::TurnCompleted(TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
turn: app_server_turn(
turn_id,
@@ -1172,6 +1172,8 @@ async fn submit_user_message_queues_while_compaction_turn_is_running() {
chat.handle_server_notification(
ServerNotification::TurnCompleted(TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: thread_id.to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
@@ -1271,6 +1271,8 @@ async fn interrupted_turn_after_goal_budget_limited_uses_budget_message_snapshot
chat.handle_server_notification(
codex_app_server_protocol::ServerNotification::TurnCompleted(
codex_app_server_protocol::TurnCompletedNotification {
final_model: None,
model_snapshot: None,
thread_id: "thread-1".to_string(),
turn: codex_app_server_protocol::Turn {
id: "turn-1".to_string(),