Skip to content

Commit 4f01798

Browse files
committed
feat(core): emit canonical turn item lifecycle events
1 parent 2cefeeb commit 4f01798

22 files changed

Lines changed: 1237 additions & 552 deletions

codex-rs/app-server/src/bespoke_event_handling.rs

Lines changed: 228 additions & 148 deletions
Large diffs are not rendered by default.

codex-rs/core/src/session/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,6 +1775,9 @@ impl Session {
17751775

17761776
let show_raw_agent_reasoning = self.show_raw_agent_reasoning();
17771777
for legacy in legacy_source.as_legacy_events(show_raw_agent_reasoning) {
1778+
self.services
1779+
.rollout_thread_trace
1780+
.record_tool_call_event(turn_context.sub_id.clone(), &legacy);
17781781
let legacy_event = Event {
17791782
id: turn_context.sub_id.clone(),
17801783
msg: legacy,

codex-rs/core/src/tasks/user_shell.rs

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,15 @@ use crate::tools::runtimes::RuntimePathPrepends;
2525
use crate::tools::runtimes::apply_package_path_prepend;
2626
use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot;
2727
use crate::tools::runtimes::strip_managed_proxy_env;
28-
use crate::turn_timing::now_unix_timestamp_ms;
2928
use crate::user_shell_command::user_shell_command_record_item;
3029
use codex_protocol::exec_output::ExecToolCallOutput;
3130
use codex_protocol::exec_output::StreamOutput;
31+
use codex_protocol::items::CommandExecutionItem;
32+
use codex_protocol::items::CommandExecutionStatus;
33+
use codex_protocol::items::TurnItem;
3234
use codex_protocol::protocol::ErrorEvent;
3335
use codex_protocol::protocol::EventMsg;
34-
use codex_protocol::protocol::ExecCommandBeginEvent;
35-
use codex_protocol::protocol::ExecCommandEndEvent;
3636
use codex_protocol::protocol::ExecCommandSource;
37-
use codex_protocol::protocol::ExecCommandStatus;
3837
use codex_protocol::protocol::TurnStartedEvent;
3938
use codex_sandboxing::SandboxType;
4039
use codex_shell_command::parse_command::parse_command;
@@ -181,18 +180,23 @@ pub(crate) async fn execute_user_shell_command(
181180

182181
let parsed_cmd = parse_command(&display_command);
183182
session
184-
.send_event(
183+
.emit_turn_item_started(
185184
turn_context.as_ref(),
186-
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
187-
call_id: call_id.clone(),
185+
&TurnItem::CommandExecution(CommandExecutionItem {
186+
id: call_id.clone(),
188187
process_id: None,
189-
turn_id: turn_context.sub_id.clone(),
190-
started_at_ms: now_unix_timestamp_ms(),
191188
command: display_command.clone(),
192189
cwd: cwd.clone().into(),
193190
parsed_cmd: parsed_cmd.clone(),
194191
source: ExecCommandSource::UserShell,
195192
interaction_input: None,
193+
status: CommandExecutionStatus::InProgress,
194+
stdout: None,
195+
stderr: None,
196+
aggregated_output: None,
197+
exit_code: None,
198+
duration: None,
199+
formatted_output: None,
196200
}),
197201
)
198202
.await;
@@ -259,57 +263,53 @@ pub(crate) async fn execute_user_shell_command(
259263
)
260264
.await;
261265
session
262-
.send_event(
266+
.emit_turn_item_completed(
263267
turn_context.as_ref(),
264-
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
265-
call_id,
268+
TurnItem::CommandExecution(CommandExecutionItem {
269+
id: call_id,
266270
process_id: None,
267-
turn_id: turn_context.sub_id.clone(),
268-
completed_at_ms: now_unix_timestamp_ms(),
269271
command: display_command.clone(),
270272
cwd: cwd.clone().into(),
271273
parsed_cmd: parsed_cmd.clone(),
272274
source: ExecCommandSource::UserShell,
273275
interaction_input: None,
274-
stdout: String::new(),
275-
stderr: aborted_message.clone(),
276-
aggregated_output: aborted_message.clone(),
277-
exit_code: -1,
278-
duration: Duration::ZERO,
279-
formatted_output: aborted_message,
280-
status: ExecCommandStatus::Failed,
276+
status: CommandExecutionStatus::Failed,
277+
stdout: Some(String::new()),
278+
stderr: Some(aborted_message.clone()),
279+
aggregated_output: Some(aborted_message.clone()),
280+
exit_code: Some(-1),
281+
duration: Some(Duration::ZERO),
282+
formatted_output: Some(aborted_message),
281283
}),
282284
)
283285
.await;
284286
}
285287
Ok(Ok(output)) => {
286288
session
287-
.send_event(
289+
.emit_turn_item_completed(
288290
turn_context.as_ref(),
289-
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
290-
call_id: call_id.clone(),
291+
TurnItem::CommandExecution(CommandExecutionItem {
292+
id: call_id.clone(),
291293
process_id: None,
292-
turn_id: turn_context.sub_id.clone(),
293-
completed_at_ms: now_unix_timestamp_ms(),
294294
command: display_command.clone(),
295295
cwd: cwd.clone().into(),
296296
parsed_cmd: parsed_cmd.clone(),
297297
source: ExecCommandSource::UserShell,
298298
interaction_input: None,
299-
stdout: output.stdout.text.clone(),
300-
stderr: output.stderr.text.clone(),
301-
aggregated_output: output.aggregated_output.text.clone(),
302-
exit_code: output.exit_code,
303-
duration: output.duration,
304-
formatted_output: format_exec_output_str(
305-
&output,
306-
turn_context.model_info.truncation_policy.into(),
307-
),
308299
status: if output.exit_code == 0 {
309-
ExecCommandStatus::Completed
300+
CommandExecutionStatus::Completed
310301
} else {
311-
ExecCommandStatus::Failed
302+
CommandExecutionStatus::Failed
312303
},
304+
stdout: Some(output.stdout.text.clone()),
305+
stderr: Some(output.stderr.text.clone()),
306+
aggregated_output: Some(output.aggregated_output.text.clone()),
307+
exit_code: Some(output.exit_code),
308+
duration: Some(output.duration),
309+
formatted_output: Some(format_exec_output_str(
310+
&output,
311+
turn_context.model_info.truncation_policy.into(),
312+
)),
313313
}),
314314
)
315315
.await;
@@ -329,28 +329,26 @@ pub(crate) async fn execute_user_shell_command(
329329
timed_out: false,
330330
};
331331
session
332-
.send_event(
332+
.emit_turn_item_completed(
333333
turn_context.as_ref(),
334-
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
335-
call_id,
334+
TurnItem::CommandExecution(CommandExecutionItem {
335+
id: call_id,
336336
process_id: None,
337-
turn_id: turn_context.sub_id.clone(),
338-
completed_at_ms: now_unix_timestamp_ms(),
339337
command: display_command,
340338
cwd: cwd.into(),
341339
parsed_cmd,
342340
source: ExecCommandSource::UserShell,
343341
interaction_input: None,
344-
stdout: exec_output.stdout.text.clone(),
345-
stderr: exec_output.stderr.text.clone(),
346-
aggregated_output: exec_output.aggregated_output.text.clone(),
347-
exit_code: exec_output.exit_code,
348-
duration: exec_output.duration,
349-
formatted_output: format_exec_output_str(
342+
status: CommandExecutionStatus::Failed,
343+
stdout: Some(exec_output.stdout.text.clone()),
344+
stderr: Some(exec_output.stderr.text.clone()),
345+
aggregated_output: Some(exec_output.aggregated_output.text.clone()),
346+
exit_code: Some(exec_output.exit_code),
347+
duration: Some(exec_output.duration),
348+
formatted_output: Some(format_exec_output_str(
350349
&exec_output,
351350
turn_context.model_info.truncation_policy.into(),
352-
),
353-
status: ExecCommandStatus::Failed,
351+
)),
354352
}),
355353
)
356354
.await;

codex-rs/core/src/tools/events.rs

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use codex_apply_patch::AppliedPatchDelta;
88
use codex_protocol::error::CodexErr;
99
use codex_protocol::error::SandboxErr;
1010
use codex_protocol::exec_output::ExecToolCallOutput;
11+
use codex_protocol::items::CommandExecutionItem;
12+
use codex_protocol::items::CommandExecutionStatus;
1113
use codex_protocol::items::FileChangeItem;
1214
use codex_protocol::items::TurnItem;
1315
use codex_protocol::parse_command::ParsedCommand;
@@ -102,19 +104,43 @@ pub(crate) async fn emit_exec_command_begin(
102104
interaction_input: Option<String>,
103105
process_id: Option<&str>,
104106
) {
107+
if matches!(source, ExecCommandSource::UnifiedExecInteraction) {
108+
ctx.session
109+
.send_event(
110+
ctx.turn,
111+
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
112+
call_id: ctx.call_id.to_string(),
113+
process_id: process_id.map(str::to_owned),
114+
turn_id: ctx.turn.sub_id.clone(),
115+
started_at_ms: now_unix_timestamp_ms(),
116+
command: command.to_vec(),
117+
cwd: cwd.clone(),
118+
parsed_cmd: parsed_cmd.to_vec(),
119+
source,
120+
interaction_input,
121+
}),
122+
)
123+
.await;
124+
return;
125+
}
105126
ctx.session
106-
.send_event(
127+
.emit_turn_item_started(
107128
ctx.turn,
108-
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
109-
call_id: ctx.call_id.to_string(),
129+
&TurnItem::CommandExecution(CommandExecutionItem {
130+
id: ctx.call_id.to_string(),
110131
process_id: process_id.map(str::to_owned),
111-
turn_id: ctx.turn.sub_id.clone(),
112-
started_at_ms: now_unix_timestamp_ms(),
113132
command: command.to_vec(),
114133
cwd: cwd.clone(),
115134
parsed_cmd: parsed_cmd.to_vec(),
116135
source,
117136
interaction_input,
137+
status: CommandExecutionStatus::InProgress,
138+
stdout: None,
139+
stderr: None,
140+
aggregated_output: None,
141+
exit_code: None,
142+
duration: None,
143+
formatted_output: None,
118144
}),
119145
)
120146
.await;
@@ -542,26 +568,50 @@ async fn emit_exec_end(
542568
exec_input: ExecCommandInput<'_>,
543569
exec_result: ExecCommandResult,
544570
) {
571+
if matches!(exec_input.source, ExecCommandSource::UnifiedExecInteraction) {
572+
ctx.session
573+
.send_event(
574+
ctx.turn,
575+
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
576+
call_id: ctx.call_id.to_string(),
577+
process_id: exec_input.process_id.map(str::to_owned),
578+
turn_id: ctx.turn.sub_id.clone(),
579+
completed_at_ms: now_unix_timestamp_ms(),
580+
command: exec_input.command.to_vec(),
581+
cwd: exec_input.cwd.clone(),
582+
parsed_cmd: exec_input.parsed_cmd.to_vec(),
583+
source: exec_input.source,
584+
interaction_input: exec_input.interaction_input.map(str::to_owned),
585+
stdout: exec_result.stdout,
586+
stderr: exec_result.stderr,
587+
aggregated_output: exec_result.aggregated_output,
588+
exit_code: exec_result.exit_code,
589+
duration: exec_result.duration,
590+
formatted_output: exec_result.formatted_output,
591+
status: exec_result.status,
592+
}),
593+
)
594+
.await;
595+
return;
596+
}
545597
ctx.session
546-
.send_event(
598+
.emit_turn_item_completed(
547599
ctx.turn,
548-
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
549-
call_id: ctx.call_id.to_string(),
600+
TurnItem::CommandExecution(CommandExecutionItem {
601+
id: ctx.call_id.to_string(),
550602
process_id: exec_input.process_id.map(str::to_owned),
551-
turn_id: ctx.turn.sub_id.clone(),
552-
completed_at_ms: now_unix_timestamp_ms(),
553603
command: exec_input.command.to_vec(),
554604
cwd: exec_input.cwd.clone(),
555605
parsed_cmd: exec_input.parsed_cmd.to_vec(),
556606
source: exec_input.source,
557607
interaction_input: exec_input.interaction_input.map(str::to_owned),
558-
stdout: exec_result.stdout,
559-
stderr: exec_result.stderr,
560-
aggregated_output: exec_result.aggregated_output,
561-
exit_code: exec_result.exit_code,
562-
duration: exec_result.duration,
563-
formatted_output: exec_result.formatted_output,
564-
status: exec_result.status,
608+
status: exec_result.status.into(),
609+
stdout: Some(exec_result.stdout),
610+
stderr: Some(exec_result.stderr),
611+
aggregated_output: Some(exec_result.aggregated_output),
612+
exit_code: Some(exec_result.exit_code),
613+
duration: Some(exec_result.duration),
614+
formatted_output: Some(exec_result.formatted_output),
565615
}),
566616
)
567617
.await;

0 commit comments

Comments
 (0)