mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(protocol): define missing rollout turn items (#30282)
## Description
This PR adds canonical core `TurnItem` shapes for command execution,
dynamic tool calls, collab agent tool calls, and sub-agent activity, to
be stored in the rollout file soon.
It also teaches app-server protocol / `ThreadHistoryBuilder` how to
render those items, and adds the small legacy fanout helpers needed for
existing event-based consumers. No core producer or rollout persistence
behavior changes here, that will be done in a followup.
## Making ThreadHistoryBuilder stateless
This is the first PR in a stack to make `ThreadHistoryBuilder` stateless
enough that we can materialize app-server `ThreadItem`s from only a
given slice of `RolloutItem` history, without ever needing to replay the
whole thread from the beginning.
The persisted legacy `RolloutItem::EventMsg` records are mostly shaped
like live UI events, not like materialized `ThreadItem`s. They work if
we replay the full rollout in order, but they often do not contain
enough stable identity or complete item state to project an arbitrary
suffix on its own.
A few examples:
- `UserMessageEvent` and `AgentMessageEvent` have content, but
historically do not carry the persisted app-server item ID that should
become the SQLite primary key.
- `AgentReasoningEvent` and `AgentReasoningRawContentEvent` are
fragments. `ThreadHistoryBuilder` currently merges them into the last
reasoning item, which means a slice starting in the middle of reasoning
cannot know whether to append to an earlier item or create a new one.
- `WebSearchEndEvent`, `McpToolCallEndEvent`, collab end events, and
similar legacy events can often render a final-looking item, but they
usually rely on prior replay state to know which turn owns the item.
- Begin/end legacy events are partial views of one logical item. The
builder correlates them by `call_id` and mutates prior state to
synthesize the final `ThreadItem`.
That is the problem this direction fixes. A persisted canonical
lifecycle record looks much closer to the read model we actually want
later:
```rust
ItemCompletedEvent {
turn_id,
item: TurnItem { id, ...full snapshot... },
completed_at_ms,
}
```
Once rollout has explicit `turn_id`, stable `item.id`, and a canonical
completed item snapshot, the future SQLite projector can reduce only the
new rollout suffix and upsert the affected `thread_items` rows. It no
longer needs to synthesize `item-N`, infer item ownership from the
active turn, or replay earlier events just to reconstruct the current
item snapshot.
## What changed
- Added core `TurnItem` variants and item structs for command execution,
dynamic tool calls, collab agent tool calls, and sub-agent activity.
- Added conversions from those canonical items back into the legacy
event shapes where current consumers still need them.
- Added app-server v2 `ThreadItem` conversion for the new core item
variants.
- Taught `ThreadHistoryBuilder` and rollout persistence metrics to
recognize the new item variants.
## Follow-up
The next PR https://github.com/openai/codex/pull/30283 switches the live
core producers for these item families onto canonical `ItemStarted` /
`ItemCompleted` events.
This commit is contained in:
committed by
GitHub
Unverified
parent
1168254bd9
commit
a107b84967
@@ -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<CommandAction> {
|
||||
pub(crate) fn command_actions_for_path_uri(
|
||||
parsed_cmd: &[ParsedCommand],
|
||||
cwd: &PathUri,
|
||||
) -> Vec<CommandAction> {
|
||||
// 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()) {
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
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";
|
||||
|
||||
@@ -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<CoreTurnItem> 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<CoreExecCommandStatus> for CommandExecutionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreCommandExecutionStatus> 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<CoreMcpToolCallStatus> for McpToolCallStatus {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreDynamicToolCallStatus> 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<CoreCollabAgentTool> 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<CoreCollabAgentToolCallStatus> 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<codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem>
|
||||
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<DynamicToolCallOutputContentItem>
|
||||
for codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathUri,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
pub source: ExecCommandSource,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub interaction_input: Option<String>,
|
||||
pub status: CommandExecutionStatus,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub stdout: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub stderr: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub aggregated_output: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub exit_code: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string", optional)]
|
||||
pub duration: Option<Duration>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub formatted_output: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
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<Vec<DynamicToolCallOutputContentItem>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub success: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub error: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string", optional)]
|
||||
pub duration: Option<Duration>,
|
||||
}
|
||||
|
||||
#[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<ThreadId>,
|
||||
#[serde(default)]
|
||||
pub receiver_agents: Vec<CollabAgentRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub prompt: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub model: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
#[serde(default)]
|
||||
pub agents_states: HashMap<ThreadId, AgentStatus>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user