use serde_json::Value;
use crate::utils::truncate_str;
const COMMAND_TRUNCATE_CHARS: usize = 80;
pub(super) fn summarize_tool_result(tool_name: &str, args_json: &str, result: &str) -> String {
let args_summary = extract_args_summary(tool_name, args_json);
let outcome = extract_outcome(tool_name, result);
if args_summary.is_empty() {
format!("{}: -> {}", tool_name, outcome)
} else {
format!("{}: {} -> {}", tool_name, args_summary, outcome)
}
}
fn extract_args_summary(tool_name: &str, args_json: &str) -> String {
let parsed: Value = match serde_json::from_str(args_json) {
Ok(v) => v,
Err(_) => return String::new(),
};
match tool_name {
"terminal" | "run_command" => parsed
.get("command")
.and_then(|v| v.as_str())
.map(|cmd| truncate_str(cmd, COMMAND_TRUNCATE_CHARS))
.unwrap_or_default(),
"http_request" => {
let method = parsed
.get("method")
.and_then(|v| v.as_str())
.unwrap_or("GET");
let url = parsed.get("url").and_then(|v| v.as_str()).unwrap_or("");
if url.is_empty() {
method.to_string()
} else {
format!("{} {}", method, url)
}
}
"read_file" | "write_file" | "edit_file" => extract_path_field(&parsed),
"web_search" => parsed
.get("query")
.and_then(|v| v.as_str())
.map(|q| format!("'{}'", q))
.unwrap_or_default(),
"remember_fact" | "manage_memories" => parsed
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
_ => first_string_value(&parsed),
}
}
fn extract_path_field(parsed: &Value) -> String {
for key in ["path", "file_path", "file", "filename"] {
if let Some(s) = parsed.get(key).and_then(|v| v.as_str()) {
return s.to_string();
}
}
String::new()
}
fn first_string_value(parsed: &Value) -> String {
if let Some(obj) = parsed.as_object() {
for (_key, val) in obj {
if let Some(s) = val.as_str() {
return truncate_str(s, COMMAND_TRUNCATE_CHARS);
}
}
}
String::new()
}
fn extract_outcome(tool_name: &str, result: &str) -> String {
match tool_name {
"terminal" | "run_command" => extract_terminal_outcome(result),
"http_request" => extract_http_outcome(result),
"read_file" => extract_read_file_outcome(result),
"write_file" | "edit_file" => extract_write_outcome(result),
_ => "completed".to_string(),
}
}
fn extract_terminal_outcome(result: &str) -> String {
for line in result.lines().rev() {
let lower = line.to_lowercase();
if let Some(pos) = lower
.find("exit_code:")
.or_else(|| lower.find("exit code:"))
{
let after_match = &lower[pos..];
if let Some(code) = after_match
.split(':')
.nth(1)
.and_then(|s| s.split_whitespace().next())
.and_then(|s| s.parse::<i64>().ok())
{
return format!("exit {}", code);
}
}
}
"completed".to_string()
}
fn extract_http_outcome(result: &str) -> String {
let first_line = result.lines().next().unwrap_or("");
if first_line.contains("HTTP") || looks_like_http_status(first_line) {
let trimmed = first_line.trim();
return truncate_str(trimmed, 40);
}
"completed".to_string()
}
fn looks_like_http_status(line: &str) -> bool {
line.split_whitespace().any(|word| {
word.len() == 3
&& word
.chars()
.next()
.is_some_and(|c| ('1'..='5').contains(&c))
&& word.chars().skip(1).all(|c| c.is_ascii_digit())
})
}
fn extract_read_file_outcome(result: &str) -> String {
if result.is_empty() {
return "empty".to_string();
}
let count = result.lines().count();
if count == 1 {
"1 line".to_string()
} else {
format!("{} lines", count)
}
}
fn extract_write_outcome(result: &str) -> String {
let lower = result.to_lowercase();
if lower.contains("successfully") || lower.contains("success") || lower.contains("written") {
"ok".to_string()
} else if lower.contains("error") || lower.contains("failed") || lower.contains("denied") {
"error".to_string()
} else {
"completed".to_string()
}
}
#[allow(dead_code)] pub(super) fn extract_skeleton(messages: &[Value]) -> Vec<Value> {
let mut result = Vec::new();
for msg in messages {
let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
match role {
"user" => {
result.push(msg.clone());
}
"assistant" => {
let has_text = msg
.get("content")
.and_then(|c| c.as_str())
.is_some_and(|s| !s.trim().is_empty());
if has_text {
let mut skeleton = serde_json::json!({
"role": "assistant",
"content": msg.get("content").unwrap(),
});
if let Some(obj) = msg.as_object() {
for (key, val) in obj {
if key != "role" && key != "content" && key != "tool_calls" {
skeleton[key] = val.clone();
}
}
}
result.push(skeleton);
} else {
let prev_is_placeholder = result.last().is_some_and(|m| {
m.get("role").and_then(|r| r.as_str()) == Some("assistant")
&& m.get("content").and_then(|c| c.as_str())
== Some("[Action completed]")
});
if !prev_is_placeholder {
result.push(serde_json::json!({
"role": "assistant",
"content": "[Action completed]",
}));
}
}
}
_ => {}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_terminal_with_exit_code() {
let result = summarize_tool_result(
"terminal",
r#"{"command": "cargo test"}"#,
"running 5 tests...\ntest result: ok\nexit_code: 0",
);
assert_eq!(result, "terminal: cargo test -> exit 0");
}
#[test]
fn test_terminal_nonzero_exit() {
let result = summarize_tool_result(
"terminal",
r#"{"command": "cargo build"}"#,
"error[E0308]: mismatched types\nExit code: 1",
);
assert_eq!(result, "terminal: cargo build -> exit 1");
}
#[test]
fn test_terminal_no_exit_code() {
let result = summarize_tool_result("terminal", r#"{"command": "echo hello"}"#, "hello");
assert_eq!(result, "terminal: echo hello -> completed");
}
#[test]
fn test_terminal_long_command_truncated() {
let long_cmd = "a".repeat(200);
let args = format!(r#"{{"command": "{}"}}"#, long_cmd);
let result = summarize_tool_result("terminal", &args, "exit_code: 0");
assert!(result.len() < 200);
assert!(result.contains("..."));
assert!(result.ends_with("-> exit 0"));
}
#[test]
fn test_run_command_alias() {
let result = summarize_tool_result(
"run_command",
r#"{"command": "ls -la"}"#,
"total 42\nexit_code: 0",
);
assert_eq!(result, "run_command: ls -la -> exit 0");
}
#[test]
fn test_http_request_with_status() {
let result = summarize_tool_result(
"http_request",
r#"{"method": "GET", "url": "api.example.com/data"}"#,
"HTTP/1.1 200 OK\nContent-Type: application/json",
);
assert_eq!(
result,
"http_request: GET api.example.com/data -> HTTP/1.1 200 OK"
);
}
#[test]
fn test_http_request_status_code_only() {
let result = summarize_tool_result(
"http_request",
r#"{"method": "POST", "url": "api.example.com/submit"}"#,
"200 OK",
);
assert_eq!(
result,
"http_request: POST api.example.com/submit -> 200 OK"
);
}
#[test]
fn test_http_request_no_status() {
let result = summarize_tool_result(
"http_request",
r#"{"method": "GET", "url": "example.com"}"#,
"{\"data\": [1, 2, 3]}",
);
assert_eq!(result, "http_request: GET example.com -> completed");
}
#[test]
fn test_http_request_default_method() {
let result = summarize_tool_result("http_request", r#"{"url": "example.com"}"#, "200 OK");
assert_eq!(result, "http_request: GET example.com -> 200 OK");
}
#[test]
fn test_read_file_line_count() {
let content = "line 1\nline 2\nline 3\nline 4\nline 5";
let result = summarize_tool_result("read_file", r#"{"path": "src/main.rs"}"#, content);
assert_eq!(result, "read_file: src/main.rs -> 5 lines");
}
#[test]
fn test_read_file_single_line() {
let result = summarize_tool_result("read_file", r#"{"path": "VERSION"}"#, "1.0.0");
assert_eq!(result, "read_file: VERSION -> 1 line");
}
#[test]
fn test_read_file_empty() {
let result = summarize_tool_result("read_file", r#"{"path": "empty.txt"}"#, "");
assert_eq!(result, "read_file: empty.txt -> empty");
}
#[test]
fn test_read_file_many_lines() {
let content = (1..=245)
.map(|i| format!("line {}", i))
.collect::<Vec<_>>()
.join("\n");
let result = summarize_tool_result("read_file", r#"{"path": "src/main.rs"}"#, &content);
assert_eq!(result, "read_file: src/main.rs -> 245 lines");
}
#[test]
fn test_write_file_success() {
let result = summarize_tool_result(
"write_file",
r#"{"path": "src/utils.rs"}"#,
"File written successfully",
);
assert_eq!(result, "write_file: src/utils.rs -> ok");
}
#[test]
fn test_write_file_error() {
let result = summarize_tool_result(
"write_file",
r#"{"path": "/etc/hosts"}"#,
"Error: permission denied",
);
assert_eq!(result, "write_file: /etc/hosts -> error");
}
#[test]
fn test_edit_file_success() {
let result = summarize_tool_result(
"edit_file",
r#"{"path": "src/config.rs"}"#,
"Edit applied successfully",
);
assert_eq!(result, "edit_file: src/config.rs -> ok");
}
#[test]
fn test_edit_file_completed() {
let result = summarize_tool_result(
"edit_file",
r#"{"path": "src/config.rs"}"#,
"Changes applied.",
);
assert_eq!(result, "edit_file: src/config.rs -> completed");
}
#[test]
fn test_web_search() {
let result = summarize_tool_result(
"web_search",
r#"{"query": "skin cancer trials DC"}"#,
"Found 10 results...",
);
assert_eq!(result, "web_search: 'skin cancer trials DC' -> completed");
}
#[test]
fn test_remember_fact() {
let result = summarize_tool_result(
"remember_fact",
r#"{"action": "store", "fact": "User likes coffee"}"#,
"Fact stored successfully",
);
assert_eq!(result, "remember_fact: store -> completed");
}
#[test]
fn test_manage_memories() {
let result = summarize_tool_result(
"manage_memories",
r#"{"action": "search", "query": "coffee"}"#,
"Found 3 matching facts",
);
assert_eq!(result, "manage_memories: search -> completed");
}
#[test]
fn test_unknown_tool_with_string_arg() {
let result =
summarize_tool_result("custom_tool", r#"{"task": "do_thing", "count": 5}"#, "done");
assert_eq!(result, "custom_tool: do_thing -> completed");
}
#[test]
fn test_unknown_tool_no_string_args() {
let result = summarize_tool_result("custom_tool", r#"{"count": 5, "flag": true}"#, "done");
assert_eq!(result, "custom_tool: -> completed");
}
#[test]
fn test_empty_args() {
let result = summarize_tool_result("custom_tool", "", "done");
assert_eq!(result, "custom_tool: -> completed");
}
#[test]
fn test_invalid_json_args() {
let result = summarize_tool_result("terminal", "not json", "exit_code: 0");
assert_eq!(result, "terminal: -> exit 0");
}
#[test]
fn test_file_path_alias() {
let result = summarize_tool_result(
"write_file",
r#"{"file_path": "output.txt"}"#,
"File written successfully",
);
assert_eq!(result, "write_file: output.txt -> ok");
}
#[test]
fn test_utf8_safety() {
let result = summarize_tool_result(
"terminal",
r#"{"command": "echo '🦀🦀🦀'"}"#,
"🦀🦀🦀\nexit_code: 0",
);
assert!(result.contains("echo '🦀🦀🦀'"));
assert!(result.ends_with("-> exit 0"));
}
#[test]
fn test_exit_code_case_insensitive() {
let result = summarize_tool_result(
"terminal",
r#"{"command": "make"}"#,
"Build complete\nEXIT_CODE: 0",
);
assert_eq!(result, "terminal: make -> exit 0");
}
#[test]
fn test_http_404_status() {
let result = summarize_tool_result(
"http_request",
r#"{"method": "GET", "url": "example.com/missing"}"#,
"404 Not Found",
);
assert_eq!(
result,
"http_request: GET example.com/missing -> 404 Not Found"
);
}
#[test]
fn test_looks_like_http_status_true() {
assert!(looks_like_http_status("200 OK"));
assert!(looks_like_http_status("404 Not Found"));
assert!(looks_like_http_status("500 Internal Server Error"));
}
#[test]
fn test_looks_like_http_status_false() {
assert!(!looks_like_http_status("{\"data\": 123}"));
assert!(!looks_like_http_status("no status here"));
assert!(!looks_like_http_status("600 is not valid"));
assert!(!looks_like_http_status("99 too short"));
}
#[test]
fn test_skeleton_strips_tool_calls_from_assistant() {
let messages = vec![
serde_json::json!({"role": "user", "content": "Hello"}),
serde_json::json!({
"role": "assistant",
"content": "Let me check that.",
"tool_calls": [{"id": "tc1", "function": {"name": "read_file", "arguments": "{}"}}]
}),
];
let skeleton = extract_skeleton(&messages);
assert_eq!(skeleton.len(), 2);
assert_eq!(skeleton[0]["role"], "user");
assert_eq!(skeleton[0]["content"], "Hello");
assert_eq!(skeleton[1]["role"], "assistant");
assert_eq!(skeleton[1]["content"], "Let me check that.");
assert!(skeleton[1].get("tool_calls").is_none());
}
#[test]
fn test_skeleton_drops_tool_results() {
let messages = vec![
serde_json::json!({"role": "user", "content": "Read my file"}),
serde_json::json!({
"role": "assistant",
"content": null,
"tool_calls": [{"id": "tc1", "function": {"name": "read_file", "arguments": "{}"}}]
}),
serde_json::json!({"role": "tool", "content": "file contents here...", "tool_call_id": "tc1", "name": "read_file"}),
serde_json::json!({"role": "assistant", "content": "Here are the contents of your file."}),
];
let skeleton = extract_skeleton(&messages);
assert_eq!(skeleton.len(), 3); assert!(skeleton.iter().all(|m| m["role"] != "tool"));
}
#[test]
fn test_skeleton_replaces_empty_assistant_with_action_completed() {
let messages = vec![
serde_json::json!({"role": "user", "content": "Write to file"}),
serde_json::json!({
"role": "assistant",
"content": null,
"tool_calls": [{"id": "tc1", "function": {"name": "write_file", "arguments": "{}"}}]
}),
];
let skeleton = extract_skeleton(&messages);
assert_eq!(skeleton.len(), 2);
assert_eq!(skeleton[1]["role"], "assistant");
assert_eq!(skeleton[1]["content"], "[Action completed]");
}
#[test]
fn test_skeleton_replaces_empty_string_assistant_with_action_completed() {
let messages = vec![
serde_json::json!({"role": "user", "content": "Do something"}),
serde_json::json!({
"role": "assistant",
"content": " ",
"tool_calls": [{"id": "tc1", "function": {"name": "terminal", "arguments": "{}"}}]
}),
];
let skeleton = extract_skeleton(&messages);
assert_eq!(skeleton.len(), 2);
assert_eq!(skeleton[1]["content"], "[Action completed]");
}
#[test]
fn test_skeleton_collapses_consecutive_placeholders() {
let messages = vec![
serde_json::json!({"role": "user", "content": "Find my files"}),
serde_json::json!({
"role": "assistant",
"content": null,
"tool_calls": [{"id": "tc1", "function": {"name": "glob", "arguments": "{}"}}]
}),
serde_json::json!({"role": "tool", "content": "a", "tool_call_id": "tc1", "name": "glob"}),
serde_json::json!({
"role": "assistant",
"content": null,
"tool_calls": [{"id": "tc2", "function": {"name": "glob", "arguments": "{}"}}]
}),
serde_json::json!({"role": "tool", "content": "b", "tool_call_id": "tc2", "name": "glob"}),
serde_json::json!({
"role": "assistant",
"content": "",
"tool_calls": [{"id": "tc3", "function": {"name": "glob", "arguments": "{}"}}]
}),
];
let skeleton = extract_skeleton(&messages);
assert_eq!(skeleton.len(), 2);
assert_eq!(skeleton[0]["content"], "Find my files");
assert_eq!(skeleton[1]["content"], "[Action completed]");
}
#[test]
fn test_skeleton_drops_system_messages() {
let messages = vec![
serde_json::json!({"role": "system", "content": "You are helpful."}),
serde_json::json!({"role": "user", "content": "Hello"}),
serde_json::json!({"role": "assistant", "content": "Hi!"}),
];
let skeleton = extract_skeleton(&messages);
assert_eq!(skeleton.len(), 2);
assert_eq!(skeleton[0]["role"], "user");
assert_eq!(skeleton[1]["role"], "assistant");
}
#[test]
fn test_skeleton_handles_multiple_interactions() {
let messages = vec![
serde_json::json!({"role": "user", "content": "What time is it?"}),
serde_json::json!({
"role": "assistant",
"content": null,
"tool_calls": [{"id": "tc1", "function": {"name": "terminal", "arguments": r#"{"command":"date"}"#}}]
}),
serde_json::json!({"role": "tool", "content": "Mon Mar 17 12:00:00 UTC 2025", "tool_call_id": "tc1", "name": "terminal"}),
serde_json::json!({"role": "assistant", "content": "It's noon on March 17th."}),
serde_json::json!({"role": "user", "content": "Write a greeting"}),
serde_json::json!({
"role": "assistant",
"content": "I'll write that for you.",
"tool_calls": [{"id": "tc2", "function": {"name": "write_file", "arguments": "{}"}}]
}),
serde_json::json!({"role": "tool", "content": "File written successfully", "tool_call_id": "tc2", "name": "write_file"}),
serde_json::json!({"role": "assistant", "content": "Done! I wrote the greeting file."}),
];
let skeleton = extract_skeleton(&messages);
assert_eq!(skeleton.len(), 6);
assert!(skeleton.iter().all(|m| m["role"] != "tool"));
assert_eq!(skeleton[0]["content"], "What time is it?");
assert_eq!(skeleton[1]["content"], "[Action completed]"); assert_eq!(skeleton[2]["content"], "It's noon on March 17th.");
assert_eq!(skeleton[3]["content"], "Write a greeting");
assert_eq!(skeleton[4]["content"], "I'll write that for you."); assert!(skeleton[4].get("tool_calls").is_none());
assert_eq!(skeleton[5]["content"], "Done! I wrote the greeting file.");
}
#[test]
fn test_skeleton_empty_input() {
let skeleton = extract_skeleton(&[]);
assert!(skeleton.is_empty());
}
}