a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
168 lines
6.0 KiB
Rust
168 lines
6.0 KiB
Rust
use crate::message::{ContentBlock, ToolCall};
|
|
use crate::tool::ToolOutput;
|
|
|
|
pub(super) const MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY: usize = 512 * 1024;
|
|
|
|
pub(super) fn cap_tool_output_for_history(tool_name: &str, mut output: ToolOutput) -> ToolOutput {
|
|
if output.output.chars().count() <= MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY {
|
|
return output;
|
|
}
|
|
|
|
let original_chars = output.output.chars().count();
|
|
let kept = crate::util::truncate_str(&output.output, MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY);
|
|
output.output = format!(
|
|
"{}\n\n[Tool output truncated by jcode: tool `{}` produced {} chars; kept first {} chars to protect the remote protocol, session history, and prompt cache. Redirect large logs to a file and read targeted sections.]",
|
|
kept, tool_name, original_chars, MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY,
|
|
);
|
|
output
|
|
}
|
|
|
|
pub(super) fn cap_sdk_tool_content_for_history(tool_name: &str, content: String) -> String {
|
|
if content.chars().count() <= MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY {
|
|
return content;
|
|
}
|
|
let original_chars = content.chars().count();
|
|
let kept = crate::util::truncate_str(&content, MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY);
|
|
format!(
|
|
"{}\n\n[Tool output truncated by jcode: tool `{}` produced {} chars; kept first {} chars to protect the remote protocol, session history, and prompt cache. Redirect large logs to a file and read targeted sections.]",
|
|
kept, tool_name, original_chars, MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY,
|
|
)
|
|
}
|
|
|
|
/// Build rendered side-pane images from a tool output's attached images.
|
|
///
|
|
/// This mirrors how `render_messages_and_images` derives images from persisted
|
|
/// session history (source = ToolResult), so live-streamed images match what a
|
|
/// later History reload would produce. `tool_name` and `tool_input` provide the
|
|
/// label fallback (e.g. the `read` tool's `file_path`); `tool_call_id` anchors
|
|
/// the image to its tool message in the transcript.
|
|
pub(super) fn tool_output_side_pane_images(
|
|
tool_call_id: &str,
|
|
tool_name: &str,
|
|
tool_input: &serde_json::Value,
|
|
output: &ToolOutput,
|
|
) -> Vec<jcode_session_types::RenderedImage> {
|
|
if output.images.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let fallback_label = tool_input
|
|
.get("file_path")
|
|
.and_then(|value| value.as_str())
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(str::to_string);
|
|
output
|
|
.images
|
|
.iter()
|
|
.map(|img| jcode_session_types::RenderedImage {
|
|
media_type: img.media_type.clone(),
|
|
data: img.data.clone(),
|
|
label: img
|
|
.label
|
|
.as_ref()
|
|
.map(|label| label.trim().to_string())
|
|
.filter(|label| !label.is_empty())
|
|
.or_else(|| fallback_label.clone()),
|
|
source: jcode_session_types::RenderedImageSource::ToolResult {
|
|
tool_name: tool_name.to_string(),
|
|
},
|
|
anchor: Some(jcode_session_types::RenderedImageAnchor::ToolCall {
|
|
id: tool_call_id.to_string(),
|
|
}),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(super) fn tool_output_to_content_blocks(
|
|
tool_use_id: String,
|
|
output: ToolOutput,
|
|
) -> Vec<ContentBlock> {
|
|
let mut blocks = vec![ContentBlock::ToolResult {
|
|
tool_use_id,
|
|
content: output.output,
|
|
is_error: None,
|
|
}];
|
|
for img in output.images {
|
|
blocks.push(ContentBlock::Image {
|
|
media_type: img.media_type,
|
|
data: img.data,
|
|
});
|
|
if let Some(label) = img.label.filter(|label| !label.trim().is_empty()) {
|
|
blocks.push(ContentBlock::Text {
|
|
text: format!(
|
|
"[Attached image associated with the preceding tool result: {}]",
|
|
label
|
|
),
|
|
cache_control: None,
|
|
});
|
|
}
|
|
}
|
|
blocks
|
|
}
|
|
|
|
pub(super) fn print_tool_summary(tool: &ToolCall) {
|
|
match tool.name.as_str() {
|
|
"bash" => {
|
|
if let Some(cmd) = tool.input.get("command").and_then(|v| v.as_str()) {
|
|
let short = if cmd.len() > 60 {
|
|
format!("{}...", crate::util::truncate_str(cmd, 60))
|
|
} else {
|
|
cmd.to_string()
|
|
};
|
|
println!("$ {}", short);
|
|
}
|
|
}
|
|
"read" | "write" | "edit" => {
|
|
if let Some(path) = tool.input.get("file_path").and_then(|v| v.as_str()) {
|
|
println!("{}", path);
|
|
}
|
|
}
|
|
"glob" | "grep" => {
|
|
if let Some(pattern) = tool.input.get("pattern").and_then(|v| v.as_str()) {
|
|
println!("'{}'", pattern);
|
|
}
|
|
}
|
|
"ls" => {
|
|
let path = tool
|
|
.input
|
|
.get("path")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or(".");
|
|
println!("{}", path);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn cap_tool_output_leaves_small_output_unchanged() {
|
|
let output = ToolOutput::new("short output");
|
|
let capped = cap_tool_output_for_history("bash", output.clone());
|
|
assert_eq!(capped.output, output.output);
|
|
}
|
|
|
|
#[test]
|
|
fn cap_tool_output_adds_visible_truncation_notice() {
|
|
let output = ToolOutput::new("x".repeat(MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY + 10));
|
|
let capped = cap_tool_output_for_history("bash", output);
|
|
assert!(capped.output.len() < MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY + 1_000);
|
|
assert!(capped.output.contains("Tool output truncated by jcode"));
|
|
assert!(capped.output.contains("tool `bash` produced"));
|
|
assert!(capped.output.contains("Redirect large logs to a file"));
|
|
}
|
|
|
|
#[test]
|
|
fn cap_sdk_tool_content_adds_same_notice() {
|
|
let capped = cap_sdk_tool_content_for_history(
|
|
"custom",
|
|
"y".repeat(MAX_TOOL_OUTPUT_CHARS_FOR_HISTORY + 10),
|
|
);
|
|
assert!(capped.contains("Tool output truncated by jcode"));
|
|
assert!(capped.contains("tool `custom` produced"));
|
|
}
|
|
}
|