[codex exec] Add item.started and support it for command execution (#4250)

Adds a new `item.started` event to `codex exec` and implements it for
command_execution item type.

```jsonl
{"type":"session.created","session_id":"019982d1-75f0-7920-b051-e0d3731a5ed8"}
{"type":"item.completed","item":{"id":"item_0","item_type":"reasoning","text":"**Executing commands securely**\n\nI'm thinking about how the default harness typically uses \"bash -lc,\" while historically \"bash\" is what we've been using. The command should be executed as a string in our CLI, so using \"bash -lc 'echo hello'\" is optimal but calling \"echo hello\" directly feels safer. The sandbox makes sure environment variables like CODEX_SANDBOX_NETWORK_DISABLED=1 are set, so I won't ask for approval. I just need to run \"echo hello\" and correctly present the output."}}
{"type":"item.completed","item":{"id":"item_1","item_type":"reasoning","text":"**Preparing for tool calls**\n\nI realize that I need to include a preamble before making any tool calls. So, I'll first state the preamble in the commentary channel, then proceed with the tool call. After that, I need to present the final message along with the output. It's possible that the CLI will show the output inline, but I must ensure that I present the result clearly regardless. Let's move forward and get this organized!"}}
{"type":"item.completed","item":{"id":"item_2","item_type":"assistant_message","text":"Running `echo` to confirm shell access and print output."}}
{"type":"item.started","item":{"id":"item_3","item_type":"command_execution","command":"bash -lc echo hello","aggregated_output":"","exit_code":null,"status":"in_progress"}}
{"type":"item.completed","item":{"id":"item_3","item_type":"command_execution","command":"bash -lc echo hello","aggregated_output":"hello\n","exit_code":0,"status":"completed"}}
{"type":"item.completed","item":{"id":"item_4","item_type":"assistant_message","text":"hello"}}
```
This commit is contained in:
pakrym-oai
2025-09-25 15:25:02 -07:00
committed by GitHub
parent 7355ca48c5
commit 67aab04c66
3 changed files with 115 additions and 18 deletions

View File

@@ -16,6 +16,7 @@ use codex_exec::exec_events::ConversationEvent;
use codex_exec::exec_events::ConversationItem;
use codex_exec::exec_events::ConversationItemDetails;
use codex_exec::exec_events::ItemCompletedEvent;
use codex_exec::exec_events::ItemStartedEvent;
use codex_exec::exec_events::PatchApplyStatus;
use codex_exec::exec_events::PatchChangeKind;
use codex_exec::exec_events::ReasoningItem;
@@ -156,7 +157,20 @@ fn exec_command_end_success_produces_completed_command_item() {
}),
);
let out_begin = ep.collect_conversation_events(&begin);
assert!(out_begin.is_empty());
assert_eq!(
out_begin,
vec![ConversationEvent::ItemStarted(ItemStartedEvent {
item: ConversationItem {
id: "item_0".to_string(),
details: ConversationItemDetails::CommandExecution(CommandExecutionItem {
command: "bash -lc 'echo hi'".to_string(),
aggregated_output: String::new(),
exit_code: None,
status: CommandExecutionStatus::InProgress,
}),
},
})]
);
// End (success) -> item.completed (item_0)
let end_ok = event(
@@ -178,9 +192,9 @@ fn exec_command_end_success_produces_completed_command_item() {
item: ConversationItem {
id: "item_0".to_string(),
details: ConversationItemDetails::CommandExecution(CommandExecutionItem {
command: "bash -lc echo hi".to_string(),
command: "bash -lc 'echo hi'".to_string(),
aggregated_output: "hi\n".to_string(),
exit_code: 0,
exit_code: Some(0),
status: CommandExecutionStatus::Completed,
}),
},
@@ -202,7 +216,20 @@ fn exec_command_end_failure_produces_failed_command_item() {
parsed_cmd: Vec::new(),
}),
);
assert!(ep.collect_conversation_events(&begin).is_empty());
assert_eq!(
ep.collect_conversation_events(&begin),
vec![ConversationEvent::ItemStarted(ItemStartedEvent {
item: ConversationItem {
id: "item_0".to_string(),
details: ConversationItemDetails::CommandExecution(CommandExecutionItem {
command: "sh -c 'exit 1'".to_string(),
aggregated_output: String::new(),
exit_code: None,
status: CommandExecutionStatus::InProgress,
}),
},
})]
);
// End (failure) -> item.completed (item_0)
let end_fail = event(
@@ -224,9 +251,9 @@ fn exec_command_end_failure_produces_failed_command_item() {
item: ConversationItem {
id: "item_0".to_string(),
details: ConversationItemDetails::CommandExecution(CommandExecutionItem {
command: "sh -c exit 1".to_string(),
command: "sh -c 'exit 1'".to_string(),
aggregated_output: String::new(),
exit_code: 1,
exit_code: Some(1),
status: CommandExecutionStatus::Failed,
}),
},
@@ -234,6 +261,27 @@ fn exec_command_end_failure_produces_failed_command_item() {
);
}
#[test]
fn exec_command_end_without_begin_is_ignored() {
let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None);
// End event arrives without a prior Begin; should produce no conversation events.
let end_only = event(
"c1",
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "no-begin".to_string(),
stdout: String::new(),
stderr: String::new(),
aggregated_output: String::new(),
exit_code: 0,
duration: Duration::from_millis(1),
formatted_output: String::new(),
}),
);
let out = ep.collect_conversation_events(&end_only);
assert!(out.is_empty());
}
#[test]
fn patch_apply_success_produces_item_completed_patchapply() {
let mut ep = ExperimentalEventProcessorWithJsonOutput::new(None);