diff --git a/codex-rs/analytics/src/tests/suite/turns/events_tests.rs b/codex-rs/analytics/src/tests/suite/turns/events_tests.rs index fcf8346080..5d7bf5d0b4 100644 --- a/codex-rs/analytics/src/tests/suite/turns/events_tests.rs +++ b/codex-rs/analytics/src/tests/suite/turns/events_tests.rs @@ -525,12 +525,16 @@ async fn turn_event_counts_completed_tool_items() { agents_states: Default::default(), }, ThreadItem::SubAgentActivity { + model: None, + reasoning_effort: None, id: "sub-agent-activity-1".to_string(), kind: SubAgentActivityKind::Interacted, agent_thread_id: "thread-child".to_string(), agent_path: "/root/child".to_string(), }, ThreadItem::SubAgentActivity { + model: None, + reasoning_effort: None, id: "sub-agent-activity-completed".to_string(), kind: SubAgentActivityKind::Completed, agent_thread_id: "thread-child".to_string(), diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index b57a1ca471..51707eddfd 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -5233,6 +5233,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 6260a20dfd..872cc47475 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -22598,6 +22598,24 @@ "kind": { "$ref": "#/definitions/v2/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 637d715a00..3f24ede0d6 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -20312,6 +20312,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index beb79b2311..31e915e4e8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -1392,6 +1392,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index 92ed6ce659..0ea615c48c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -1392,6 +1392,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 3934100532..f1f9c0dcb8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -1595,6 +1595,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 116c703cc2..b1aff21738 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -2209,6 +2209,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json index 80fb6a9501..5bb7c64b97 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json @@ -1392,6 +1392,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 03f1aa2c97..ab43908fbe 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -2000,6 +2000,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index 44cdf7f557..fe79128b0a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -2000,6 +2000,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index e20bfa1c75..d824e57036 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -2000,6 +2000,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 9c03d89695..b7e1f4cbfa 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -2261,6 +2261,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json index f51ce46103..7a8a97b9d9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json @@ -2000,6 +2000,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index ed56fcde2a..00d698b329 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -2209,6 +2209,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 58d032d99c..b0cee0b30a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -2000,6 +2000,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json index 8e5a334fb1..ea56799c41 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json @@ -1595,6 +1595,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index de9b859959..e2039c8c32 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -2000,6 +2000,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index eb4ac85aa6..a0c543c01f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -1595,6 +1595,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 0d6620adfb..f2f5c903f3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -1595,6 +1595,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index ab6c2ea72c..6f5969c8e9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -1595,6 +1595,24 @@ "kind": { "$ref": "#/definitions/SubAgentActivityKind" }, + "model": { + "description": "Resolved model at sub-agent creation; absent from older records and other activities.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Resolved reasoning effort at sub-agent creation, when known." + }, "type": { "enum": [ "subAgentActivity" diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index f568cd09ab..fd63249fb4 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 0c2346600a..6d7d75bb7a 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index 31f1430ebe..1f132df8da 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -122,4 +122,12 @@ reasoningEffort: ReasoningEffort | null, /** * Last known status of the target agents, when available. */ -agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep" } & SleepItem | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; +agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", +/** + * Resolved model at sub-agent creation; absent from older records and other activities. + */ +model: string | null, +/** + * Resolved reasoning effort at sub-agent creation, when known. + */ +reasoningEffort: ReasoningEffort | null, id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep" } & SleepItem | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; diff --git a/codex-rs/app-server-protocol/src/protocol/event_mapping.rs b/codex-rs/app-server-protocol/src/protocol/event_mapping.rs index b32b15272b..e0fcfb6759 100644 --- a/codex-rs/app-server-protocol/src/protocol/event_mapping.rs +++ b/codex-rs/app-server-protocol/src/protocol/event_mapping.rs @@ -183,6 +183,8 @@ pub fn item_event_to_server_notification( } EventMsg::SubAgentActivity(activity) => { let item = ThreadItem::SubAgentActivity { + model: activity.model, + reasoning_effort: activity.reasoning_effort, id: activity.event_id, kind: activity.kind.into(), agent_thread_id: activity.agent_thread_id.to_string(), diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 1559004762..18b87d9ce4 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -1022,6 +1022,8 @@ impl ThreadHistoryBuilder { payload: &codex_protocol::protocol::SubAgentActivityEvent, ) { self.upsert_item_in_current_turn(ThreadItem::SubAgentActivity { + model: payload.model.clone(), + reasoning_effort: payload.reasoning_effort.clone(), id: payload.event_id.clone(), kind: payload.kind.into(), agent_thread_id: payload.agent_thread_id.to_string(), @@ -5125,56 +5127,96 @@ mod tests { #[test] fn completed_sub_agent_activity_updates_completed_parent_turn() { + use codex_protocol::protocol::HasLegacyEvent; + let child_thread_id = ThreadId::new(); let child_path = codex_protocol::AgentPath::root() .join("worker") .expect("worker path"); - let items = vec![ - RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { - turn_attribution: None, - turn_id: "turn-a".into(), - root_turn_id: None, - trace_id: None, - started_at: None, - model_context_window: None, - collaboration_mode_kind: Default::default(), - })), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - root_turn_id: None, - turn_id: "turn-a".into(), - started_at: None, - last_agent_message: None, - error: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })), - RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent { - thread_id: ThreadId::new(), - turn_id: "turn-a".into(), - item: CoreTurnItem::SubAgentActivity(CoreSubAgentActivityItem { - id: "child-turn-completed".into(), - kind: CoreSubAgentActivityKind::Completed, - agent_thread_id: child_thread_id, - agent_path: child_path, - }), - started_at_ms: None, - completed_at_ms: 0, - })), - ]; - - let turns = build_turns_from_rollout_items(&items); - - assert_eq!(turns.len(), 1); + let spawn = ItemCompletedEvent { + thread_id: ThreadId::new(), + turn_id: "turn-a".into(), + item: CoreTurnItem::SubAgentActivity(CoreSubAgentActivityItem { + model: Some("gpt-5".into()), + reasoning_effort: Some(codex_protocol::openai_models::ReasoningEffort::High), + id: "spawn-child".into(), + kind: CoreSubAgentActivityKind::Started, + agent_thread_id: child_thread_id, + agent_path: child_path.clone(), + }), + started_at_ms: Some(1), + completed_at_ms: 2, + }; + let expected_spawn = ThreadItem::from(spawn.item.clone()); + let legacy_spawn = spawn + .as_legacy_events(/*show_raw_agent_reasoning*/ false) + .pop() + .expect("legacy spawn activity"); + let crate::ServerNotification::ItemCompleted(notification) = + crate::item_event_to_server_notification(legacy_spawn.clone(), "parent", "turn-a") + else { + panic!("expected item completed"); + }; assert_eq!( - turns[0].items, - vec![ThreadItem::SubAgentActivity { - id: "child-turn-completed".into(), - kind: crate::protocol::v2::SubAgentActivityKind::Completed, - agent_thread_id: child_thread_id.to_string(), - agent_path: "/root/worker".into(), - }] + (notification.item, notification.completed_at_ms), + (expected_spawn.clone(), 2) ); + for spawn in [EventMsg::ItemCompleted(spawn), legacy_spawn] { + let items = vec![ + RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { + turn_attribution: None, + turn_id: "turn-a".into(), + root_turn_id: None, + trace_id: None, + started_at: None, + model_context_window: None, + collaboration_mode_kind: Default::default(), + })), + RolloutItem::EventMsg(spawn), + RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { + root_turn_id: None, + turn_id: "turn-a".into(), + started_at: None, + last_agent_message: None, + error: None, + completed_at: None, + duration_ms: None, + time_to_first_token_ms: None, + })), + RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent { + thread_id: ThreadId::new(), + turn_id: "turn-a".into(), + item: CoreTurnItem::SubAgentActivity(CoreSubAgentActivityItem { + model: None, + reasoning_effort: None, + id: "child-turn-completed".into(), + kind: CoreSubAgentActivityKind::Completed, + agent_thread_id: child_thread_id, + agent_path: child_path.clone(), + }), + started_at_ms: None, + completed_at_ms: 0, + })), + ]; + + let turns = build_turns_from_rollout_items(&items); + + assert_eq!(turns.len(), 1); + assert_eq!( + turns[0].items, + vec![ + expected_spawn.clone(), + ThreadItem::SubAgentActivity { + model: None, + reasoning_effort: None, + id: "child-turn-completed".into(), + kind: crate::protocol::v2::SubAgentActivityKind::Completed, + agent_thread_id: child_thread_id.to_string(), + agent_path: "/root/worker".into(), + }, + ] + ); + } } #[test] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index 04f68a3351..65d2838270 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -395,6 +395,10 @@ pub enum ThreadItem { #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] SubAgentActivity { + /// Resolved model at sub-agent creation; absent from older records and other activities. + model: Option, + /// Resolved reasoning effort at sub-agent creation, when known. + reasoning_effort: Option, id: String, kind: SubAgentActivityKind, agent_thread_id: String, @@ -978,6 +982,8 @@ impl From for ThreadItem { .collect(), }, CoreTurnItem::SubAgentActivity(activity) => ThreadItem::SubAgentActivity { + model: activity.model, + reasoning_effort: activity.reasoning_effort, id: activity.id, kind: activity.kind.into(), agent_thread_id: activity.agent_thread_id.to_string(), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 598c9b71d1..404cba091f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -33,6 +33,7 @@ use codex_protocol::models::ImageReference as CoreImageReference; use codex_protocol::models::MessagePhase; use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; use codex_protocol::models::WebSearchAction as CoreWebSearchAction; +use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMode; use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry; @@ -3446,8 +3447,10 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { ); let sub_agent_activity_item = TurnItem::SubAgentActivity(SubAgentActivityItem { + model: Some("gpt-5".into()), + reasoning_effort: Some(ReasoningEffort::High), id: "activity-1".to_string(), - kind: CoreSubAgentActivityKind::Completed, + kind: CoreSubAgentActivityKind::Started, agent_thread_id: receiver_thread_id, agent_path: codex_protocol::AgentPath::root() .join("worker") @@ -3457,13 +3460,35 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { assert_eq!( ThreadItem::from(sub_agent_activity_item), ThreadItem::SubAgentActivity { + model: Some("gpt-5".into()), + reasoning_effort: Some(ReasoningEffort::High), id: "activity-1".to_string(), - kind: SubAgentActivityKind::Completed, + kind: SubAgentActivityKind::Started, agent_thread_id: receiver_thread_id.to_string(), agent_path: "/root/worker".to_string(), } ); + let old_activity: SubAgentActivityItem = serde_json::from_value(json!({ + "id": "old-activity", + "kind": "started", + "agent_thread_id": receiver_thread_id, + "agent_path": "/root/worker", + })) + .unwrap(); + let old_wire_activity: ThreadItem = serde_json::from_value(json!({ + "type": "subAgentActivity", + "id": "old-activity", + "kind": "started", + "agentThreadId": receiver_thread_id, + "agentPath": "/root/worker", + })) + .unwrap(); + assert_eq!( + ThreadItem::from(TurnItem::SubAgentActivity(old_activity)), + old_wire_activity + ); + let search_item = TurnItem::WebSearch(CoreWebSearchItem { id: "search-1".to_string(), query: "docs".to_string(), diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 0e0c31fbca..f632857401 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -3252,6 +3252,8 @@ mod tests { thread_id: conversation_id, turn_id: "turn-1".to_string(), item: CoreTurnItem::SubAgentActivity(SubAgentActivityItem { + model: None, + reasoning_effort: None, id: "activity-1".to_string(), kind: SubAgentActivityKind::Interrupted, agent_thread_id: child_thread_id, @@ -3286,6 +3288,8 @@ mod tests { payload, ItemCompletedNotification { item: ThreadItem::SubAgentActivity { + model: None, + reasoning_effort: None, id: "activity-1".to_string(), kind: codex_app_server_protocol::SubAgentActivityKind::Interrupted, agent_thread_id: child_thread_id_string, diff --git a/codex-rs/core/src/agent/api.rs b/codex-rs/core/src/agent/api.rs index d9c894832c..1a498228b6 100644 --- a/codex-rs/core/src/agent/api.rs +++ b/codex-rs/core/src/agent/api.rs @@ -53,6 +53,8 @@ pub trait AgentControl: Send + Sync { ) -> BoxFuture<'a, Result>; /// Start a child and accept its initial input, returning its effective settings. + /// V2 snapshots resolve effort against captured startup metadata without changing the + /// child's configured effort (for example, Ultra still enables proactive behavior). fn spawn( &self, request: SpawnRequest, diff --git a/codex-rs/core/src/agent/control/completion.rs b/codex-rs/core/src/agent/control/completion.rs index 48a468ea21..587f4cdaeb 100644 --- a/codex-rs/core/src/agent/control/completion.rs +++ b/codex-rs/core/src/agent/control/completion.rs @@ -73,6 +73,8 @@ impl LocalAgentControl { initiating_thread_id, parent_turn_id, SubAgentActivityItem { + model: None, + reasoning_effort: None, id: format!("subagent-completed-{}", outcome.turn_id), kind: SubAgentActivityKind::Completed, agent_thread_id: outcome.thread_id, diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs index c5a17767da..7492562e33 100644 --- a/codex-rs/core/src/agent/control/spawn.rs +++ b/codex-rs/core/src/agent/control/spawn.rs @@ -900,6 +900,25 @@ impl LocalAgentControl { } let durability_wait = durability_wait_started_at.elapsed(); + // Capture startup settings before the child can publish metadata for a later turn. + let spawn_config = if multi_agent_version == MultiAgentVersion::V2 { + let mut config = new_thread.thread.config_snapshot().await; + let model_info = new_thread + .thread + .thread_extension_data() + .get::() + .ok_or_else(|| { + CodexErr::Fatal("spawned thread is missing model metadata".to_string()) + })?; + config.reasoning_effort = config + .reasoning_effort + .or_else(|| model_info.default_reasoning_level.clone()) + .map(|effort| model_info.resolve_reasoning_effort(effort)); + Some(config) + } else { + None + }; + let start_options = TurnStartOptions { parent_turn_id: options.parent_turn_id, turn_trigger: options.turn_trigger, @@ -956,7 +975,10 @@ impl LocalAgentControl { metadata: agent_metadata, status: self.get_status(new_thread.thread_id).await, }; - let config = new_thread.thread.config_snapshot().await; + let config = match spawn_config { + Some(config) => config, + None => new_thread.thread.config_snapshot().await, + }; let session_telemetry = new_thread .thread .session_telemetry() diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index efe629928c..012df7aab8 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -68,6 +68,7 @@ use codex_protocol::models::InternalChatMessageMetadataPassthrough; use codex_protocol::models::MessagePhase; use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; +use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::ErrorEvent; @@ -1863,6 +1864,62 @@ async fn spawn_agent_creates_thread_and_sends_prompt() { wait_for_recorded_user_message(thread.as_ref(), "spawned").await; } +#[test_case::test_case(None, ReasoningEffort::Medium; "model default")] +#[test_case::test_case(Some(ReasoningEffort::Ultra), ReasoningEffort::XHigh; "resolved ultra")] +#[tokio::test] +async fn v2_spawn_resolves_reported_effort_without_changing_child_selection( + selected_effort: Option, + reported_effort: ReasoningEffort, +) { + let (home, mut config) = test_config().await; + config + .features + .enable(Feature::MultiAgentV2) + .expect("enable v2"); + config.model_reasoning_effort = selected_effort.clone(); + let harness = AgentControlHarness::new_with_config(home, config).await; + let (parent_id, parent) = harness.start_thread().await; + let source = thread_spawn_source( + parent_id, + &parent.session_source, + next_thread_spawn_depth(&parent.session_source), + /*agent_role*/ None, + Some("worker".to_string()), + ) + .expect("child source"); + let (agent, snapshot) = harness + .control + .spawn(SpawnRequest { + caller: parent_id, + config: harness.config.clone(), + input: AgentInput::Message { + message: AgentMessage::Plaintext("child task".to_string()), + mode: MessageDeliveryMode::TriggerTurn, + }, + source, + options: SpawnAgentOptions { + parent_thread_id: Some(parent_id), + ..Default::default() + }, + }) + .await + .expect("spawn child"); + let child = harness + .manager + .get_thread(agent.thread_id) + .await + .expect("child is registered"); + assert_eq!( + ( + snapshot.reasoning_effort, + child.config_snapshot().await.reasoning_effort, + ), + (Some(reported_effort), selected_effort), + ); + child.shutdown_and_wait().await.expect("shutdown child"); + parent.shutdown_and_wait().await.expect("shutdown parent"); +} + #[tokio::test] async fn pending_environment_failure_reaches_child_and_grandchild() { let (home, mut config) = test_config().await; diff --git a/codex-rs/core/src/realtime_history_tests.rs b/codex-rs/core/src/realtime_history_tests.rs index 18aea1636a..92e2b7c61a 100644 --- a/codex-rs/core/src/realtime_history_tests.rs +++ b/codex-rs/core/src/realtime_history_tests.rs @@ -258,6 +258,8 @@ fn promotes_backing_agent_artifacts_once_without_a_client_request() { started_at_ms: 0, }); let subagent = completed_item(TurnItem::SubAgentActivity(SubAgentActivityItem { + model: None, + reasoning_effort: None, id: "subagent-1".to_string(), kind: SubAgentActivityKind::Started, agent_thread_id: ThreadId::new(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 01f1e7d651..598c915886 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2597,14 +2597,20 @@ async fn item_completion_without_a_start_uses_completion_timestamp() { async fn subagent_activity_emits_matching_start_and_completion() { let (session, turn_context, rx) = make_session_and_context_with_rx().await; let item = codex_protocol::items::SubAgentActivityItem { + model: Some("requested-model".to_string()), + reasoning_effort: Some(codex_protocol::openai_models::ReasoningEffort::High), id: "activity-1".to_string(), kind: codex_protocol::protocol::SubAgentActivityKind::Started, agent_thread_id: ThreadId::new(), agent_path: AgentPath::root(), }; - crate::tools::handlers::multi_agents_v2::emit_sub_agent_activity(&session, &turn_context, item) - .await; + crate::tools::handlers::multi_agents_v2::emit_sub_agent_activity( + &session, + &turn_context, + item.clone(), + ) + .await; let EventMsg::ItemStarted(started) = rx.recv().await.expect("started item event").msg else { panic!("expected started item event"); @@ -2613,6 +2619,14 @@ async fn subagent_activity_emits_matching_start_and_completion() { else { panic!("expected completed item event"); }; + let TurnItem::SubAgentActivity(started_item) = started.item else { + panic!("expected started sub-agent activity"); + }; + let TurnItem::SubAgentActivity(completed_item) = completed.item else { + panic!("expected completed sub-agent activity"); + }; + assert_eq!(started_item, item); + assert_eq!(completed_item, item); assert_eq!(completed.started_at_ms, Some(started.started_at_ms)); } diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/interrupt_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/interrupt_agent.rs index b200c3efb4..e78b3629bd 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/interrupt_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/interrupt_agent.rs @@ -63,6 +63,8 @@ async fn handle_interrupt_agent( &session, &turn, SubAgentActivityItem { + model: None, + reasoning_effort: None, id: call_id, agent_thread_id: agent_id, agent_path, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs index 26d329fa0e..8392960121 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs @@ -87,6 +87,8 @@ pub(super) async fn handle_message_string_tool( &session, &turn, SubAgentActivityItem { + model: None, + reasoning_effort: None, id: call_id, agent_thread_id: receiver_thread_id, agent_path: receiver_agent_path, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 218fbdc254..da7325bccb 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -228,6 +228,8 @@ async fn handle_spawn_agent( &session, turn, SubAgentActivityItem { + model: Some(agent_snapshot.model.clone()), + reasoning_effort: agent_snapshot.reasoning_effort.clone(), id: call_id, agent_thread_id: new_thread_id, agent_path: new_agent_path.clone(), diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 5280af100f..7f56f82912 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -2211,14 +2211,20 @@ async fn spawned_multi_agent_v2_child_inherits_parent_developer_context() -> Res Ok(()) } -#[test_case(None, false; "encrypted")] -#[test_case(None, true; "plaintext")] -#[test_case(Some("gpt-5.6-luna"), false; "luna encrypted leaf")] -#[test_case(Some("gpt-5.5"), false; "legacy encrypted leaf")] +#[test_case(None, None, Some(ReasoningEffort::Ultra), Some("medium"), false, ThreadHistoryMode::Legacy; "encrypted")] +#[test_case(None, None, Some(ReasoningEffort::Ultra), Some("medium"), true, ThreadHistoryMode::Paginated; "plaintext")] +#[test_case(Some("gpt-5.6-luna"), None, Some(ReasoningEffort::Ultra), Some("medium"), false, ThreadHistoryMode::Legacy; "luna encrypted leaf")] +#[test_case(Some("gpt-5.5"), Some(ReasoningEffort::High), Some(ReasoningEffort::Ultra), Some("high"), false, ThreadHistoryMode::Paginated; "legacy encrypted leaf")] +#[test_case(None, None, Some(ReasoningEffort::Persistent), Some("disabled"), false, ThreadHistoryMode::Paginated; "inherited persistent effort")] +#[test_case(None, None, None, None, false, ThreadHistoryMode::Legacy; "unknown model without effort default")] #[tokio::test] async fn multi_agent_v2_spawn_sends_agent_message_to_child( model: Option<&str>, + reasoning_effort: Option, + parent_reasoning_effort: Option, + expected_reasoning_effort: Option<&str>, plaintext: bool, + history_mode: ThreadHistoryMode, ) -> Result<()> { let output: &'static Mutex> = Box::leak(Box::new(Mutex::new(Vec::new()))); let subscriber = tracing_subscriber::fmt() @@ -2249,6 +2255,9 @@ async fn multi_agent_v2_spawn_sends_agent_message_to_child( spawn_args["fork_turns"] = json!("none"); } } + if let Some(reasoning_effort) = reasoning_effort { + spawn_args["reasoning_effort"] = json!(reasoning_effort); + } let spawn_args = serde_json::to_string(&spawn_args)?; let mut spawn_event = ev_function_call_with_namespace( SPAWN_CALL_ID, @@ -2297,8 +2306,10 @@ async fn multi_agent_v2_spawn_sends_agent_message_to_child( "koffing" }; let mut builder = test_codex() + .with_history_mode(history_mode) .with_model(parent_model) .with_config(move |config| { + config.model_reasoning_effort = parent_reasoning_effort; config .features .enable(Feature::Collab) @@ -2317,8 +2328,31 @@ async fn multi_agent_v2_spawn_sends_agent_message_to_child( let test = builder.build(&server).await?; let root_thread_id = test.session_configured.thread_id; - test.submit_turn(TURN_1_PROMPT).await?; - + test.submit_text_turn(TURN_1_PROMPT).await?; + test.codex.ensure_rollout_materialized().await; + test.codex.flush_rollout().await?; + let rollout = codex_rollout::RolloutRecorder::get_rollout_history( + &test.codex.rollout_path().expect("parent rollout path"), + ) + .await?; + let settings = rollout + .get_rollout_items() + .iter() + .find_map(|item| match item { + RolloutItem::EventMsg(EventMsg::ItemCompleted(completed)) => match &completed.item { + TurnItem::SubAgentActivity(activity) if activity.id == SPAWN_CALL_ID => { + Some((activity.model.clone(), activity.reasoning_effort.clone())) + } + _ => None, + }, + RolloutItem::EventMsg(EventMsg::SubAgentActivity(activity)) + if activity.event_id == SPAWN_CALL_ID => + { + Some((activity.model.clone(), activity.reasoning_effort.clone())) + } + _ => None, + }) + .expect("persisted spawn activity"); // The response mock records candidate requests before its request matcher runs, so wait for // the child request instead of assuming the latest recorded request is already it. let deadline = Instant::now() + Duration::from_secs(2); @@ -2335,6 +2369,25 @@ async fn multi_agent_v2_spawn_sends_agent_message_to_child( } sleep(Duration::from_millis(10)).await; }; + let child_body = child_request.body_json(); + let expected_settings = ( + Some(model.unwrap_or(parent_model)), + expected_reasoning_effort, + ); + assert_eq!( + ( + settings.0.as_deref(), + settings.1.as_ref().map(ReasoningEffort::as_str), + ), + expected_settings, + ); + assert_eq!( + ( + child_body["model"].as_str(), + child_body["reasoning"]["effort"].as_str(), + ), + expected_settings, + ); let content = if plaintext { vec![json!({ "type": "input_text", @@ -2365,8 +2418,7 @@ async fn multi_agent_v2_spawn_sends_agent_message_to_child( "content": content, })]) ); - if let Some(model) = model { - assert_eq!(child_request.body_json()["model"], json!(model)); + if model.is_some() { assert!( !child_request .body_json() @@ -2778,6 +2830,8 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message( assert_eq!( completed_item, &SubAgentActivityItem { + model: None, + reasoning_effort: None, id: format!("subagent-completed-{child_turn_id}"), kind: SubAgentActivityKind::Completed, agent_thread_id: child_thread_id, @@ -3094,6 +3148,8 @@ async fn multi_agent_v2_peer_followup_completion_notifies_initiating_turn( requester_thread_id, requester_turn_id, SubAgentActivityItem { + model: None, + reasoning_effort: None, id: format!("subagent-completed-{worker_followup_turn_id}"), kind: SubAgentActivityKind::Completed, agent_thread_id: worker_thread_id, diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index 0e48870650..8cf7e4646d 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -359,6 +359,10 @@ pub struct CollabAgentToolCallItem { #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] pub struct SubAgentActivityItem { + /// Resolved model at sub-agent creation; absent from older records and other activities. + pub model: Option, + /// Resolved reasoning effort at sub-agent creation, when known. + pub reasoning_effort: Option, pub id: String, pub kind: SubAgentActivityKind, pub agent_thread_id: ThreadId, diff --git a/codex-rs/protocol/src/legacy_events.rs b/codex-rs/protocol/src/legacy_events.rs index 05a66f6cee..4ae3b4e9b9 100644 --- a/codex-rs/protocol/src/legacy_events.rs +++ b/codex-rs/protocol/src/legacy_events.rs @@ -435,6 +435,8 @@ impl CollabAgentToolCallItem { impl SubAgentActivityItem { pub(crate) fn as_legacy_event(&self, occurred_at_ms: i64) -> EventMsg { EventMsg::SubAgentActivity(SubAgentActivityEvent { + model: self.model.clone(), + reasoning_effort: self.reasoning_effort.clone(), event_id: self.id.clone(), occurred_at_ms, agent_thread_id: self.agent_thread_id, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 5d84ca8c76..bbabeacf7e 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -4381,6 +4381,10 @@ pub enum SubAgentActivityKind { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct SubAgentActivityEvent { + /// Resolved model at sub-agent creation; absent from older records and other activities. + pub model: Option, + /// Resolved reasoning effort at sub-agent creation, when known. + pub reasoning_effort: Option, pub event_id: String, #[serde(default)] pub occurred_at_ms: i64, diff --git a/codex-rs/rollout-trace/src/protocol_event_tests.rs b/codex-rs/rollout-trace/src/protocol_event_tests.rs index acd1f0231a..e8acb12272 100644 --- a/codex-rs/rollout-trace/src/protocol_event_tests.rs +++ b/codex-rs/rollout-trace/src/protocol_event_tests.rs @@ -19,6 +19,8 @@ use crate::ExecutionStatus; fn sub_agent_activity_is_a_terminal_tool_runtime_event() -> anyhow::Result<()> { let agent_thread_id = ThreadId::new(); let event = EventMsg::SubAgentActivity(SubAgentActivityEvent { + model: Some("gpt-5".into()), + reasoning_effort: Some(codex_protocol::openai_models::ReasoningEffort::High), event_id: "call-spawn".to_string(), occurred_at_ms: 1234, agent_thread_id, @@ -44,7 +46,9 @@ fn sub_agent_activity_is_a_terminal_tool_runtime_event() -> anyhow::Result<()> { "occurred_at_ms": 1234, "agent_thread_id": agent_thread_id, "agent_path": "/root/reviewer", - "kind": "started" + "kind": "started", + "model": "gpt-5", + "reasoning_effort": "high" }) ); Ok(()) @@ -53,6 +57,8 @@ fn sub_agent_activity_is_a_terminal_tool_runtime_event() -> anyhow::Result<()> { #[test] fn completed_sub_agent_activity_is_not_a_tool_runtime_event() -> anyhow::Result<()> { let event = EventMsg::SubAgentActivity(SubAgentActivityEvent { + model: None, + reasoning_effort: None, event_id: "child-turn-completed".to_string(), occurred_at_ms: 1234, agent_thread_id: ThreadId::new(), diff --git a/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs b/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs index a1481d62cd..48fe3eeff4 100644 --- a/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs +++ b/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs @@ -258,6 +258,8 @@ pub(super) fn completed_item( )), EventMsg::SubAgentActivity(event) => Some(( TurnItem::SubAgentActivity(SubAgentActivityItem { + model: event.model.clone(), + reasoning_effort: event.reasoning_effort.clone(), id: event.event_id.clone(), kind: event.kind, agent_thread_id: event.agent_thread_id, diff --git a/codex-rs/thread-store/src/local/rollout_migration_tests.rs b/codex-rs/thread-store/src/local/rollout_migration_tests.rs index 4ce943a61e..ea49b65150 100644 --- a/codex-rs/thread-store/src/local/rollout_migration_tests.rs +++ b/codex-rs/thread-store/src/local/rollout_migration_tests.rs @@ -431,6 +431,22 @@ async fn migration_projects_explicit_and_implicit_legacy_completed_items() { "payload": {"type": "agent_reasoning_raw_content", "text": "raw"} })) .expect("build legacy reasoning content"); + let child_id = ThreadId::new(); + let [old_activity, activity] = [None, Some("gpt-5")].map(|model| { + let mut payload = json!({ + "type": "sub_agent_activity", + "event_id": if model.is_some() { "spawn" } else { "old-spawn" }, + "kind": "started", + "agent_thread_id": child_id, + "agent_path": "/root/worker", + }); + if let Some(model) = model { + payload["model"] = json!(model); + payload["reasoning_effort"] = json!("high"); + } + serde_json::from_value(json!({"type": "event_msg", "payload": payload})) + .expect("build legacy activity") + }); write_rollout( home.path(), thread_id, @@ -438,6 +454,8 @@ async fn migration_projects_explicit_and_implicit_legacy_completed_items() { vec![ started("explicit"), exec, + old_activity, + activity, completed("explicit"), reasoning, raw_reasoning, @@ -466,13 +484,13 @@ async fn migration_projects_explicit_and_implicit_legacy_completed_items() { }) .await .expect("read projected items"); - assert_eq!(items.items.len(), 2); + assert_eq!(items.items.len(), 4); assert_eq!(items.items[0].turn_id, "explicit"); - assert_eq!(items.items[1].turn_id, turns.turns[1].turn_id); + assert_eq!(items.items[3].turn_id, turns.turns[1].turn_id); let command: serde_json::Value = serde_json::from_slice(&items.items[0].item_json).expect("parse projected command"); let reasoning: serde_json::Value = - serde_json::from_slice(&items.items[1].item_json).expect("parse projected reasoning"); + serde_json::from_slice(&items.items[3].item_json).expect("parse projected reasoning"); assert_eq!(command["type"], "commandExecution"); let aggregated_output = command["aggregatedOutput"] .as_str() @@ -484,6 +502,21 @@ async fn migration_projects_explicit_and_implicit_legacy_completed_items() { assert_eq!(reasoning["type"], "reasoning"); assert_eq!(reasoning["summary"], json!(["summary"])); assert_eq!(reasoning["content"], json!(["raw"])); + for (item, model) in items.items[1..3].iter().zip([None, Some("gpt-5")]) { + assert_eq!(item.turn_id, "explicit"); + assert_eq!( + serde_json::from_slice::(&item.item_json).unwrap(), + json!({ + "type": "subAgentActivity", + "id": if model.is_some() { "spawn" } else { "old-spawn" }, + "kind": "started", + "agentThreadId": child_id, + "agentPath": "/root/worker", + "model": model, + "reasoningEffort": model.map(|_| "high"), + }) + ); + } } #[tokio::test] diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 8a4f864aaa..8eeb1d24ce 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -1348,6 +1348,8 @@ async fn live_app_server_command_output_delta_transcript_snapshot() { async fn live_app_server_sub_agent_activity_renders_once() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let activity = AppServerThreadItem::SubAgentActivity { + model: None, + reasoning_effort: None, id: "activity-1".to_string(), kind: codex_app_server_protocol::SubAgentActivityKind::Completed, agent_thread_id: ThreadId::new().to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/subagent_activity_tests.rs b/codex-rs/tui/src/chatwidget/tests/subagent_activity_tests.rs index 2266b3d0c6..9f04098d5e 100644 --- a/codex-rs/tui/src/chatwidget/tests/subagent_activity_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/subagent_activity_tests.rs @@ -15,6 +15,8 @@ enum FinalizedEvent { fn complete_subagent(chat: &mut ChatWidget, id: &str) { let item = AppServerThreadItem::SubAgentActivity { + model: None, + reasoning_effort: None, id: id.to_string(), kind: codex_app_server_protocol::SubAgentActivityKind::Completed, agent_thread_id: ThreadId::new().to_string(), diff --git a/codex-rs/tui/src/dynamic_tools.rs b/codex-rs/tui/src/dynamic_tools.rs index 547a8264bf..662ceac09e 100644 --- a/codex-rs/tui/src/dynamic_tools.rs +++ b/codex-rs/tui/src/dynamic_tools.rs @@ -1491,6 +1491,7 @@ fn turn_summary(turn: &Turn, include_outputs: bool, output_chars: usize) -> Valu kind, agent_thread_id, agent_path, + .. } => json!({ "type": "subAgentActivity", "id": id, "kind": kind, "agentThreadId": agent_thread_id, "agentPath": agent_path diff --git a/codex-rs/tui/src/multi_agents.rs b/codex-rs/tui/src/multi_agents.rs index c81112b444..8210270580 100644 --- a/codex-rs/tui/src/multi_agents.rs +++ b/codex-rs/tui/src/multi_agents.rs @@ -681,6 +681,8 @@ mod tests { #[test] fn interacted_sub_agent_activity_does_not_change_liveness() { let item = ThreadItem::SubAgentActivity { + model: None, + reasoning_effort: None, id: "activity-1".to_string(), kind: SubAgentActivityKind::Interacted, agent_thread_id: ThreadId::new().to_string(), @@ -694,6 +696,8 @@ mod tests { fn completed_sub_agent_activity_stops_running_liveness() { let thread_id = ThreadId::new(); let item = ThreadItem::SubAgentActivity { + model: None, + reasoning_effort: None, id: "activity-1".to_string(), kind: SubAgentActivityKind::Completed, agent_thread_id: thread_id.to_string(), diff --git a/codex-rs/tui/src/thread_transcript/other_items_tests.rs b/codex-rs/tui/src/thread_transcript/other_items_tests.rs index 845f25e2a1..e116a6025c 100644 --- a/codex-rs/tui/src/thread_transcript/other_items_tests.rs +++ b/codex-rs/tui/src/thread_transcript/other_items_tests.rs @@ -155,6 +155,8 @@ fn tool_and_notice_projection_uses_normal_transcript_presentation() { ..image }), ThreadItem::SubAgentActivity { + model: None, + reasoning_effort: None, id: "agent-1".to_string(), kind: SubAgentActivityKind::Completed, agent_thread_id: "01912345-1234-7123-8123-123456789abc".to_string(), diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index de694cbfed..820677b8ce 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -5465,6 +5465,19 @@ class SubAgentActivityThreadItem(BaseModel): agent_thread_id: Annotated[str, Field(alias="agentThreadId")] id: str kind: SubAgentActivityKind + model: Annotated[ + str | None, + Field( + description="Resolved model at sub-agent creation; absent from older records and other activities." + ), + ] = None + reasoning_effort: Annotated[ + ReasoningEffort | None, + Field( + alias="reasoningEffort", + description="Resolved reasoning effort at sub-agent creation, when known.", + ), + ] = None type: Annotated[Literal["subAgentActivity"], Field(title="SubAgentActivityThreadItemType")]