Include output truncation message in tool call results (#2183)
To avoid model being confused about incomplete output.
This commit is contained in:
@@ -51,6 +51,7 @@ use crate::exec::ExecParams;
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::exec::SandboxType;
|
||||
use crate::exec::StdoutStream;
|
||||
use crate::exec::StreamOutput;
|
||||
use crate::exec::process_exec_tool_call;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
@@ -431,8 +432,8 @@ impl Session {
|
||||
// Because stdout and stderr could each be up to 100 KiB, we send
|
||||
// truncated versions.
|
||||
const MAX_STREAM_OUTPUT: usize = 5 * 1024; // 5KiB
|
||||
let stdout = stdout.chars().take(MAX_STREAM_OUTPUT).collect();
|
||||
let stderr = stderr.chars().take(MAX_STREAM_OUTPUT).collect();
|
||||
let stdout = stdout.text.chars().take(MAX_STREAM_OUTPUT).collect();
|
||||
let stderr = stderr.text.chars().take(MAX_STREAM_OUTPUT).collect();
|
||||
|
||||
let msg = if is_apply_patch {
|
||||
EventMsg::PatchApplyEnd(PatchApplyEndEvent {
|
||||
@@ -504,8 +505,8 @@ impl Session {
|
||||
Err(e) => {
|
||||
output_stderr = ExecToolCallOutput {
|
||||
exit_code: -1,
|
||||
stdout: String::new(),
|
||||
stderr: get_error_message_ui(e),
|
||||
stdout: StreamOutput::new(String::new()),
|
||||
stderr: StreamOutput::new(get_error_message_ui(e)),
|
||||
duration: Duration::default(),
|
||||
};
|
||||
&output_stderr
|
||||
@@ -1977,19 +1978,10 @@ async fn handle_container_exec_with_params(
|
||||
|
||||
match output_result {
|
||||
Ok(output) => {
|
||||
let ExecToolCallOutput {
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
duration,
|
||||
} = &output;
|
||||
let ExecToolCallOutput { exit_code, .. } = &output;
|
||||
|
||||
let is_success = *exit_code == 0;
|
||||
let content = format_exec_output(
|
||||
if is_success { stdout } else { stderr },
|
||||
*exit_code,
|
||||
*duration,
|
||||
);
|
||||
let content = format_exec_output(&output);
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
output: FunctionCallOutputPayload {
|
||||
@@ -2118,19 +2110,10 @@ async fn handle_sandbox_error(
|
||||
|
||||
match retry_output_result {
|
||||
Ok(retry_output) => {
|
||||
let ExecToolCallOutput {
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
duration,
|
||||
} = &retry_output;
|
||||
let ExecToolCallOutput { exit_code, .. } = &retry_output;
|
||||
|
||||
let is_success = *exit_code == 0;
|
||||
let content = format_exec_output(
|
||||
if is_success { stdout } else { stderr },
|
||||
*exit_code,
|
||||
*duration,
|
||||
);
|
||||
let content = format_exec_output(&retry_output);
|
||||
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
@@ -2163,7 +2146,14 @@ async fn handle_sandbox_error(
|
||||
}
|
||||
|
||||
/// Exec output is a pre-serialized JSON payload
|
||||
fn format_exec_output(output: &str, exit_code: i32, duration: Duration) -> String {
|
||||
fn format_exec_output(exec_output: &ExecToolCallOutput) -> String {
|
||||
let ExecToolCallOutput {
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
duration,
|
||||
} = exec_output;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExecMetadata {
|
||||
exit_code: i32,
|
||||
@@ -2179,10 +2169,20 @@ fn format_exec_output(output: &str, exit_code: i32, duration: Duration) -> Strin
|
||||
// round to 1 decimal place
|
||||
let duration_seconds = ((duration.as_secs_f32()) * 10.0).round() / 10.0;
|
||||
|
||||
let is_success = *exit_code == 0;
|
||||
let output = if is_success { stdout } else { stderr };
|
||||
|
||||
let mut formatted_output = output.text.clone();
|
||||
if let Some(truncated_after_lines) = output.truncated_after_lines {
|
||||
formatted_output.push_str(&format!(
|
||||
"\n\n[Output truncated after {truncated_after_lines} lines: too many lines or bytes.]",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = ExecOutput {
|
||||
output,
|
||||
output: &formatted_output,
|
||||
metadata: ExecMetadata {
|
||||
exit_code,
|
||||
exit_code: *exit_code,
|
||||
duration_seconds,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -130,8 +130,8 @@ pub async fn process_exec_tool_call(
|
||||
let duration = start.elapsed();
|
||||
match raw_output_result {
|
||||
Ok(raw_output) => {
|
||||
let stdout = String::from_utf8_lossy(&raw_output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&raw_output.stderr).to_string();
|
||||
let stdout = raw_output.stdout.from_utf8_lossy();
|
||||
let stderr = raw_output.stderr.from_utf8_lossy();
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
match raw_output.exit_status.signal() {
|
||||
@@ -146,7 +146,9 @@ pub async fn process_exec_tool_call(
|
||||
|
||||
if exit_code != 0 && is_likely_sandbox_denied(sandbox_type, exit_code) {
|
||||
return Err(CodexErr::Sandbox(SandboxErr::Denied(
|
||||
exit_code, stdout, stderr,
|
||||
exit_code,
|
||||
stdout.text,
|
||||
stderr.text,
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -243,18 +245,41 @@ fn is_likely_sandbox_denied(sandbox_type: SandboxType, exit_code: i32) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StreamOutput<T> {
|
||||
pub text: T,
|
||||
pub truncated_after_lines: Option<u32>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct RawExecToolCallOutput {
|
||||
pub exit_status: ExitStatus,
|
||||
pub stdout: Vec<u8>,
|
||||
pub stderr: Vec<u8>,
|
||||
pub stdout: StreamOutput<Vec<u8>>,
|
||||
pub stderr: StreamOutput<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl StreamOutput<String> {
|
||||
pub fn new(text: String) -> Self {
|
||||
Self {
|
||||
text,
|
||||
truncated_after_lines: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamOutput<Vec<u8>> {
|
||||
pub fn from_utf8_lossy(&self) -> StreamOutput<String> {
|
||||
StreamOutput {
|
||||
text: String::from_utf8_lossy(&self.text).to_string(),
|
||||
truncated_after_lines: self.truncated_after_lines,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecToolCallOutput {
|
||||
pub exit_code: i32,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub stdout: StreamOutput<String>,
|
||||
pub stderr: StreamOutput<String>,
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
@@ -363,7 +388,7 @@ async fn read_capped<R: AsyncRead + Unpin + Send + 'static>(
|
||||
max_lines: usize,
|
||||
stream: Option<StdoutStream>,
|
||||
is_stderr: bool,
|
||||
) -> io::Result<Vec<u8>> {
|
||||
) -> io::Result<StreamOutput<Vec<u8>>> {
|
||||
let mut buf = Vec::with_capacity(max_output.min(8 * 1024));
|
||||
let mut tmp = [0u8; 8192];
|
||||
|
||||
@@ -413,7 +438,16 @@ async fn read_capped<R: AsyncRead + Unpin + Send + 'static>(
|
||||
// Continue reading to EOF to avoid back-pressure, but discard once caps are hit.
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
let truncated = remaining_lines == 0 || remaining_bytes == 0;
|
||||
|
||||
Ok(StreamOutput {
|
||||
text: buf,
|
||||
truncated_after_lines: if truncated {
|
||||
Some((max_lines - remaining_lines) as u32)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -230,7 +230,7 @@ mod tests {
|
||||
assert_eq!(output.exit_code, 0, "input: {input:?} output: {output:?}");
|
||||
if let Some(expected) = expected_output {
|
||||
assert_eq!(
|
||||
output.stdout, expected,
|
||||
output.stdout.text, expected,
|
||||
"input: {input:?} output: {output:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user