diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 9de202169..dbf9fc8e9 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -3015,10 +3015,16 @@ "description": "Identifier for the collab tool call.", "type": "string" }, + "model": { + "type": "string" + }, "prompt": { "description": "Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the beginning.", "type": "string" }, + "reasoning_effort": { + "$ref": "#/definitions/ReasoningEffort" + }, "sender_thread_id": { "allOf": [ { @@ -3037,7 +3043,9 @@ }, "required": [ "call_id", + "model", "prompt", + "reasoning_effort", "sender_thread_id", "type" ], @@ -9144,10 +9152,16 @@ "description": "Identifier for the collab tool call.", "type": "string" }, + "model": { + "type": "string" + }, "prompt": { "description": "Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the beginning.", "type": "string" }, + "reasoning_effort": { + "$ref": "#/definitions/ReasoningEffort" + }, "sender_thread_id": { "allOf": [ { @@ -9166,7 +9180,9 @@ }, "required": [ "call_id", + "model", "prompt", + "reasoning_effort", "sender_thread_id", "type" ], 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 6e40a6eb2..0a8dd747f 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 @@ -4378,10 +4378,16 @@ "description": "Identifier for the collab tool call.", "type": "string" }, + "model": { + "type": "string" + }, "prompt": { "description": "Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the beginning.", "type": "string" }, + "reasoning_effort": { + "$ref": "#/definitions/v2/ReasoningEffort" + }, "sender_thread_id": { "allOf": [ { @@ -4400,7 +4406,9 @@ }, "required": [ "call_id", + "model", "prompt", + "reasoning_effort", "sender_thread_id", "type" ], 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 90c576612..79add1f96 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 @@ -6180,10 +6180,16 @@ "description": "Identifier for the collab tool call.", "type": "string" }, + "model": { + "type": "string" + }, "prompt": { "description": "Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the beginning.", "type": "string" }, + "reasoning_effort": { + "$ref": "#/definitions/ReasoningEffort" + }, "sender_thread_id": { "allOf": [ { @@ -6202,7 +6208,9 @@ }, "required": [ "call_id", + "model", "prompt", + "reasoning_effort", "sender_thread_id", "type" ], diff --git a/codex-rs/app-server-protocol/schema/typescript/CollabAgentSpawnBeginEvent.ts b/codex-rs/app-server-protocol/schema/typescript/CollabAgentSpawnBeginEvent.ts index a86598e20..5f8692244 100644 --- a/codex-rs/app-server-protocol/schema/typescript/CollabAgentSpawnBeginEvent.ts +++ b/codex-rs/app-server-protocol/schema/typescript/CollabAgentSpawnBeginEvent.ts @@ -1,6 +1,7 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReasoningEffort } from "./ReasoningEffort"; import type { ThreadId } from "./ThreadId"; export type CollabAgentSpawnBeginEvent = { @@ -16,4 +17,4 @@ sender_thread_id: ThreadId, * Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the * beginning. */ -prompt: string, }; +prompt: string, model: string, reasoning_effort: ReasoningEffort, }; diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index abcf9de4c..54e146518 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -157,6 +157,8 @@ mod spawn { call_id: call_id.clone(), sender_thread_id: session.conversation_id, prompt: prompt.clone(), + model: args.model.clone().unwrap_or_default(), + reasoning_effort: args.reasoning_effort.unwrap_or_default(), } .into(), ) diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 5c6cd1c47..79c0f1b69 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -698,6 +698,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { call_id, sender_thread_id: _, prompt, + .. }) => { ts_msg!( self, diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index a051b5bb0..e31da9dc6 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -34,6 +34,7 @@ use codex_protocol::ThreadId; use codex_protocol::config_types::ModeKind; use codex_protocol::mcp::CallToolResult; use codex_protocol::models::WebSearchAction; +use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::plan_tool::PlanItemArg; use codex_protocol::plan_tool::StepStatus; use codex_protocol::plan_tool::UpdatePlanArgs; @@ -547,6 +548,8 @@ fn collab_spawn_begin_and_end_emit_item_events() { call_id: "call-10".to_string(), sender_thread_id, prompt: prompt.clone(), + model: "gpt-5".to_string(), + reasoning_effort: ReasoningEffortConfig::default(), }), ); let begin_events = ep.collect_thread_events(&begin); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e76ae0764..2d7c63a75 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3132,6 +3132,8 @@ pub struct CollabAgentSpawnBeginEvent { /// Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the /// beginning. pub prompt: String, + pub model: String, + pub reasoning_effort: ReasoningEffortConfig, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 7fdc294da..70b524466 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -101,6 +101,7 @@ use codex_protocol::protocol::AgentReasoningRawContentEvent; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::BackgroundEventEvent; use codex_protocol::protocol::CodexErrorInfo; +use codex_protocol::protocol::CollabAgentSpawnBeginEvent; use codex_protocol::protocol::CreditsSnapshot; use codex_protocol::protocol::DeprecationNoticeEvent; use codex_protocol::protocol::ErrorEvent; @@ -579,6 +580,7 @@ pub(crate) struct ChatWidget { // Latest completed user-visible Codex output that `/copy` should place on the clipboard. last_copyable_output: Option, running_commands: HashMap, + pending_collab_spawn_requests: HashMap, suppressed_exec_calls: HashSet, skills_all: Vec, skills_initial_state: Option>, @@ -3243,6 +3245,7 @@ impl ChatWidget { plan_stream_controller: None, last_copyable_output: None, running_commands: HashMap::new(), + pending_collab_spawn_requests: HashMap::new(), suppressed_exec_calls: HashSet::new(), last_unified_wait: None, unified_exec_wait_streak: None, @@ -3427,6 +3430,7 @@ impl ChatWidget { plan_stream_controller: None, last_copyable_output: None, running_commands: HashMap::new(), + pending_collab_spawn_requests: HashMap::new(), suppressed_exec_calls: HashSet::new(), last_unified_wait: None, unified_exec_wait_streak: None, @@ -3603,6 +3607,7 @@ impl ChatWidget { plan_stream_controller: None, last_copyable_output: None, running_commands: HashMap::new(), + pending_collab_spawn_requests: HashMap::new(), suppressed_exec_calls: HashSet::new(), last_unified_wait: None, unified_exec_wait_streak: None, @@ -4999,8 +5004,24 @@ impl ChatWidget { } EventMsg::ExitedReviewMode(review) => self.on_exited_review_mode(review), EventMsg::ContextCompacted(_) => self.on_agent_message("Context compacted".to_owned()), - EventMsg::CollabAgentSpawnBegin(_) => {} - EventMsg::CollabAgentSpawnEnd(ev) => self.on_collab_event(multi_agents::spawn_end(ev)), + EventMsg::CollabAgentSpawnBegin(CollabAgentSpawnBeginEvent { + call_id, + model, + reasoning_effort, + .. + }) => { + self.pending_collab_spawn_requests.insert( + call_id, + multi_agents::SpawnRequestSummary { + model, + reasoning_effort, + }, + ); + } + EventMsg::CollabAgentSpawnEnd(ev) => { + let spawn_request = self.pending_collab_spawn_requests.remove(&ev.call_id); + self.on_collab_event(multi_agents::spawn_end(ev, spawn_request.as_ref())); + } EventMsg::CollabAgentInteractionBegin(_) => {} EventMsg::CollabAgentInteractionEnd(ev) => { self.on_collab_event(multi_agents::interaction_end(ev)) diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 18f1f83d8..c18d1070b 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -58,9 +58,12 @@ use codex_protocol::protocol::AgentMessageDeltaEvent; use codex_protocol::protocol::AgentMessageEvent; use codex_protocol::protocol::AgentReasoningDeltaEvent; use codex_protocol::protocol::AgentReasoningEvent; +use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::BackgroundEventEvent; use codex_protocol::protocol::CodexErrorInfo; +use codex_protocol::protocol::CollabAgentSpawnBeginEvent; +use codex_protocol::protocol::CollabAgentSpawnEndEvent; use codex_protocol::protocol::CreditsSnapshot; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -1838,6 +1841,7 @@ async fn make_chatwidget_manual( plan_stream_controller: None, last_copyable_output: None, running_commands: HashMap::new(), + pending_collab_spawn_requests: HashMap::new(), suppressed_exec_calls: HashSet::new(), skills_all: Vec::new(), skills_initial_state: None, @@ -2011,6 +2015,48 @@ fn lines_to_single_string(lines: &[ratatui::text::Line<'static>]) -> String { s } +#[tokio::test] +async fn collab_spawn_end_shows_requested_model_and_effort() { + let (mut chat, mut rx, _ops) = make_chatwidget_manual(None).await; + let sender_thread_id = ThreadId::new(); + let spawned_thread_id = ThreadId::new(); + + chat.handle_codex_event(Event { + id: "spawn-begin".into(), + msg: EventMsg::CollabAgentSpawnBegin(CollabAgentSpawnBeginEvent { + call_id: "call-spawn".to_string(), + sender_thread_id, + prompt: "Explore the repo".to_string(), + model: "gpt-5".to_string(), + reasoning_effort: ReasoningEffortConfig::High, + }), + }); + chat.handle_codex_event(Event { + id: "spawn-end".into(), + msg: EventMsg::CollabAgentSpawnEnd(CollabAgentSpawnEndEvent { + call_id: "call-spawn".to_string(), + sender_thread_id, + new_thread_id: Some(spawned_thread_id), + new_agent_nickname: Some("Robie".to_string()), + new_agent_role: Some("explorer".to_string()), + prompt: "Explore the repo".to_string(), + status: AgentStatus::PendingInit, + }), + }); + + let cells = drain_insert_history(&mut rx); + let rendered = cells + .iter() + .map(|lines| lines_to_single_string(lines)) + .collect::>() + .join("\n"); + + assert!( + rendered.contains("Spawned Robie [explorer] (gpt-5 high)"), + "expected spawn line to include agent metadata and requested model, got {rendered:?}" + ); +} + fn status_line_text(chat: &ChatWidget) -> Option { chat.status_line_text() } diff --git a/codex-rs/tui/src/multi_agents.rs b/codex-rs/tui/src/multi_agents.rs index 7161e8880..3a90f7741 100644 --- a/codex-rs/tui/src/multi_agents.rs +++ b/codex-rs/tui/src/multi_agents.rs @@ -2,6 +2,7 @@ use crate::history_cell::PlainHistoryCell; use crate::render::line_utils::prefix_lines; use crate::text_formatting::truncate_text; use codex_protocol::ThreadId; +use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::CollabAgentInteractionEndEvent; use codex_protocol::protocol::CollabAgentRef; @@ -36,6 +37,12 @@ struct AgentLabel<'a> { role: Option<&'a str>, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SpawnRequestSummary { + pub(crate) model: String, + pub(crate) reasoning_effort: ReasoningEffortConfig, +} + pub(crate) fn agent_picker_status_dot_spans(is_closed: bool) -> Vec> { let dot = if is_closed { "•".into() @@ -74,7 +81,10 @@ pub(crate) fn sort_agent_picker_threads(agent_threads: &mut [(ThreadId, AgentPic }); } -pub(crate) fn spawn_end(ev: CollabAgentSpawnEndEvent) -> PlainHistoryCell { +pub(crate) fn spawn_end( + ev: CollabAgentSpawnEndEvent, + spawn_request: Option<&SpawnRequestSummary>, +) -> PlainHistoryCell { let CollabAgentSpawnEndEvent { call_id: _, sender_thread_id: _, @@ -93,6 +103,7 @@ pub(crate) fn spawn_end(ev: CollabAgentSpawnEndEvent) -> PlainHistoryCell { nickname: new_agent_nickname.as_deref(), role: new_agent_role.as_deref(), }, + spawn_request, ), None => title_text("Agent spawn failed"), }; @@ -122,6 +133,7 @@ pub(crate) fn interaction_end(ev: CollabAgentInteractionEndEvent) -> PlainHistor nickname: receiver_agent_nickname.as_deref(), role: receiver_agent_role.as_deref(), }, + None, ); let mut details = Vec::new(); @@ -141,7 +153,7 @@ pub(crate) fn waiting_begin(ev: CollabWaitingBeginEvent) -> PlainHistoryCell { let receiver_agents = merge_wait_receivers(&receiver_thread_ids, receiver_agents); let title = match receiver_agents.as_slice() { - [receiver] => title_with_agent("Waiting for", agent_label_from_ref(receiver)), + [receiver] => title_with_agent("Waiting for", agent_label_from_ref(receiver), None), [] => title_text("Waiting for agents"), _ => title_text(format!("Waiting for {} agents", receiver_agents.len())), }; @@ -187,6 +199,7 @@ pub(crate) fn close_end(ev: CollabCloseEndEvent) -> PlainHistoryCell { nickname: receiver_agent_nickname.as_deref(), role: receiver_agent_role.as_deref(), }, + None, ), Vec::new(), ) @@ -209,6 +222,7 @@ pub(crate) fn resume_begin(ev: CollabResumeBeginEvent) -> PlainHistoryCell { nickname: receiver_agent_nickname.as_deref(), role: receiver_agent_role.as_deref(), }, + None, ), Vec::new(), ) @@ -232,6 +246,7 @@ pub(crate) fn resume_end(ev: CollabResumeEndEvent) -> PlainHistoryCell { nickname: receiver_agent_nickname.as_deref(), role: receiver_agent_role.as_deref(), }, + None, ), vec![status_summary_line(&status)], ) @@ -249,9 +264,14 @@ fn title_text(title: impl Into) -> Line<'static> { title_spans_line(vec![Span::from(title.into()).bold()]) } -fn title_with_agent(prefix: &str, agent: AgentLabel<'_>) -> Line<'static> { +fn title_with_agent( + prefix: &str, + agent: AgentLabel<'_>, + spawn_request: Option<&SpawnRequestSummary>, +) -> Line<'static> { let mut spans = vec![Span::from(format!("{prefix} ")).bold()]; spans.extend(agent_label_spans(agent)); + spans.extend(spawn_request_spans(spawn_request)); title_spans_line(spans) } @@ -298,6 +318,25 @@ fn agent_label_spans(agent: AgentLabel<'_>) -> Vec> { spans } +fn spawn_request_spans(spawn_request: Option<&SpawnRequestSummary>) -> Vec> { + let Some(spawn_request) = spawn_request else { + return Vec::new(); + }; + + let model = spawn_request.model.trim(); + if model.is_empty() && spawn_request.reasoning_effort == ReasoningEffortConfig::default() { + return Vec::new(); + } + + let details = if model.is_empty() { + format!("({})", spawn_request.reasoning_effort) + } else { + format!("({model} {})", spawn_request.reasoning_effort) + }; + + vec![Span::from(" ").dim(), Span::from(details).magenta()] +} + fn prompt_line(prompt: &str) -> Option> { let trimmed = prompt.trim(); if trimmed.is_empty() { @@ -460,15 +499,21 @@ mod tests { let bob_id = ThreadId::from_string("00000000-0000-0000-0000-000000000003") .expect("valid bob thread id"); - let spawn = spawn_end(CollabAgentSpawnEndEvent { - call_id: "call-spawn".to_string(), - sender_thread_id, - new_thread_id: Some(robie_id), - new_agent_nickname: Some("Robie".to_string()), - new_agent_role: Some("explorer".to_string()), - prompt: "Compute 11! and reply with just the integer result.".to_string(), - status: AgentStatus::PendingInit, - }); + let spawn = spawn_end( + CollabAgentSpawnEndEvent { + call_id: "call-spawn".to_string(), + sender_thread_id, + new_thread_id: Some(robie_id), + new_agent_nickname: Some("Robie".to_string()), + new_agent_role: Some("explorer".to_string()), + prompt: "Compute 11! and reply with just the integer result.".to_string(), + status: AgentStatus::PendingInit, + }, + Some(&SpawnRequestSummary { + model: "gpt-5".to_string(), + reasoning_effort: ReasoningEffortConfig::High, + }), + ); let send = interaction_end(CollabAgentInteractionEndEvent { call_id: "call-send".to_string(), @@ -540,15 +585,21 @@ mod tests { .expect("valid sender thread id"); let robie_id = ThreadId::from_string("00000000-0000-0000-0000-000000000002") .expect("valid robie thread id"); - let cell = spawn_end(CollabAgentSpawnEndEvent { - call_id: "call-spawn".to_string(), - sender_thread_id, - new_thread_id: Some(robie_id), - new_agent_nickname: Some("Robie".to_string()), - new_agent_role: Some("explorer".to_string()), - prompt: String::new(), - status: AgentStatus::PendingInit, - }); + let cell = spawn_end( + CollabAgentSpawnEndEvent { + call_id: "call-spawn".to_string(), + sender_thread_id, + new_thread_id: Some(robie_id), + new_agent_nickname: Some("Robie".to_string()), + new_agent_role: Some("explorer".to_string()), + prompt: String::new(), + status: AgentStatus::PendingInit, + }, + Some(&SpawnRequestSummary { + model: "gpt-5".to_string(), + reasoning_effort: ReasoningEffortConfig::High, + }), + ); let lines = cell.display_lines(200); let title = &lines[0]; @@ -558,6 +609,8 @@ mod tests { assert_eq!(title.spans[4].content.as_ref(), "[explorer]"); assert_eq!(title.spans[4].style.fg, None); assert!(!title.spans[4].style.add_modifier.contains(Modifier::DIM)); + assert_eq!(title.spans[6].content.as_ref(), "(gpt-5 high)"); + assert_eq!(title.spans[6].style.fg, Some(Color::Magenta)); } fn cell_to_text(cell: &PlainHistoryCell) -> String { diff --git a/codex-rs/tui/src/snapshots/codex_tui__multi_agents__tests__collab_agent_transcript.snap b/codex-rs/tui/src/snapshots/codex_tui__multi_agents__tests__collab_agent_transcript.snap index 19001a70d..2bc6083fc 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__multi_agents__tests__collab_agent_transcript.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__multi_agents__tests__collab_agent_transcript.snap @@ -2,7 +2,7 @@ source: tui/src/multi_agents.rs expression: snapshot --- -• Spawned Robie [explorer] +• Spawned Robie [explorer] (gpt-5 high) └ Compute 11! and reply with just the integer result. • Sent input to Robie [explorer]