diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 66019980a..c041fe962 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -128,7 +128,10 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread } } -fn command_actions_for_path_uri(parsed_cmd: &[ParsedCommand], cwd: &PathUri) -> Vec { +pub(crate) fn command_actions_for_path_uri( + parsed_cmd: &[ParsedCommand], + cwd: &PathUri, +) -> Vec { // TODO(anp): Carry PathUri into CommandAction so foreign Read actions retain resolved paths. // Until then, omit those actions rather than project a foreign cwd onto the host. let native_cwd = if cwd.infer_path_convention() == Some(PathConvention::native()) { 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 aed87b18a..2d9c6b6a1 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -574,52 +574,25 @@ impl ThreadHistoryBuilder { } fn handle_item_started(&mut self, payload: &ItemStartedEvent) { - match &payload.item { - codex_protocol::items::TurnItem::Plan(plan) => { - if plan.text.is_empty() { - return; - } - self.upsert_item_in_turn_id( - &payload.turn_id, - ThreadItem::from(payload.item.clone()), - ); - } - codex_protocol::items::TurnItem::Sleep(_) => { - self.upsert_item_in_turn_id( - &payload.turn_id, - ThreadItem::from(payload.item.clone()), - ); - } - codex_protocol::items::TurnItem::UserMessage(_) - | codex_protocol::items::TurnItem::HookPrompt(_) - | codex_protocol::items::TurnItem::AgentMessage(_) - | codex_protocol::items::TurnItem::Reasoning(_) - | codex_protocol::items::TurnItem::WebSearch(_) - | codex_protocol::items::TurnItem::ImageView(_) - | codex_protocol::items::TurnItem::ImageGeneration(_) - | codex_protocol::items::TurnItem::FileChange(_) - | codex_protocol::items::TurnItem::McpToolCall(_) - | codex_protocol::items::TurnItem::ContextCompaction(_) => {} - } + self.handle_materialized_item_lifecycle(&payload.turn_id, &payload.item); } fn handle_item_completed(&mut self, payload: &ItemCompletedEvent) { - match &payload.item { - codex_protocol::items::TurnItem::Plan(plan) => { - if plan.text.is_empty() { - return; - } - self.upsert_item_in_turn_id( - &payload.turn_id, - ThreadItem::from(payload.item.clone()), - ); - } - codex_protocol::items::TurnItem::Sleep(_) => { - self.upsert_item_in_turn_id( - &payload.turn_id, - ThreadItem::from(payload.item.clone()), - ); - } + self.handle_materialized_item_lifecycle(&payload.turn_id, &payload.item); + } + + fn handle_materialized_item_lifecycle( + &mut self, + turn_id: &str, + item: &codex_protocol::items::TurnItem, + ) { + let should_upsert = match item { + codex_protocol::items::TurnItem::Plan(plan) => !plan.text.is_empty(), + codex_protocol::items::TurnItem::Sleep(_) + | codex_protocol::items::TurnItem::CommandExecution(_) + | codex_protocol::items::TurnItem::DynamicToolCall(_) + | codex_protocol::items::TurnItem::CollabAgentToolCall(_) + | codex_protocol::items::TurnItem::SubAgentActivity(_) => true, codex_protocol::items::TurnItem::UserMessage(_) | codex_protocol::items::TurnItem::HookPrompt(_) | codex_protocol::items::TurnItem::AgentMessage(_) @@ -629,7 +602,11 @@ impl ThreadHistoryBuilder { | codex_protocol::items::TurnItem::ImageGeneration(_) | codex_protocol::items::TurnItem::FileChange(_) | codex_protocol::items::TurnItem::McpToolCall(_) - | codex_protocol::items::TurnItem::ContextCompaction(_) => {} + | codex_protocol::items::TurnItem::ContextCompaction(_) => false, + }; + + if should_upsert { + self.upsert_item_in_turn_id(turn_id, ThreadItem::from(item.clone())); } } @@ -1573,6 +1550,8 @@ mod tests { use crate::protocol::v2::CommandExecutionSource; use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; + use codex_protocol::items::CommandExecutionItem as CoreCommandExecutionItem; + use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus; use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment; use codex_protocol::items::SleepItem as CoreSleepItem; use codex_protocol::items::TurnItem as CoreTurnItem; @@ -1867,6 +1846,77 @@ mod tests { ); } + #[test] + fn rebuilds_command_execution_item_from_persisted_completion() { + let turn_id = "turn-1"; + let thread_id = ThreadId::new(); + let command_item = CoreTurnItem::CommandExecution(CoreCommandExecutionItem { + id: "exec-1".to_string(), + process_id: Some("pid-1".to_string()), + command: vec!["echo".to_string(), "hello world".to_string()], + cwd: test_path_buf("/tmp").abs().into(), + parsed_cmd: vec![ParsedCommand::Unknown { + cmd: "echo hello world".to_string(), + }], + source: ExecCommandSource::Agent, + interaction_input: None, + status: CoreCommandExecutionStatus::Completed, + stdout: Some("hello world\n".to_string()), + stderr: Some(String::new()), + aggregated_output: Some("hello world\n".to_string()), + exit_code: Some(0), + duration: Some(Duration::from_millis(12)), + formatted_output: Some("hello world\n".to_string()), + }); + let events = vec![ + EventMsg::TurnStarted(TurnStartedEvent { + turn_id: turn_id.to_string(), + trace_id: None, + started_at: None, + model_context_window: None, + collaboration_mode_kind: Default::default(), + }), + EventMsg::ItemCompleted(ItemCompletedEvent { + thread_id, + turn_id: turn_id.to_string(), + item: command_item, + completed_at_ms: 1_000, + }), + EventMsg::TurnComplete(TurnCompleteEvent { + turn_id: turn_id.to_string(), + last_agent_message: None, + completed_at: None, + duration_ms: None, + time_to_first_token_ms: None, + }), + ]; + + let items = events + .into_iter() + .map(RolloutItem::EventMsg) + .collect::>(); + let turns = build_turns_from_rollout_items(&items); + + assert_eq!(turns.len(), 1); + assert_eq!( + turns[0].items, + vec![ThreadItem::CommandExecution { + id: "exec-1".to_string(), + command: "echo 'hello world'".to_string(), + cwd: test_path_buf("/tmp").abs().into(), + process_id: Some("pid-1".to_string()), + source: CommandExecutionSource::Agent, + status: CommandExecutionStatus::Completed, + command_actions: vec![CommandAction::Unknown { + command: "echo hello world".to_string(), + }], + aggregated_output: Some("hello world\n".to_string()), + exit_code: Some(0), + duration_ms: Some(12), + }] + ); + } + #[test] fn preserves_user_message_client_id_from_legacy_event() { let turn_id = "turn-1"; 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 751eab824..cd0475244 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -8,12 +8,17 @@ use super::NetworkPolicyAmendment; use super::RequestPermissionProfile; use super::UserInput; use super::shared::v2_enum_from_core; +use crate::protocol::item_builders::command_actions_for_path_uri; use crate::protocol::item_builders::convert_patch_changes; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction; use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource; use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource; use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; +use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool; +use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus; +use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus; +use codex_protocol::items::DynamicToolCallStatus as CoreDynamicToolCallStatus; use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus; use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; @@ -30,6 +35,7 @@ use codex_protocol::protocol::GuardianUserAuthorization as CoreGuardianUserAutho use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus; use codex_protocol::protocol::ReviewDecision as CoreReviewDecision; use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind; +use codex_shell_command::parse_command::shlex_join; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::LegacyAppPathString; use schemars::JsonSchema; @@ -854,6 +860,64 @@ impl From for ThreadItem { summary: reasoning.summary_text, content: reasoning.raw_content, }, + CoreTurnItem::CommandExecution(command) => ThreadItem::CommandExecution { + id: command.id, + command: shlex_join(&command.command), + cwd: command.cwd.clone().into(), + process_id: command.process_id, + source: command.source.into(), + status: command.status.into(), + command_actions: command_actions_for_path_uri(&command.parsed_cmd, &command.cwd), + aggregated_output: command + .aggregated_output + .filter(|output| !output.is_empty()), + exit_code: command.exit_code, + duration_ms: command + .duration + .and_then(|duration| i64::try_from(duration.as_millis()).ok()), + }, + CoreTurnItem::DynamicToolCall(call) => ThreadItem::DynamicToolCall { + id: call.id, + namespace: call.namespace, + tool: call.tool, + arguments: call.arguments, + status: call.status.into(), + content_items: call.content_items.map(|items| { + items + .into_iter() + .map(DynamicToolCallOutputContentItem::from) + .collect() + }), + success: call.success, + duration_ms: call + .duration + .and_then(|duration| i64::try_from(duration.as_millis()).ok()), + }, + CoreTurnItem::CollabAgentToolCall(call) => ThreadItem::CollabAgentToolCall { + id: call.id, + tool: call.tool.into(), + status: call.status.into(), + sender_thread_id: call.sender_thread_id.to_string(), + receiver_thread_ids: call + .receiver_thread_ids + .into_iter() + .map(String::from) + .collect(), + prompt: call.prompt, + model: call.model, + reasoning_effort: call.reasoning_effort, + agents_states: call + .agents_states + .into_iter() + .map(|(thread_id, status)| (thread_id.to_string(), status.into())) + .collect(), + }, + CoreTurnItem::SubAgentActivity(activity) => ThreadItem::SubAgentActivity { + id: activity.id, + kind: activity.kind.into(), + agent_thread_id: activity.agent_thread_id.to_string(), + agent_path: String::from(activity.agent_path), + }, CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch { id: search.id, query: search.query, @@ -941,6 +1005,17 @@ impl From for CommandExecutionStatus { } } +impl From for CommandExecutionStatus { + fn from(value: CoreCommandExecutionStatus) -> Self { + match value { + CoreCommandExecutionStatus::InProgress => Self::InProgress, + CoreCommandExecutionStatus::Completed => Self::Completed, + CoreCommandExecutionStatus::Failed => Self::Failed, + CoreCommandExecutionStatus::Declined => Self::Declined, + } + } +} + impl From<&CoreExecCommandStatus> for CommandExecutionStatus { fn from(value: &CoreExecCommandStatus) -> Self { match value { @@ -1028,6 +1103,16 @@ impl From for McpToolCallStatus { } } +impl From for DynamicToolCallStatus { + fn from(value: CoreDynamicToolCallStatus) -> Self { + match value { + CoreDynamicToolCallStatus::InProgress => Self::InProgress, + CoreDynamicToolCallStatus::Completed => Self::Completed, + CoreDynamicToolCallStatus::Failed => Self::Failed, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1055,6 +1140,28 @@ pub enum CollabAgentToolCallStatus { Failed, } +impl From for CollabAgentTool { + fn from(value: CoreCollabAgentTool) -> Self { + match value { + CoreCollabAgentTool::SpawnAgent => Self::SpawnAgent, + CoreCollabAgentTool::SendInput => Self::SendInput, + CoreCollabAgentTool::ResumeAgent => Self::ResumeAgent, + CoreCollabAgentTool::Wait => Self::Wait, + CoreCollabAgentTool::CloseAgent => Self::CloseAgent, + } + } +} + +impl From for CollabAgentToolCallStatus { + fn from(value: CoreCollabAgentToolCallStatus) -> Self { + match value { + CoreCollabAgentToolCallStatus::InProgress => Self::InProgress, + CoreCollabAgentToolCallStatus::Completed => Self::Completed, + CoreCollabAgentToolCallStatus::Failed => Self::Failed, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1462,6 +1569,21 @@ pub enum DynamicToolCallOutputContentItem { InputImage { image_url: String }, } +impl From + for DynamicToolCallOutputContentItem +{ + fn from(item: codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem) -> Self { + match item { + codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputText { text } => { + Self::InputText { text } + } + codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputImage { + image_url, + } => Self::InputImage { image_url }, + } + } +} + impl From for codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem { 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 677f4470f..48f92bf78 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -4,11 +4,19 @@ use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest; use codex_protocol::config_types::MultiAgentMode; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; +use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool; +use codex_protocol::items::CollabAgentToolCallItem; +use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus; +use codex_protocol::items::CommandExecutionItem; +use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus; +use codex_protocol::items::DynamicToolCallItem; +use codex_protocol::items::DynamicToolCallStatus as CoreDynamicToolCallStatus; use codex_protocol::items::FileChangeItem; use codex_protocol::items::ImageViewItem; use codex_protocol::items::McpToolCallItem; use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus; use codex_protocol::items::ReasoningItem; +use codex_protocol::items::SubAgentActivityItem; use codex_protocol::items::TurnItem; use codex_protocol::items::UserMessageItem; use codex_protocol::items::WebSearchItem; @@ -30,8 +38,10 @@ use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialP use codex_protocol::protocol::AgentStatus as CoreAgentStatus; use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use codex_protocol::protocol::ConversationTextRole; +use codex_protocol::protocol::ExecCommandSource as CoreExecCommandSource; use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig; use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; +use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind; use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile; use codex_protocol::user_input::UserInput as CoreUserInput; use codex_utils_absolute_path::AbsolutePathBuf; @@ -2632,6 +2642,134 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { } ); + let command_item = TurnItem::CommandExecution(CommandExecutionItem { + id: "exec-1".to_string(), + process_id: Some("pid-1".to_string()), + command: vec!["echo".to_string(), "done".to_string()], + cwd: PathUri::from_abs_path(&test_path_buf("/tmp").abs()), + parsed_cmd: vec![codex_protocol::parse_command::ParsedCommand::Unknown { + cmd: "echo done".to_string(), + }], + source: CoreExecCommandSource::Agent, + interaction_input: None, + status: CoreCommandExecutionStatus::Completed, + stdout: Some("done\n".to_string()), + stderr: Some(String::new()), + aggregated_output: Some("done\n".to_string()), + exit_code: Some(0), + duration: Some(Duration::from_millis(5)), + formatted_output: Some("done\n".to_string()), + }); + + assert_eq!( + ThreadItem::from(command_item), + ThreadItem::CommandExecution { + id: "exec-1".to_string(), + command: "echo done".to_string(), + cwd: LegacyAppPathString::from_abs_path(&test_path_buf("/tmp").abs()), + process_id: Some("pid-1".to_string()), + source: CommandExecutionSource::Agent, + status: CommandExecutionStatus::Completed, + command_actions: vec![CommandAction::Unknown { + command: "echo done".to_string(), + }], + aggregated_output: Some("done\n".to_string()), + exit_code: Some(0), + duration_ms: Some(5), + } + ); + + let dynamic_tool_call_item = TurnItem::DynamicToolCall(DynamicToolCallItem { + id: "dynamic-1".to_string(), + namespace: Some("apps".to_string()), + tool: "lookup".to_string(), + arguments: json!({"id": "123"}), + status: CoreDynamicToolCallStatus::Completed, + content_items: Some(vec![ + codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputText { + text: "ok".to_string(), + }, + ]), + success: Some(true), + error: None, + duration: Some(Duration::from_millis(5)), + }); + + assert_eq!( + ThreadItem::from(dynamic_tool_call_item), + ThreadItem::DynamicToolCall { + id: "dynamic-1".to_string(), + namespace: Some("apps".to_string()), + tool: "lookup".to_string(), + arguments: json!({"id": "123"}), + status: DynamicToolCallStatus::Completed, + content_items: Some(vec![DynamicToolCallOutputContentItem::InputText { + text: "ok".to_string(), + }]), + success: Some(true), + duration_ms: Some(5), + } + ); + + let sender_thread_id = codex_protocol::ThreadId::default(); + let receiver_thread_id = codex_protocol::ThreadId::default(); + let collab_item = TurnItem::CollabAgentToolCall(CollabAgentToolCallItem { + id: "collab-1".to_string(), + tool: CoreCollabAgentTool::SendInput, + status: CoreCollabAgentToolCallStatus::Completed, + sender_thread_id, + receiver_thread_ids: vec![receiver_thread_id], + receiver_agents: Vec::new(), + prompt: Some("continue".to_string()), + model: None, + reasoning_effort: None, + agents_states: [(receiver_thread_id, CoreAgentStatus::Completed(None))] + .into_iter() + .collect(), + }); + + assert_eq!( + ThreadItem::from(collab_item), + ThreadItem::CollabAgentToolCall { + id: "collab-1".to_string(), + tool: CollabAgentTool::SendInput, + status: CollabAgentToolCallStatus::Completed, + sender_thread_id: sender_thread_id.to_string(), + receiver_thread_ids: vec![receiver_thread_id.to_string()], + prompt: Some("continue".to_string()), + model: None, + reasoning_effort: None, + agents_states: [( + receiver_thread_id.to_string(), + CollabAgentState { + status: CollabAgentStatus::Completed, + message: None, + }, + )] + .into_iter() + .collect(), + } + ); + + let sub_agent_activity_item = TurnItem::SubAgentActivity(SubAgentActivityItem { + id: "activity-1".to_string(), + kind: CoreSubAgentActivityKind::Interrupted, + agent_thread_id: receiver_thread_id, + agent_path: codex_protocol::AgentPath::root() + .join("worker") + .expect("worker path"), + }); + + assert_eq!( + ThreadItem::from(sub_agent_activity_item), + ThreadItem::SubAgentActivity { + id: "activity-1".to_string(), + kind: SubAgentActivityKind::Interrupted, + agent_thread_id: receiver_thread_id.to_string(), + agent_path: "/root/worker".to_string(), + } + ); + let search_item = TurnItem::WebSearch(WebSearchItem { id: "search-1".to_string(), query: "docs".to_string(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 46f7ca6e6..d6336acd5 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -7016,6 +7016,7 @@ async fn submission_loop_channel_close_runs_full_thread_teardown() { dynamic_tools: Vec::new(), selected_capability_roots: Vec::new(), multi_agent_version: None, + history_mode: Default::default(), initial_window_id: Uuid::now_v7().to_string(), metadata: ThreadPersistenceMetadata { cwd: Some(config.cwd.to_path_buf()), diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index c4ee26d96..c31957344 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -1,3 +1,6 @@ +use crate::AgentPath; +use crate::ThreadId; +use crate::dynamic_tools::DynamicToolCallOutputContentItem; use crate::mcp::CallToolResult; use crate::memory_citation::MemoryCitation; use crate::models::ContentItem; @@ -5,11 +8,16 @@ use crate::models::ImageDetail; use crate::models::MessagePhase; use crate::models::ResponseItem; use crate::models::WebSearchAction; +use crate::openai_models::ReasoningEffort as ReasoningEffortConfig; +use crate::parse_command::ParsedCommand; use crate::protocol::AgentMessageEvent; use crate::protocol::AgentReasoningEvent; use crate::protocol::AgentReasoningRawContentEvent; +use crate::protocol::AgentStatus; +use crate::protocol::CollabAgentRef; use crate::protocol::ContextCompactedEvent; use crate::protocol::EventMsg; +use crate::protocol::ExecCommandSource; use crate::protocol::FileChange; use crate::protocol::ImageGenerationEndEvent; use crate::protocol::McpInvocation; @@ -18,6 +26,7 @@ use crate::protocol::McpToolCallEndEvent; use crate::protocol::PatchApplyBeginEvent; use crate::protocol::PatchApplyEndEvent; use crate::protocol::PatchApplyStatus; +use crate::protocol::SubAgentActivityKind; use crate::protocol::UserMessageEvent; use crate::protocol::ViewImageToolCallEvent; use crate::protocol::WebSearchEndEvent; @@ -46,6 +55,10 @@ pub enum TurnItem { AgentMessage(AgentMessageItem), Plan(PlanItem), Reasoning(ReasoningItem), + CommandExecution(CommandExecutionItem), + DynamicToolCall(DynamicToolCallItem), + CollabAgentToolCall(CollabAgentToolCallItem), + SubAgentActivity(SubAgentActivityItem), WebSearch(WebSearchItem), ImageView(ImageViewItem), Sleep(SleepItem), @@ -129,6 +142,129 @@ pub struct ReasoningItem { pub raw_content: Vec, } +#[derive(Debug, Clone, Copy, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum CommandExecutionStatus { + InProgress, + Completed, + Failed, + Declined, +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] +pub struct CommandExecutionItem { + pub id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub process_id: Option, + pub command: Vec, + pub cwd: PathUri, + pub parsed_cmd: Vec, + pub source: ExecCommandSource, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub interaction_input: Option, + pub status: CommandExecutionStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub stdout: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub stderr: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub aggregated_output: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub exit_code: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(type = "string", optional)] + pub duration: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub formatted_output: Option, +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum DynamicToolCallStatus { + InProgress, + Completed, + Failed, +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] +pub struct DynamicToolCallItem { + pub id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub namespace: Option, + pub tool: String, + pub arguments: serde_json::Value, + pub status: DynamicToolCallStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub content_items: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub success: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub error: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(type = "string", optional)] + pub duration: Option, +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum CollabAgentTool { + SpawnAgent, + SendInput, + ResumeAgent, + Wait, + CloseAgent, +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum CollabAgentToolCallStatus { + InProgress, + Completed, + Failed, +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] +pub struct CollabAgentToolCallItem { + pub id: String, + pub tool: CollabAgentTool, + pub status: CollabAgentToolCallStatus, + pub sender_thread_id: ThreadId, + #[serde(default)] + pub receiver_thread_ids: Vec, + #[serde(default)] + pub receiver_agents: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub prompt: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub model: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub reasoning_effort: Option, + #[serde(default)] + pub agents_states: HashMap, +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +pub struct SubAgentActivityItem { + pub id: String, + pub kind: SubAgentActivityKind, + pub agent_thread_id: ThreadId, + pub agent_path: AgentPath, +} + #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct WebSearchItem { pub id: String, @@ -611,6 +747,10 @@ impl TurnItem { TurnItem::AgentMessage(item) => item.id.clone(), TurnItem::Plan(item) => item.id.clone(), TurnItem::Reasoning(item) => item.id.clone(), + TurnItem::CommandExecution(item) => item.id.clone(), + TurnItem::DynamicToolCall(item) => item.id.clone(), + TurnItem::CollabAgentToolCall(item) => item.id.clone(), + TurnItem::SubAgentActivity(item) => item.id.clone(), TurnItem::WebSearch(item) => item.id.clone(), TurnItem::ImageView(item) => item.id.clone(), TurnItem::Sleep(item) => item.id.clone(), @@ -627,6 +767,10 @@ impl TurnItem { TurnItem::HookPrompt(_) => Vec::new(), TurnItem::AgentMessage(item) => item.as_legacy_events(), TurnItem::Plan(_) => Vec::new(), + TurnItem::CommandExecution(_) + | TurnItem::DynamicToolCall(_) + | TurnItem::CollabAgentToolCall(_) => Vec::new(), + TurnItem::SubAgentActivity(_) => Vec::new(), TurnItem::WebSearch(item) => vec![item.as_legacy_event()], TurnItem::ImageView(item) => { vec![EventMsg::ViewImageToolCall(ViewImageToolCallEvent { diff --git a/codex-rs/rollout/src/persistence_metrics.rs b/codex-rs/rollout/src/persistence_metrics.rs index 249a23cc7..8c2a7e18d 100644 --- a/codex-rs/rollout/src/persistence_metrics.rs +++ b/codex-rs/rollout/src/persistence_metrics.rs @@ -247,6 +247,10 @@ fn turn_item_type(item: &TurnItem) -> &'static str { TurnItem::AgentMessage(_) => "agent_message", TurnItem::Plan(_) => "plan", TurnItem::Reasoning(_) => "reasoning", + TurnItem::CommandExecution(_) => "command_execution", + TurnItem::DynamicToolCall(_) => "dynamic_tool_call", + TurnItem::CollabAgentToolCall(_) => "collab_agent_tool_call", + TurnItem::SubAgentActivity(_) => "sub_agent_activity", TurnItem::WebSearch(_) => "web_search", TurnItem::ImageView(_) => "image_view", TurnItem::Sleep(_) => "sleep",