Consolidate command execution output into aggregated_output (#50402)

## What changed

- Remove `stdout`, `stderr`, and `formatted_output` from `CommandExecutionItem` and `ExecCommandEndEvent`, retaining `aggregated_output` as the output field.
- Update command event producers, legacy conversion, rollout traces, and the precomputed schema. Use aggregated output for terminal trace results.
- Clear `interaction_input` in emitted and migrated command completion items.

## Testing

Update command execution and trace tests to use aggregated output, and assert that legacy rollout migration preserves `aggregatedOutput`.

GitOrigin-RevId: ed59827d4a5ec9a767084ee4feac72c2042440f2
This commit is contained in:
Owen Lin
2026-10-02 18:48:49 +00:00
committed by copyberry
parent d61c7a824f
commit c5d242fa79
20 changed files with 47 additions and 167 deletions
@@ -2408,12 +2408,9 @@ mod tests {
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 {
@@ -2456,12 +2453,9 @@ mod tests {
parsed_cmd,
source: ExecCommandSource::Agent,
interaction_input: None,
stdout: "hello world\n".to_string(),
stderr: String::new(),
aggregated_output: "hello world\n".to_string(),
exit_code: 0,
duration: Duration::from_millis(12),
formatted_output: "hello world\n".to_string(),
status: CoreExecCommandStatus::Completed,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
@@ -3145,12 +3139,9 @@ mod tests {
}],
source: ExecCommandSource::Agent,
interaction_input: None,
stdout: String::new(),
stderr: String::new(),
aggregated_output: "hello world\n".into(),
exit_code: 0,
duration: Duration::from_millis(12),
formatted_output: String::new(),
status: CoreExecCommandStatus::Completed,
}),
EventMsg::McpToolCallEnd(McpToolCallEndEvent {
@@ -3439,12 +3430,9 @@ mod tests {
parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }],
source: ExecCommandSource::Agent,
interaction_input: None,
stdout: String::new(),
stderr: "exec command rejected by user".into(),
aggregated_output: "exec command rejected by user".into(),
exit_code: -1,
duration: Duration::ZERO,
formatted_output: String::new(),
status: CoreExecCommandStatus::Declined,
}),
EventMsg::PatchApplyEnd(PatchApplyEndEvent {
@@ -3833,12 +3821,9 @@ mod tests {
}],
source: ExecCommandSource::Agent,
interaction_input: None,
stdout: "done\n".into(),
stderr: String::new(),
aggregated_output: "done\n".into(),
exit_code: 0,
duration: Duration::from_millis(5),
formatted_output: "done\n".into(),
status: CoreExecCommandStatus::Completed,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
@@ -3943,12 +3928,9 @@ mod tests {
}],
source: ExecCommandSource::Agent,
interaction_input: None,
stdout: "done\n".into(),
stderr: String::new(),
aggregated_output: "done\n".into(),
exit_code: 0,
duration: Duration::from_millis(5),
formatted_output: "done\n".into(),
status: CoreExecCommandStatus::Completed,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
@@ -3330,12 +3330,9 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
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!(
+1 -1
View File
@@ -1710,7 +1710,7 @@ async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::R
let event = rx.recv().await.expect("channel open");
if let EventMsg::ExecCommandEnd(event) = event.msg {
assert_eq!(event.exit_code, 0);
assert_eq!(event.stdout.trim(), "not-set");
assert_eq!(event.aggregated_output.trim(), "not-set");
break;
}
}
-19
View File
@@ -21,7 +21,6 @@ use crate::session::TurnInput;
use crate::session::turn_context::TurnContext;
use crate::shell::Shell;
use crate::state::TaskKind;
use crate::tools::format_exec_output_str;
use crate::tools::runtimes::RuntimePathPrepends;
#[cfg(unix)]
use crate::tools::runtimes::apply_package_path_prepend;
@@ -200,12 +199,9 @@ pub(crate) async fn execute_user_shell_command(
source: ExecCommandSource::UserShell,
interaction_input: None,
status: CommandExecutionStatus::InProgress,
stdout: None,
stderr: None,
aggregated_output: None,
exit_code: None,
duration: None,
formatted_output: None,
}),
)
.await;
@@ -281,12 +277,9 @@ pub(crate) async fn execute_user_shell_command(
source: ExecCommandSource::UserShell,
interaction_input: None,
status: CommandExecutionStatus::Failed,
stdout: Some(String::new()),
stderr: Some(aborted_message.clone()),
aggregated_output: Some(aborted_message.clone()),
exit_code: Some(-1),
duration: Some(Duration::ZERO),
formatted_output: Some(aborted_message),
}),
)
.await;
@@ -312,15 +305,9 @@ pub(crate) async fn execute_user_shell_command(
} else {
CommandExecutionStatus::Failed
},
stdout: Some(output.stdout.text.clone()),
stderr: Some(output.stderr.text.clone()),
aggregated_output: Some(output.aggregated_output.text.clone()),
exit_code: Some(output.exit_code),
duration: Some(output.duration),
formatted_output: Some(format_exec_output_str(
&output,
turn_context.model_info().truncation_policy.into(),
)),
}),
)
.await;
@@ -355,15 +342,9 @@ pub(crate) async fn execute_user_shell_command(
source: ExecCommandSource::UserShell,
interaction_input: None,
status: CommandExecutionStatus::Failed,
stdout: Some(exec_output.stdout.text.clone()),
stderr: Some(exec_output.stderr.text.clone()),
aggregated_output: Some(exec_output.aggregated_output.text.clone()),
exit_code: Some(exec_output.exit_code),
duration: Some(exec_output.duration),
formatted_output: Some(format_exec_output_str(
&exec_output,
turn_context.model_info().truncation_policy.into(),
)),
}),
)
.await;
+4 -27
View File
@@ -34,8 +34,6 @@ use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use super::format_exec_output_str;
const REJECTION_MESSAGE_MAX_TOKENS: usize = 900;
pub(super) fn truncate_rejection_message(message: &str) -> String {
@@ -185,12 +183,9 @@ async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: &ExecCommand
source: exec_input.source,
interaction_input: exec_input.interaction_input.map(str::to_owned),
status: CommandExecutionStatus::InProgress,
stdout: None,
stderr: None,
aggregated_output: None,
exit_code: None,
duration: None,
formatted_output: None,
}),
)
.await;
@@ -511,12 +506,9 @@ impl<'a> ExecCommandInput<'a> {
}
struct ExecCommandResult {
stdout: String,
stderr: String,
aggregated_output: String,
exit_code: i32,
duration: Duration,
formatted_output: String,
status: ExecCommandStatus,
}
@@ -532,15 +524,9 @@ async fn emit_exec_stage(
ToolEventStage::Success { output, .. }
| ToolEventStage::Failure(ToolEventFailure::Output(output)) => {
let exec_result = ExecCommandResult {
stdout: output.stdout.text.clone(),
stderr: output.stderr.text.clone(),
aggregated_output: output.aggregated_output.text.clone(),
aggregated_output: output.aggregated_output.text,
exit_code: output.exit_code,
duration: output.duration,
formatted_output: format_exec_output_str(
&output,
ctx.model_info.truncation_policy.into(),
),
status: if output.exit_code == 0 {
ExecCommandStatus::Completed
} else {
@@ -552,12 +538,9 @@ async fn emit_exec_stage(
ToolEventStage::Failure(ToolEventFailure::Message(message)) => {
let text = message.to_string();
let exec_result = ExecCommandResult {
stdout: String::new(),
stderr: text.clone(),
aggregated_output: text.clone(),
aggregated_output: text,
exit_code: -1,
duration: Duration::ZERO,
formatted_output: text,
status: ExecCommandStatus::Failed,
};
emit_exec_end(ctx, exec_input, exec_result).await;
@@ -565,12 +548,9 @@ async fn emit_exec_stage(
ToolEventStage::Failure(ToolEventFailure::Rejected { message, .. }) => {
let text = message.to_string();
let exec_result = ExecCommandResult {
stdout: String::new(),
stderr: text.clone(),
aggregated_output: text.clone(),
aggregated_output: text,
exit_code: -1,
duration: Duration::ZERO,
formatted_output: text,
status: ExecCommandStatus::Declined,
};
emit_exec_end(ctx, exec_input, exec_result).await;
@@ -598,14 +578,11 @@ async fn emit_exec_end(
cwd: exec_input.cwd.clone(),
parsed_cmd: exec_input.parsed_cmd.to_vec(),
source: exec_input.source,
interaction_input: exec_input.interaction_input.map(str::to_owned),
interaction_input: None,
status: exec_result.status.into(),
stdout: Some(exec_result.stdout),
stderr: Some(exec_result.stderr),
aggregated_output: Some(exec_result.aggregated_output),
exit_code: Some(exec_result.exit_code),
duration: Some(exec_result.duration),
formatted_output: Some(exec_result.formatted_output),
}),
)
.await;
@@ -357,13 +357,6 @@ async fn exit_watcher_waits_for_late_network_denial_before_classifying_end() ->
Some("LATE_DENIAL")
)
);
assert_eq!(
item.formatted_output,
Some(codex_utils_output_truncation::formatted_truncate_text(
"LATE_DENIAL",
codex_utils_output_truncation::TruncationPolicy::Bytes(4),
))
);
assert!(
elapsed >= Duration::from_millis(10) && elapsed < TRAILING_OUTPUT_GRACE,
"completion should wait for denial without falling back to the output grace: {elapsed:?}"
+17 -11
View File
@@ -905,7 +905,7 @@ async fn shell_snapshot_v2_filters_profile_secrets_without_creating_files() -> R
assert_eq!(end.exit_code, 0);
assert_eq!(
normalize_newlines(&end.stdout).trim(),
normalize_newlines(&end.aggregated_output).trim(),
"helper|path|policy|missing"
);
}
@@ -953,7 +953,7 @@ async fn shell_snapshot_v2_preserves_legacy_snapshots_for_user_shell() -> Result
.await;
assert_eq!(end.exit_code, 0);
assert_eq!(normalize_newlines(&end.stdout).trim(), "legacy");
assert_eq!(normalize_newlines(&end.aggregated_output).trim(), "legacy");
Ok(())
}
@@ -962,7 +962,7 @@ async fn shell_snapshot_v2_preserves_legacy_snapshots_for_user_shell() -> Result
async fn linux_unified_exec_uses_shell_snapshot() -> Result<()> {
let command = "echo snapshot-linux";
let run = run_snapshot_command(command).await?;
let stdout = normalize_newlines(&run.end.stdout);
let output = normalize_newlines(&run.end.aggregated_output);
assert_eq!(run.begin.command.get(1).map(String::as_str), Some("-lc"));
assert_eq!(run.begin.command.get(2).map(String::as_str), Some(command));
@@ -971,8 +971,8 @@ async fn linux_unified_exec_uses_shell_snapshot() -> Result<()> {
assert_posix_snapshot_sections(&run.snapshot_content);
assert_eq!(run.end.exit_code, 0);
assert!(
stdout.contains("snapshot-linux"),
"stdout should contain snapshot marker; stdout={stdout:?}"
output.contains("snapshot-linux"),
"output should contain snapshot marker; output={output:?}"
);
Ok(())
@@ -1018,7 +1018,7 @@ async fn unified_exec_snapshot_preserves_shell_environment_policy_set() -> Resul
.await?;
assert_eq!(
normalize_newlines(&end.stdout).trim(),
normalize_newlines(&end.aggregated_output).trim(),
POLICY_SUCCESS_OUTPUT
);
assert_eq!(end.exit_code, 0);
@@ -1239,10 +1239,13 @@ async fn macos_unified_exec_resolves_command_from_tied_path_snapshot(
assert_eq!(
end.exit_code, 0,
"tied-path command failed: stderr={:?}",
end.stderr
"tied-path command failed: output={:?}",
end.aggregated_output
);
assert_eq!(
normalize_newlines(&end.aggregated_output).trim(),
"tied-path-command"
);
assert_eq!(normalize_newlines(&end.stdout).trim(), "tied-path-command");
Ok(())
}
@@ -1274,7 +1277,10 @@ async fn macos_unified_exec_uses_shell_snapshot() -> Result<()> {
assert!(run.snapshot_path.starts_with(&run.codex_home));
assert_posix_snapshot_sections(&run.snapshot_content);
assert_eq!(normalize_newlines(&run.end.stdout).trim(), "snapshot-macos");
assert_eq!(
normalize_newlines(&run.end.aggregated_output).trim(),
"snapshot-macos"
);
assert_eq!(run.end.exit_code, 0);
Ok(())
@@ -1308,7 +1314,7 @@ async fn windows_unified_exec_uses_shell_snapshot() -> Result<()> {
assert!(run.snapshot_content.contains("# aliases "));
assert!(run.snapshot_content.contains("# exports "));
assert_eq!(
normalize_newlines(&run.end.stdout).trim(),
normalize_newlines(&run.end.aggregated_output).trim(),
"snapshot-windows"
);
assert_eq!(run.end.exit_code, 0);
@@ -2815,8 +2815,6 @@ async fn captured_step_controls_exec_completion_and_write_stdin_output() -> Resu
.await;
let end = end.expect("exec completion");
use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_output_truncation::formatted_truncate_text;
let requests = responses.requests();
let exec = requests[2]
.function_call_output_text("exec-b")
@@ -2836,13 +2834,7 @@ async fn captured_step_controls_exec_completion_and_write_stdin_output() -> Resu
"{stdin}"
);
assert_eq!(end.exit_code, 7);
// The formatting check needs an untruncated chunk, independent of how the executor
// aggregates output across the initial command and later stdin interactions.
assert!(end.aggregated_output.contains(&output));
assert_eq!(
end.formatted_output,
formatted_truncate_text(&end.aggregated_output, TruncationPolicy::Bytes(400))
);
Ok(())
}
+2 -6
View File
@@ -1113,12 +1113,8 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> {
);
let end_event = end_event.expect("expected command completion");
assert_eq!(
(
end_event.exit_code,
end_event.stdout,
end_event.aggregated_output
),
(0, "HELLO-UEXECé�".to_string(), "HELLO-UEXECé�".to_string())
(end_event.exit_code, end_event.aggregated_output),
(0, "HELLO-UEXECé�".to_string())
);
Ok(())
}
+12 -11
View File
@@ -77,15 +77,17 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() {
.unwrap();
let msg = wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExecCommandEnd(_))).await;
let EventMsg::ExecCommandEnd(ExecCommandEndEvent {
stdout, exit_code, ..
aggregated_output,
exit_code,
..
}) = msg
else {
unreachable!()
};
assert_eq!(exit_code, 0);
assert!(
stdout.contains(file_name),
"ls output should include {file_name}, got: {stdout:?}"
aggregated_output.contains(file_name),
"ls output should include {file_name}, got: {aggregated_output:?}"
);
// 2) shell command should print the file contents verbatim
@@ -99,7 +101,7 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() {
.unwrap();
let msg = wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExecCommandEnd(_))).await;
let EventMsg::ExecCommandEnd(ExecCommandEndEvent {
mut stdout,
mut aggregated_output,
exit_code,
..
}) = msg
@@ -109,9 +111,9 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() {
assert_eq!(exit_code, 0);
if cfg!(windows) {
// Windows shells emit CRLF line endings; normalize so the assertion remains portable.
stdout = stdout.replace("\r\n", "\n");
aggregated_output = aggregated_output.replace("\r\n", "\n");
}
assert_eq!(stdout, contents);
assert_eq!(aggregated_output, contents);
}
#[tokio::test]
@@ -447,7 +449,7 @@ async fn user_shell_command_history_is_persisted_and_shared_with_model() -> anyh
})
.await;
assert_eq!(end_event.exit_code, 0);
assert_eq!(end_event.stdout.trim(), "not-set");
assert_eq!(end_event.aggregated_output.trim(), "not-set");
let _ = wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -507,8 +509,7 @@ async fn user_shell_command_does_not_set_network_sandbox_env_var() -> anyhow::Re
let ExecCommandEndEvent {
exit_code,
stdout,
stderr,
aggregated_output,
..
} = wait_for_event_match(&test.codex, |ev| match ev {
EventMsg::ExecCommandEnd(event) => Some(event.clone()),
@@ -518,9 +519,9 @@ async fn user_shell_command_does_not_set_network_sandbox_env_var() -> anyhow::Re
assert_eq!(
exit_code, 0,
"shell command should execute successfully. stdout=`{stdout}`, stderr=`{stderr}`",
"shell command should execute successfully. output=`{aggregated_output}`",
);
assert_eq!(stdout.trim(), "not-set");
assert_eq!(aggregated_output.trim(), "not-set");
Ok(())
}
-9
View File
@@ -271,12 +271,6 @@ pub struct CommandExecutionItem {
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)]
@@ -284,9 +278,6 @@ pub struct CommandExecutionItem {
#[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)]
-3
View File
@@ -222,12 +222,9 @@ impl CommandExecutionItem {
parsed_cmd: self.parsed_cmd.clone(),
source: self.source,
interaction_input: self.interaction_input.clone(),
stdout: self.stdout.clone().unwrap_or_default(),
stderr: self.stderr.clone().unwrap_or_default(),
aggregated_output: self.aggregated_output.clone().unwrap_or_default(),
exit_code: self.exit_code.unwrap_or_default(),
duration: self.duration.unwrap_or_default(),
formatted_output: self.formatted_output.clone().unwrap_or_default(),
status,
}))
}
-12
View File
@@ -3625,10 +3625,6 @@ pub struct ExecCommandEndEvent {
#[ts(optional)]
pub interaction_input: Option<String>,
/// Captured stdout
pub stdout: String,
/// Captured stderr
pub stderr: String,
/// Captured aggregated output
#[serde(default)]
pub aggregated_output: String,
@@ -3637,8 +3633,6 @@ pub struct ExecCommandEndEvent {
/// The duration of the command execution.
#[ts(type = "string")]
pub duration: Duration,
/// Formatted output from the command, as seen by the model.
pub formatted_output: String,
/// Completion status for this command execution.
pub status: ExecCommandStatus,
}
@@ -5598,12 +5592,9 @@ mod tests {
source: ExecCommandSource::Agent,
interaction_input: None,
status: CommandExecutionStatus::InProgress,
stdout: None,
stderr: None,
aggregated_output: None,
exit_code: None,
duration: None,
formatted_output: None,
}),
};
let completed = ItemCompletedEvent {
@@ -5626,12 +5617,9 @@ mod tests {
source: ExecCommandSource::Agent,
interaction_input: None,
status: CommandExecutionStatus::Completed,
stdout: Some("done\n".into()),
stderr: Some(String::new()),
aggregated_output: Some("done\n".into()),
exit_code: Some(0),
duration: Some(Duration::from_millis(5)),
formatted_output: Some("done\n".into()),
}),
};
@@ -220,12 +220,9 @@ struct ExecCommandEndTracePayload<'a> {
source: ExecCommandSource,
#[serde(skip_serializing_if = "Option::is_none")]
interaction_input: Option<&'a str>,
stdout: &'a str,
stderr: &'a str,
aggregated_output: &'a str,
exit_code: i32,
duration: Duration,
formatted_output: &'a str,
status: &'a ExecCommandStatus,
}
@@ -243,12 +240,9 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> {
parsed_cmd,
source,
interaction_input,
stdout,
stderr,
aggregated_output,
exit_code,
duration,
formatted_output,
status,
} = event;
Self {
@@ -263,12 +257,9 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> {
parsed_cmd,
source: *source,
interaction_input: interaction_input.as_deref(),
stdout,
stderr,
aggregated_output,
exit_code: *exit_code,
duration: *duration,
formatted_output,
status,
}
}
@@ -93,12 +93,9 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> {
parsed_cmd: Vec::new(),
source: ExecCommandSource::UnifiedExecInteraction,
interaction_input: Some("input".to_string()),
stdout: "output".to_string(),
stderr: String::new(),
aggregated_output: "output".to_string(),
aggregated_output: "combined output".to_string(),
exit_code: 0,
duration: Duration::from_millis(250),
formatted_output: "output".to_string(),
status: ExecCommandStatus::Completed,
});
@@ -138,12 +135,9 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> {
"parsed_cmd": [],
"source": "unified_exec_interaction",
"interaction_input": "input",
"stdout": "output",
"stderr": "",
"aggregated_output": "output",
"aggregated_output": "combined output",
"exit_code": 0,
"duration": {"secs": 0, "nanos": 250000000},
"formatted_output": "output",
"status": "completed"
})
);
@@ -450,9 +450,9 @@ fn parse_protocol_terminal_response(payload: ExecCommandEndPayload) -> ParsedTer
terminal_id: payload.process_id,
result: TerminalResult {
exit_code: Some(payload.exit_code),
stdout: payload.stdout,
stderr: payload.stderr,
formatted_output: Some(payload.formatted_output),
stdout: payload.aggregated_output,
stderr: String::new(),
formatted_output: None,
original_token_count: None,
chunk_id: None,
},
@@ -557,10 +557,9 @@ struct ExecCommandBeginPayload {
#[derive(Deserialize)]
struct ExecCommandEndPayload {
process_id: Option<String>,
stdout: String,
stderr: String,
#[serde(default)]
aggregated_output: String,
exit_code: i32,
formatted_output: String,
}
#[derive(Deserialize)]
@@ -81,10 +81,8 @@ fn exec_tool_reduces_to_terminal_operation_and_session() -> anyhow::Result<()> {
"turn_id": "turn-1",
"command": ["cargo", "test"],
"cwd": "/repo",
"stdout": "ok\n",
"stderr": "",
"aggregated_output": "ok\n",
"exit_code": 0,
"formatted_output": "ok\n",
"status": "completed"
}),
)?;
@@ -180,7 +178,7 @@ fn exec_tool_reduces_to_terminal_operation_and_session() -> anyhow::Result<()> {
exit_code: Some(0),
stdout: "ok\n".to_string(),
stderr: String::new(),
formatted_output: Some("ok\n".to_string()),
formatted_output: None,
original_token_count: None,
chunk_id: None,
}),
@@ -277,16 +277,12 @@ pub(super) fn completed_item(
cwd: event.cwd.clone(),
parsed_cmd: event.parsed_cmd.clone(),
source: event.source,
interaction_input: event.interaction_input.clone(),
interaction_input: None,
status: event.status.clone().into(),
stdout: (!event.stdout.is_empty()).then(|| event.stdout.clone()),
stderr: (!event.stderr.is_empty()).then(|| event.stderr.clone()),
aggregated_output: (!event.aggregated_output.is_empty())
.then(|| event.aggregated_output.clone()),
exit_code: Some(event.exit_code),
duration: Some(event.duration),
formatted_output: (!event.formatted_output.is_empty())
.then(|| event.formatted_output.clone()),
}),
Some(event.turn_id.clone()),
)),
@@ -471,6 +471,7 @@ async fn migration_projects_explicit_and_implicit_legacy_completed_items() {
let reasoning: serde_json::Value =
serde_json::from_slice(&items.items[1].item_json).expect("parse projected reasoning");
assert_eq!(command["type"], "commandExecution");
assert_eq!(command["aggregatedOutput"], "ok");
assert_eq!(reasoning["type"], "reasoning");
assert_eq!(reasoning["summary"], json!(["summary"]));
assert_eq!(reasoning["content"], json!(["raw"]));