Agent: Include partial output if terminal tool fails (#29115)

This PR addresses the behavior of the agent's terminal tool when the
executed command is interrupted or fails after producing some output.
Currently, if the command doesn't finish successfully, any partial
output captured before the interruption/failure is discarded, and only
an error message (or a generic cancellation message) is returned to the
LLM.

This change modifies the `run_command_limited` function in the terminal
tool to catch errors when awaiting the command's status (which includes
interruptions). In the case of such an error, it now includes any
partial stdout/stderr captured up to that point within the error message
returned to the `ToolUseState`. This ensures the LLM receives the
partial context even when the command doesn't complete cleanly, framed
appropriately as part of an error/interruption message.

Closes #29101

Release Notes:

- N/A
This commit is contained in:
Mani Rash Ahmadi 2025-04-28 11:25:11 -04:00 committed by GitHub
parent e98e6c7426
commit 68e0105627
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -202,39 +202,52 @@ async fn run_command_limited(working_dir: Arc<Path>, command: String) -> Result<
consume_reader(out_reader, truncated).await?; consume_reader(out_reader, truncated).await?;
consume_reader(err_reader, truncated).await?; consume_reader(err_reader, truncated).await?;
let status = cmd.status().await.context("Failed to get command status")?; // Handle potential errors during status retrieval, including interruption.
match cmd.status().await {
Ok(status) => {
let output_string = if truncated {
// Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in
// multi-byte characters.
let last_line_ix = combined_buffer.bytes().rposition(|b| b == b'\n');
let buffer_content =
&combined_buffer[..last_line_ix.unwrap_or(combined_buffer.len())];
let output_string = if truncated { format!(
// Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in "Command output too long. The first {} bytes:\n\n{}",
// multi-byte characters. buffer_content.len(),
let last_line_ix = combined_buffer.bytes().rposition(|b| b == b'\n'); output_block(buffer_content),
let combined_buffer = &combined_buffer[..last_line_ix.unwrap_or(combined_buffer.len())]; )
} else {
output_block(&combined_buffer)
};
format!( let output_with_status = if status.success() {
"Command output too long. The first {} bytes:\n\n{}", if output_string.is_empty() {
combined_buffer.len(), "Command executed successfully.".to_string()
output_block(&combined_buffer), } else {
) output_string
} else { }
output_block(&combined_buffer) } else {
}; format!(
"Command failed with exit code {} (shell: {}).\n\n{}",
status.code().unwrap_or(-1),
shell,
output_string,
)
};
let output_with_status = if status.success() { Ok(output_with_status)
if output_string.is_empty() {
"Command executed successfully.".to_string()
} else {
output_string.to_string()
} }
} else { Err(err) => {
format!( // Error occurred getting status (potential interruption). Include partial output.
"Command failed with exit code {} (shell: {}).\n\n{}", let partial_output = output_block(&combined_buffer);
status.code().unwrap_or(-1), let error_message = format!(
shell, "Command failed or was interrupted.\nPartial output captured:\n\n{}",
output_string, partial_output
) );
}; Err(anyhow!(err).context(error_message))
}
Ok(output_with_status) }
} }
async fn consume_reader<T: AsyncReadExt + Unpin>( async fn consume_reader<T: AsyncReadExt + Unpin>(