use crate::{
output::{ToolStatus, is_credential_like_key, redact_sensitive_text},
providers::ToolCall,
subagents::SubagentsOutput,
tools::{
ToolCapability, ToolResult,
contract::{
REDACTED_METADATA_KEYS, arg_key, display_label as label, metadata_key as meta,
summary_metadata_label as summary_label,
},
},
};
use serde_json::Value;
pub(crate) fn format_tool_block(call: &ToolCall, result: &ToolResult) -> String {
format_tool_block_with_options(call, result, false)
}
pub(crate) fn format_tool_activity_detail_block(call: &ToolCall, result: &ToolResult) -> String {
format_tool_block_with_options(call, result, true)
}
fn format_tool_block_with_options(
call: &ToolCall,
result: &ToolResult,
activity_detail: bool,
) -> String {
let tool_name = if result.tool_name.is_empty() {
call.name.as_str()
} else {
result.tool_name.as_str()
};
let use_subagents_safe_output = is_tool(tool_name, ToolCapability::Subagents);
if activity_detail {
return format_tool_activity_block(call, result, use_subagents_safe_output);
}
let mut lines = Vec::new();
lines.push(format!(
"STATUS: {}",
if result.success { "success" } else { "failure" }
));
append_metadata_lines(&mut lines, call, result);
if should_display_output(&call.name, result) {
lines.push("OUTPUT:".to_string());
}
let mut block = lines.join("\n");
block.push('\n');
if should_display_output(&call.name, result) {
let display_content = if use_subagents_safe_output {
subagents_safe_output(result)
} else {
redact_sensitive_text(&result.content)
};
block.push_str(&display_content);
if !display_content.ends_with('\n') {
block.push('\n');
}
}
block
}
fn format_tool_activity_block(
call: &ToolCall,
result: &ToolResult,
use_subagents_safe_output: bool,
) -> String {
let mut metadata = Vec::new();
append_metadata_lines(&mut metadata, call, result);
if result.success && is_tool(&call.name, ToolCapability::Write) {
append_write_activity_detail_lines(&mut metadata, &call.arguments);
}
let mut block = format!(
"Status: {}\n",
if result.success { "success" } else { "failure" }
);
if !metadata.is_empty() {
block.push_str("\nTool metadata\n");
for line in metadata {
block.push_str(" ");
block.push_str(&humanize_tool_metadata_line(&line));
block.push('\n');
}
}
if should_display_output(&call.name, result) {
block.push_str("\nOutput preview\n");
let display_content = if use_subagents_safe_output {
subagents_safe_output(result)
} else {
redact_sensitive_text(&result.content)
};
block.push_str(&display_content);
if !display_content.ends_with('\n') {
block.push('\n');
}
}
if result.success
&& matches!(
ToolCapability::from_dispatch_name(&call.name),
Some(ToolCapability::HashEdit)
)
&& let Some(diff) = result.display.edit_diff.as_deref()
{
block.push_str("\nDiff preview\n");
block.push_str(&redact_sensitive_text(diff));
if !block.ends_with('\n') {
block.push('\n');
}
}
block
}
fn humanize_tool_metadata_line(line: &str) -> String {
let Some((label, value)) = line.split_once(':') else {
return line.to_string();
};
format!("{}:{}", humanize_tool_label(label), value)
}
fn humanize_tool_label(label: &str) -> String {
label
.split('_')
.map(|part| {
let mut chars = part.chars();
let Some(first) = chars.next() else {
return String::new();
};
format!("{}{}", first.to_uppercase(), chars.as_str().to_lowercase())
})
.collect::<Vec<_>>()
.join(" ")
}
fn should_display_output(_call_name: &str, result: &ToolResult) -> bool {
!result.content.is_empty()
}
fn is_tool(name: &str, capability: ToolCapability) -> bool {
ToolCapability::from_dispatch_name(name) == Some(capability)
}
fn append_write_activity_detail_lines(lines: &mut Vec<String>, args: &Value) {
if let Some(path) = args.get(arg_key::PATH).and_then(Value::as_str) {
lines.push(format!("DESTINATION_FILE: {}", redact_sensitive_text(path)));
}
if let Some(content) = args.get(arg_key::CONTENT).and_then(Value::as_str) {
lines.push("WRITTEN_DATA:".to_string());
lines.push(redact_sensitive_text(content));
}
}
fn subagents_safe_output(result: &ToolResult) -> String {
if let Ok(output) = serde_json::from_str::<SubagentsOutput>(&result.content) {
let mut lines = vec![subagents_summary_line(
output.summary.total as u64,
output.summary.completed as u64,
output.summary.failed as u64,
output.summary.total_tokens,
)];
for task in output.results.iter().take(10) {
let status = match task.status {
crate::subagents::SubagentStatus::Completed => "completed",
crate::subagents::SubagentStatus::Failed => "failed",
};
let mut parts = vec![format!("{}={status}", task.id)];
if let Some(identity) = task.identity.as_deref().filter(|value| !value.is_empty()) {
parts.push(format!("identity={}", redact_sensitive_text(identity)));
}
if let Some(session_path) = task.session_path.as_ref() {
parts.push(format!(
"session_path={}",
redact_sensitive_text(&session_path.display().to_string())
));
}
if task.output_truncated {
parts.push("output_truncated=true".to_string());
}
if let Some(tokens) = task.total_tokens {
parts.push(format!("total_tokens={tokens}"));
}
lines.push(parts.join(" "));
}
return lines.join("\n");
}
let Some(summary) = result.metadata.get(meta::SUMMARY) else {
return "summary unavailable".to_string();
};
let total = summary
.get(meta::TOTAL)
.and_then(Value::as_u64)
.unwrap_or(0);
let completed = summary
.get(meta::COMPLETED)
.and_then(Value::as_u64)
.unwrap_or(0);
let failed = summary
.get(meta::FAILED)
.and_then(Value::as_u64)
.unwrap_or(0);
let total_tokens = summary.get(meta::TOTAL_TOKENS).and_then(Value::as_u64);
subagents_summary_line(total, completed, failed, total_tokens)
}
fn subagents_summary_line(
total: u64,
completed: u64,
failed: u64,
total_tokens: Option<u64>,
) -> String {
let mut parts = vec![
format!("total={total}"),
format!("completed={completed}"),
format!("failed={failed}"),
];
if let Some(total_tokens) = total_tokens {
parts.push(format!("total_tokens={total_tokens}"));
}
format!("summary: {}", parts.join(" "))
}
fn append_metadata_lines(lines: &mut Vec<String>, call: &ToolCall, result: &ToolResult) {
match ToolCapability::from_dispatch_name(&call.name) {
Some(ToolCapability::Read) => {
push_arg_string(lines, label::PATH, &call.arguments, arg_key::PATH);
push_arg_or_meta_string_array(
lines,
label::PATHS,
&call.arguments,
&result.metadata,
arg_key::PATHS,
);
push_arg_or_meta_usize(
lines,
label::OFFSET,
&call.arguments,
&result.metadata,
meta::OFFSET,
);
push_arg_or_meta_usize(
lines,
label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
}
Some(ToolCapability::ViewImage) => {
push_arg_string(lines, label::PATH, &call.arguments, arg_key::PATH);
push_meta_value(lines, label::PROVIDER, &result.metadata, meta::PROVIDER);
push_meta_value(lines, label::MODEL, &result.metadata, meta::MODEL);
push_meta_value(lines, label::MEDIA_TYPE, &result.metadata, meta::MEDIA_TYPE);
push_meta_value(
lines,
label::IMAGE_BYTES,
&result.metadata,
meta::IMAGE_BYTES,
);
push_meta_value(lines, label::TRUNCATED, &result.metadata, meta::TRUNCATED);
}
Some(ToolCapability::Bash) => {
for key in crate::tools::contract::BASH_COMMAND_ARG_KEYS {
push_arg_string(lines, label::COMMAND, &call.arguments, key);
}
push_arg_or_meta_u64(
lines,
label::TIMEOUT,
&call.arguments,
&result.metadata,
arg_key::TIMEOUT,
);
push_meta_value(lines, label::EXIT_CODE, &result.metadata, meta::EXIT_CODE);
push_meta_value(lines, label::TIMED_OUT, &result.metadata, meta::TIMED_OUT);
push_meta_value(
lines,
label::CLEANUP_WARNING,
&result.metadata,
meta::CLEANUP_WARNING,
);
}
Some(ToolCapability::HashEdit) => {
if let Some(input) = call.arguments.get(arg_key::INPUT).and_then(Value::as_str) {
lines.push(format!("{}: {}", label::CONTENT_BYTES, input.len()));
} else {
push_meta_value(lines, label::CONTENT_BYTES, &result.metadata, "input_bytes");
}
}
Some(ToolCapability::Write) => {
push_arg_string(lines, label::PATH, &call.arguments, arg_key::PATH);
if let Some(content) = call.arguments.get(arg_key::CONTENT).and_then(Value::as_str) {
lines.push(format!("{}: {}", label::CONTENT_BYTES, content.len()));
} else {
push_meta_value(lines, label::CONTENT_BYTES, &result.metadata, meta::BYTES);
}
}
Some(ToolCapability::Grep) => {
push_arg_string(lines, label::PATTERN, &call.arguments, arg_key::PATTERN);
push_arg_string(lines, label::PATH, &call.arguments, arg_key::PATH);
push_arg_or_meta_usize(
lines,
label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
push_arg_or_meta_usize(
lines,
label::CONTEXT,
&call.arguments,
&result.metadata,
arg_key::CONTEXT,
);
push_meta_value(lines, label::ENGINE, &result.metadata, meta::ENGINE);
push_meta_value(lines, label::EXIT_CODE, &result.metadata, meta::EXIT_CODE);
}
Some(ToolCapability::Diagnostics) => {
push_arg_or_meta_string(
lines,
label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_arg_or_meta_usize(
lines,
label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
push_meta_value(
lines,
label::DIAGNOSTICS,
&result.metadata,
meta::DIAGNOSTIC_COUNT,
);
push_meta_value(lines, label::STATUS, &result.metadata, meta::LSP_ENABLED);
}
Some(ToolCapability::References) => {
push_arg_or_meta_string(
lines,
label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_arg_or_meta_usize(
lines,
label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
push_meta_value(lines, label::REFERENCES, &result.metadata, meta::REFERENCES);
push_meta_value(lines, label::STATUS, &result.metadata, meta::LSP_ENABLED);
}
Some(ToolCapability::AstGrep) => {
push_arg_string(lines, label::PATTERN, &call.arguments, arg_key::PATTERN);
push_arg_string(lines, label::LANGUAGE, &call.arguments, arg_key::LANGUAGE);
push_arg_string(lines, label::PATH, &call.arguments, arg_key::PATH);
push_arg_or_meta_string(
lines,
label::REWRITE,
&call.arguments,
&result.metadata,
meta::REWRITE,
);
push_arg_or_meta_usize(
lines,
label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
push_meta_value(lines, label::EXIT_CODE, &result.metadata, meta::EXIT_CODE);
}
Some(ToolCapability::Find) => {
push_arg_string(lines, label::QUERY, &call.arguments, arg_key::QUERY);
push_arg_string(lines, label::PATH, &call.arguments, arg_key::PATH);
push_arg_or_meta_usize(
lines,
label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
push_arg_or_meta_usize(
lines,
label::OFFSET,
&call.arguments,
&result.metadata,
meta::OFFSET,
);
push_arg_or_meta_string(
lines,
label::KIND,
&call.arguments,
&result.metadata,
meta::KIND,
);
push_meta_value(lines, label::ENGINE, &result.metadata, meta::ENGINE);
push_meta_value(
lines,
label::TOTAL_MATCHED,
&result.metadata,
meta::TOTAL_MATCHED,
);
push_meta_value(lines, label::TRUNCATED, &result.metadata, meta::TRUNCATED);
push_meta_value(
lines,
label::INDEX_READY,
&result.metadata,
meta::INDEX_READY,
);
}
Some(ToolCapability::ListFiles) => {
push_arg_string(lines, label::PATH, &call.arguments, arg_key::PATH);
push_meta_value(lines, label::FILES, &result.metadata, meta::FILES);
push_meta_value(
lines,
label::DIRECTORIES,
&result.metadata,
meta::DIRECTORIES,
);
push_meta_value(lines, label::TRUNCATED, &result.metadata, meta::TRUNCATED);
}
Some(ToolCapability::RepoMap) => {
push_arg_or_meta_string(
lines,
label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_arg_or_meta_string(
lines,
label::QUERY,
&call.arguments,
&result.metadata,
meta::QUERY,
);
push_meta_value(
lines,
label::FILES_SCANNED,
&result.metadata,
meta::FILES_SCANNED,
);
push_meta_value(
lines,
label::FILES_PARSED,
&result.metadata,
meta::FILES_PARSED,
);
push_meta_value(lines, label::SYMBOLS, &result.metadata, meta::SYMBOLS);
push_meta_value(lines, label::REFERENCES, &result.metadata, meta::REFERENCES);
push_meta_value(lines, label::LANGUAGE, &result.metadata, meta::LANGUAGES);
push_meta_value(lines, label::TRUNCATED, &result.metadata, meta::TRUNCATED);
}
Some(ToolCapability::Subagents) => {
if let Some(tasks) = call.arguments.get(arg_key::TASKS).and_then(Value::as_array) {
lines.push(format!("{}: {}", label::TASK_COUNT, tasks.len()));
let ids = (1..=tasks.len())
.map(|index| format!("g{index}"))
.collect::<Vec<_>>()
.join(", ");
if !ids.is_empty() {
lines.push(format!("{}: {ids}", label::TASK_IDS));
}
if let Some(identities) = subagent_identity_list(tasks) {
lines.push(format!("{}: {identities}", label::IDENTITIES));
}
}
push_arg_or_meta_usize(
lines,
label::CONCURRENCY,
&call.arguments,
&result.metadata,
meta::CONCURRENCY,
);
if let Some(summary) = result.metadata.get(meta::SUMMARY) {
push_meta_value(lines, label::TOTAL_TOKENS, summary, meta::TOTAL_TOKENS);
}
}
Some(ToolCapability::WebSearch) => {
push_arg_or_meta_string_array(
lines,
label::QUERY,
&call.arguments,
&result.metadata,
arg_key::QUERIES,
);
push_meta_value(
lines,
label::QUERY_COUNT,
&result.metadata,
meta::QUERY_COUNT,
);
push_meta_value(lines, label::RESULTS, &result.metadata, meta::TOTAL_RESULTS);
push_meta_value(
lines,
label::INCLUDE_CONTENT,
&result.metadata,
meta::INCLUDE_CONTENT,
);
push_meta_value(lines, label::TRUNCATED, &result.metadata, meta::TRUNCATED);
}
Some(ToolCapability::CodeSearch) => {
push_arg_string(lines, label::QUERY, &call.arguments, arg_key::QUERY);
push_meta_value(lines, label::RESULTS, &result.metadata, meta::RESULTS);
push_meta_value(lines, label::TRUNCATED, &result.metadata, meta::TRUNCATED);
}
Some(ToolCapability::Browser) => {
push_arg_string(lines, label::COMMAND, &call.arguments, arg_key::COMMAND);
push_arg_or_meta_string(
lines,
label::MODE,
&call.arguments,
&result.metadata,
meta::MODE,
);
push_meta_value(lines, label::ACTION, &result.metadata, meta::ACTION);
push_meta_value(
lines,
label::CONNECT_URL,
&result.metadata,
meta::CONNECT_URL,
);
push_meta_value(
lines,
label::IGNORE_HTTPS_ERRORS,
&result.metadata,
meta::IGNORE_HTTPS_ERRORS,
);
push_meta_value(lines, label::EXIT_CODE, &result.metadata, meta::EXIT_CODE);
push_meta_value(lines, label::TIMED_OUT, &result.metadata, meta::TIMED_OUT);
push_meta_value(lines, label::TRUNCATED, &result.metadata, meta::TRUNCATED);
push_meta_value(
lines,
label::SCREENSHOT_PATH,
&result.metadata,
meta::SCREENSHOT_PATH,
);
push_meta_value(
lines,
label::CLEANUP_WARNING,
&result.metadata,
meta::CLEANUP_WARNING,
);
}
None => append_safe_scalar_arguments(lines, &call.arguments),
}
}
fn subagent_identity_list(tasks: &[Value]) -> Option<String> {
let identities = tasks
.iter()
.enumerate()
.filter_map(|(index, task)| {
task.get(arg_key::IDENTITY)
.and_then(Value::as_str)
.map(|identity| format!("g{}={}", index + 1, redact_sensitive_text(identity)))
})
.collect::<Vec<_>>();
(!identities.is_empty()).then(|| identities.join(", "))
}
fn push_arg_string(lines: &mut Vec<String>, label: &str, args: &Value, key: &str) {
if let Some(value) = args.get(key).and_then(Value::as_str) {
lines.push(format!("{label}: {}", redact_sensitive_text(value)));
}
}
fn push_arg_or_meta_string_array(
lines: &mut Vec<String>,
label: &str,
args: &Value,
meta: &Value,
key: &str,
) {
let values = args
.get(key)
.or_else(|| meta.get(key))
.and_then(Value::as_array)
.map(|values| {
values
.iter()
.filter_map(Value::as_str)
.map(redact_sensitive_text)
.collect::<Vec<_>>()
})
.unwrap_or_default();
if !values.is_empty() {
lines.push(format!("{label}: {}", values.join(", ")));
}
}
fn push_arg_or_meta_usize(
lines: &mut Vec<String>,
label: &str,
args: &Value,
meta: &Value,
key: &str,
) {
if let Some(value) = args.get(key).and_then(Value::as_u64) {
lines.push(format!("{label}: {value}"));
} else {
push_meta_value(lines, label, meta, key);
}
}
fn push_arg_or_meta_u64(
lines: &mut Vec<String>,
label: &str,
args: &Value,
meta: &Value,
key: &str,
) {
push_arg_or_meta_usize(lines, label, args, meta, key);
}
fn push_arg_or_meta_string(
lines: &mut Vec<String>,
label: &str,
args: &Value,
meta: &Value,
key: &str,
) {
if let Some(value) = args.get(key).and_then(Value::as_str) {
lines.push(format!("{label}: {}", redact_sensitive_text(value)));
} else {
push_meta_value(lines, label, meta, key);
}
}
fn push_meta_value(lines: &mut Vec<String>, label: &str, meta: &Value, key: &str) {
let Some(value) = meta.get(key) else {
return;
};
if value.is_null() {
return;
}
match value {
Value::String(text) => lines.push(format!("{label}: {}", redact_sensitive_text(text))),
Value::Bool(value) => lines.push(format!("{label}: {value}")),
Value::Number(value) => lines.push(format!("{label}: {value}")),
_ => {}
}
}
fn append_safe_scalar_arguments(lines: &mut Vec<String>, args: &Value) {
let Some(object) = args.as_object() else {
return;
};
for (key, value) in object {
if is_redacted_metadata_key(key) {
continue;
}
match value {
Value::String(text) => lines.push(format!(
"{}: {}",
key.to_uppercase(),
redact_sensitive_text(text)
)),
Value::Bool(value) => lines.push(format!("{}: {value}", key.to_uppercase())),
Value::Number(value) => lines.push(format!("{}: {value}", key.to_uppercase())),
_ => {}
}
}
}
fn is_redacted_metadata_key(key: &str) -> bool {
let normalized = key
.chars()
.filter(|character| {
character.is_ascii_alphanumeric() || *character == '_' || *character == '-'
})
.flat_map(char::to_lowercase)
.collect::<String>();
matches!(
normalized.as_str(),
key if REDACTED_METADATA_KEYS.contains(&key)
) || is_credential_like_key(&normalized)
}
pub(crate) fn tool_display_label(call: &ToolCall) -> String {
redact_sensitive_text(&tool_label(call))
}
fn tool_label(call: &ToolCall) -> String {
match ToolCapability::from_dispatch_name(&call.name) {
Some(ToolCapability::Bash) => string_arg(&call.arguments, arg_key::COMMAND)
.or_else(|| string_arg(&call.arguments, arg_key::CMD))
.map(|command| format!("{} {command}", call.name))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::Browser) => string_arg(&call.arguments, arg_key::COMMAND)
.map(|command| format!("browser {command}"))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::HashEdit | ToolCapability::Write) => {
string_arg(&call.arguments, arg_key::PATH)
.map(|path| format!("{} {path}", call.name))
.unwrap_or_else(|| call.name.clone())
}
Some(ToolCapability::Read | ToolCapability::ViewImage) => {
string_arg(&call.arguments, arg_key::PATH)
.map(|path| format!("{} {path}", call.name))
.or_else(|| {
call.arguments
.get(arg_key::PATHS)
.and_then(Value::as_array)
.map(|paths| format!("{} {}", call.name, file_count_label(paths.len())))
})
.unwrap_or_else(|| call.name.clone())
}
Some(ToolCapability::Grep | ToolCapability::AstGrep) => {
string_arg(&call.arguments, arg_key::PATTERN)
.map(|pattern| format!("{} {pattern}", call.name))
.unwrap_or_else(|| call.name.clone())
}
Some(ToolCapability::Find) => string_arg(&call.arguments, arg_key::QUERY)
.map(|query| format!("{} {query}", call.name))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::RepoMap) => {
let path =
string_arg(&call.arguments, arg_key::PATH).unwrap_or_else(|| ".".to_string());
let mut label = format!("repo_map {path}");
if let Some(query) = string_arg(&call.arguments, arg_key::QUERY) {
label.push_str(" query=");
label.push_str(&query);
}
label
}
Some(ToolCapability::ListFiles) => string_arg(&call.arguments, arg_key::PATH)
.map(|path| format!("{} {path}", call.name))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::Diagnostics | ToolCapability::References) => {
string_arg(&call.arguments, arg_key::PATH)
.map(|path| format!("{} {path}", call.name))
.unwrap_or_else(|| call.name.clone())
}
Some(ToolCapability::Subagents) => call
.arguments
.get(arg_key::TASKS)
.and_then(Value::as_array)
.map(|tasks| format!("subagents · {}", task_count_label(tasks.len())))
.unwrap_or_else(|| call.name.clone()),
Some(ToolCapability::WebSearch) => {
let queries = call
.arguments
.get(arg_key::QUERIES)
.and_then(Value::as_array);
let label = match queries {
Some(queries) if queries.len() == 1 => queries[0]
.as_str()
.map(redact_sensitive_text)
.map(|query| format!("{} {query}", call.name)),
Some(queries) => Some(format!("{} {} queries", call.name, queries.len())),
None => None,
};
label.unwrap_or_else(|| call.name.clone())
}
Some(ToolCapability::CodeSearch) => string_arg(&call.arguments, arg_key::QUERY)
.map(|query| format!("{} {query}", call.name))
.unwrap_or_else(|| call.name.clone()),
None => call.name.clone(),
}
}
pub(crate) fn should_show_pending_tool(call: &ToolCall) -> bool {
matches!(
ToolCapability::from_dispatch_name(&call.name),
Some(
ToolCapability::Write
| ToolCapability::HashEdit
| ToolCapability::ViewImage
| ToolCapability::Find
| ToolCapability::RepoMap
| ToolCapability::Diagnostics
| ToolCapability::References
| ToolCapability::WebSearch
| ToolCapability::CodeSearch
| ToolCapability::Browser,
)
)
}
pub(crate) fn pending_tool_status(call: &ToolCall) -> ToolStatus {
if matches!(
ToolCapability::from_dispatch_name(&call.name),
Some(ToolCapability::Write)
) {
ToolStatus::Writing
} else {
ToolStatus::Running
}
}
pub(crate) fn pending_tool_summary_metadata(call: &ToolCall) -> Vec<(String, String)> {
let mut metadata = Vec::new();
match ToolCapability::from_dispatch_name(&call.name) {
Some(ToolCapability::Write) => {
if let Some(content) = call.arguments.get(arg_key::CONTENT).and_then(Value::as_str) {
metadata.push((summary_label::BYTES.to_string(), content.len().to_string()));
}
}
Some(ToolCapability::HashEdit) => {
if let Some(input) = call.arguments.get(arg_key::INPUT).and_then(Value::as_str) {
metadata.push((summary_label::BYTES.to_string(), input.len().to_string()));
}
}
Some(ToolCapability::WebSearch) => {
if let Some(query_count) = call
.arguments
.get(arg_key::QUERIES)
.and_then(Value::as_array)
.map(|queries| queries.len())
{
metadata.push((meta::QUERY_COUNT.to_string(), query_count.to_string()));
}
}
Some(ToolCapability::CodeSearch) | Some(ToolCapability::ViewImage) => {}
Some(ToolCapability::Browser) => {
if let Some(mode) = string_arg(&call.arguments, arg_key::MODE) {
metadata.push((meta::MODE.to_string(), mode));
}
}
Some(ToolCapability::Find) => {
if let Some(path) = string_arg(&call.arguments, arg_key::PATH) {
metadata.push((summary_label::PATH.to_string(), path));
}
if let Some(limit) = scalar_value(call.arguments.get(arg_key::LIMIT)) {
metadata.push((summary_label::LIMIT.to_string(), limit));
}
}
Some(ToolCapability::RepoMap) => {
if let Some(path) = string_arg(&call.arguments, arg_key::PATH) {
metadata.push((summary_label::PATH.to_string(), path));
}
if let Some(query) = string_arg(&call.arguments, arg_key::QUERY) {
metadata.push((summary_label::QUERY.to_string(), query));
}
}
Some(ToolCapability::Diagnostics | ToolCapability::References) => {
if let Some(path) = string_arg(&call.arguments, arg_key::PATH) {
metadata.push((summary_label::PATH.to_string(), path));
}
if let Some(limit) = scalar_value(call.arguments.get(arg_key::LIMIT)) {
metadata.push((summary_label::LIMIT.to_string(), limit));
}
}
_ => {}
}
metadata
}
pub(crate) fn tool_summary_metadata(call: &ToolCall, result: &ToolResult) -> Vec<(String, String)> {
let mut metadata = Vec::new();
match ToolCapability::from_dispatch_name(&call.name) {
Some(ToolCapability::Bash) => {
push_summary_arg_or_meta(
&mut metadata,
summary_label::TIMEOUT,
&call.arguments,
&result.metadata,
arg_key::TIMEOUT,
);
push_summary_meta(
&mut metadata,
summary_label::EXIT_CODE,
&result.metadata,
meta::EXIT_CODE,
);
push_summary_meta(
&mut metadata,
summary_label::TIMED_OUT,
&result.metadata,
meta::TIMED_OUT,
);
metadata.push((
summary_label::OUTPUT_LINES.to_string(),
line_count(&result.content).to_string(),
));
}
Some(ToolCapability::HashEdit) => {
push_summary_meta(
&mut metadata,
summary_label::BYTES,
&result.metadata,
"input_bytes",
);
}
Some(ToolCapability::Write) => {
push_summary_meta(
&mut metadata,
summary_label::BYTES,
&result.metadata,
meta::BYTES,
);
}
Some(ToolCapability::Read) => {
if let Some(count) = call
.arguments
.get(arg_key::PATHS)
.and_then(Value::as_array)
.map(Vec::len)
{
metadata.push((summary_label::FILES.to_string(), count.to_string()));
}
push_summary_arg_or_meta(
&mut metadata,
summary_label::OFFSET,
&call.arguments,
&result.metadata,
meta::OFFSET,
);
push_summary_arg_or_meta(
&mut metadata,
summary_label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
metadata.push((
summary_label::OUTPUT_LINES.to_string(),
line_count(&result.content).to_string(),
));
}
Some(ToolCapability::ViewImage) => {
push_summary_arg_or_meta(
&mut metadata,
summary_label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_summary_meta(
&mut metadata,
meta::PROVIDER,
&result.metadata,
meta::PROVIDER,
);
push_summary_meta(&mut metadata, meta::MODEL, &result.metadata, meta::MODEL);
push_summary_meta(
&mut metadata,
meta::IMAGE_BYTES,
&result.metadata,
meta::IMAGE_BYTES,
);
push_summary_meta(
&mut metadata,
meta::TRUNCATED,
&result.metadata,
meta::TRUNCATED,
);
}
Some(ToolCapability::Grep | ToolCapability::AstGrep) => {
push_summary_arg_or_meta(
&mut metadata,
summary_label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_summary_arg_or_meta(
&mut metadata,
summary_label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
if matches!(
ToolCapability::from_dispatch_name(&call.name),
Some(ToolCapability::AstGrep)
) {
push_summary_arg_or_meta(
&mut metadata,
summary_label::LANGUAGE,
&call.arguments,
&result.metadata,
meta::LANGUAGE,
);
}
push_summary_meta(
&mut metadata,
summary_label::MATCHES_RETURNED,
&result.metadata,
meta::MATCHES_RETURNED,
);
push_summary_meta(
&mut metadata,
summary_label::EXIT_CODE,
&result.metadata,
meta::EXIT_CODE,
);
metadata.push((
summary_label::OUTPUT_LINES.to_string(),
line_count(&result.content).to_string(),
));
}
Some(ToolCapability::Diagnostics) => {
push_summary_arg_or_meta(
&mut metadata,
summary_label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_summary_arg_or_meta(
&mut metadata,
summary_label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
push_summary_meta(
&mut metadata,
meta::DIAGNOSTIC_COUNT,
&result.metadata,
meta::DIAGNOSTIC_COUNT,
);
metadata.push((
summary_label::OUTPUT_LINES.to_string(),
line_count(&result.content).to_string(),
));
}
Some(ToolCapability::References) => {
push_summary_arg_or_meta(
&mut metadata,
summary_label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_summary_arg_or_meta(
&mut metadata,
summary_label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
push_summary_meta(
&mut metadata,
meta::REFERENCES,
&result.metadata,
meta::REFERENCES,
);
metadata.push((
summary_label::OUTPUT_LINES.to_string(),
line_count(&result.content).to_string(),
));
}
Some(ToolCapability::Find) => {
push_summary_arg_or_meta(
&mut metadata,
summary_label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_summary_arg_or_meta(
&mut metadata,
summary_label::LIMIT,
&call.arguments,
&result.metadata,
meta::LIMIT,
);
push_summary_meta(
&mut metadata,
summary_label::MATCHES_RETURNED,
&result.metadata,
meta::MATCHES_RETURNED,
);
metadata.push((
summary_label::OUTPUT_LINES.to_string(),
line_count(&result.content).to_string(),
));
}
Some(ToolCapability::ListFiles) => {
push_summary_arg_or_meta(
&mut metadata,
summary_label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_summary_meta(
&mut metadata,
summary_label::FILES,
&result.metadata,
meta::FILES,
);
push_summary_meta(
&mut metadata,
meta::TRUNCATED,
&result.metadata,
meta::TRUNCATED,
);
}
Some(ToolCapability::RepoMap) => {
push_summary_arg_or_meta(
&mut metadata,
summary_label::PATH,
&call.arguments,
&result.metadata,
meta::PATH,
);
push_summary_meta(
&mut metadata,
meta::FILES_SCANNED,
&result.metadata,
meta::FILES_SCANNED,
);
push_summary_meta(
&mut metadata,
meta::FILES_PARSED,
&result.metadata,
meta::FILES_PARSED,
);
push_summary_meta(
&mut metadata,
meta::SYMBOLS,
&result.metadata,
meta::SYMBOLS,
);
push_summary_meta(
&mut metadata,
meta::TRUNCATED,
&result.metadata,
meta::TRUNCATED,
);
}
Some(ToolCapability::Subagents) => {
if let Some(tasks) = call.arguments.get(arg_key::TASKS).and_then(Value::as_array) {
metadata.push((summary_label::TASKS.to_string(), tasks.len().to_string()));
if let Some(identities) = subagent_identity_metadata(tasks) {
metadata.push((summary_label::IDENTITIES.to_string(), identities));
}
}
if let Some(summary) = result.metadata.get(meta::SUMMARY) {
if let Some(value) = scalar_value(summary.get(meta::TOTAL_TOKENS)) {
metadata.push((summary_label::TOTAL_TOKENS.to_string(), value));
}
for key in [meta::TOTAL, meta::COMPLETED, meta::FAILED] {
if let Some(value) = scalar_value(summary.get(key)) {
metadata.push((key.to_string(), value));
}
}
}
}
Some(ToolCapability::WebSearch) => {
push_summary_meta(
&mut metadata,
meta::QUERY_COUNT,
&result.metadata,
meta::QUERY_COUNT,
);
push_summary_meta(
&mut metadata,
meta::RESULTS,
&result.metadata,
meta::TOTAL_RESULTS,
);
push_summary_meta(
&mut metadata,
meta::TRUNCATED,
&result.metadata,
meta::TRUNCATED,
);
push_summary_meta(
&mut metadata,
meta::TIMEOUT,
&result.metadata,
meta::TIMEOUT,
);
}
Some(ToolCapability::CodeSearch) => {
push_summary_meta(
&mut metadata,
meta::RESULTS,
&result.metadata,
meta::RESULTS,
);
push_summary_meta(
&mut metadata,
meta::TRUNCATED,
&result.metadata,
meta::TRUNCATED,
);
push_summary_meta(
&mut metadata,
meta::TIMEOUT,
&result.metadata,
meta::TIMEOUT,
);
}
Some(ToolCapability::Browser) => {
push_summary_meta(&mut metadata, meta::ACTION, &result.metadata, meta::ACTION);
push_summary_meta(&mut metadata, meta::MODE, &result.metadata, meta::MODE);
push_summary_meta(
&mut metadata,
summary_label::EXIT_CODE,
&result.metadata,
meta::EXIT_CODE,
);
push_summary_meta(
&mut metadata,
summary_label::TIMED_OUT,
&result.metadata,
meta::TIMED_OUT,
);
push_summary_meta(
&mut metadata,
meta::TRUNCATED,
&result.metadata,
meta::TRUNCATED,
);
}
None => {}
}
metadata
}
fn subagent_identity_metadata(tasks: &[Value]) -> Option<String> {
let identities = tasks
.iter()
.enumerate()
.filter_map(|(index, task)| {
task.get(arg_key::IDENTITY)
.and_then(Value::as_str)
.map(|identity| format!("g{}={identity}", index + 1))
})
.collect::<Vec<_>>();
(!identities.is_empty()).then(|| identities.join(", "))
}
fn string_arg(args: &Value, key: &str) -> Option<String> {
args.get(key).and_then(Value::as_str).map(str::to_string)
}
fn task_count_label(count: usize) -> String {
let unit = if count == 1 { "task" } else { "tasks" };
format!("{count} {unit}")
}
fn file_count_label(count: usize) -> String {
let unit = if count == 1 { "file" } else { "files" };
format!("{count} {unit}")
}
fn push_summary_arg_or_meta(
metadata: &mut Vec<(String, String)>,
label: &str,
args: &Value,
meta: &Value,
key: &str,
) {
if let Some(value) = scalar_value(args.get(key)) {
metadata.push((label.to_string(), value));
} else {
push_summary_meta(metadata, label, meta, key);
}
}
fn push_summary_meta(metadata: &mut Vec<(String, String)>, label: &str, meta: &Value, key: &str) {
if let Some(value) = scalar_value(meta.get(key)) {
metadata.push((label.to_string(), value));
}
}
fn scalar_value(value: Option<&Value>) -> Option<String> {
match value? {
Value::String(text) => Some(text.clone()),
Value::Bool(value) => Some(value.to_string()),
Value::Number(value) => Some(value.to_string()),
_ => None,
}
}
fn line_count(content: &str) -> usize {
content.lines().count()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn call(name: &str, arguments: Value) -> ToolCall {
ToolCall {
id: "call_1".to_string(),
name: name.to_string(),
arguments,
}
}
fn result(name: &str, success: bool, content: &str, metadata: Value) -> ToolResult {
ToolResult {
tool_name: name.to_string(),
success,
content: content.to_string(),
metadata,
display: crate::tools::ToolResultDisplay::default(),
}
}
fn result_with_diff(
name: &str,
success: bool,
content: &str,
metadata: Value,
diff: &str,
) -> ToolResult {
ToolResult {
tool_name: name.to_string(),
success,
content: content.to_string(),
metadata,
display: crate::tools::ToolResultDisplay {
edit_diff: Some(diff.to_string()),
},
}
}
#[test]
fn hash_edit_display_label_pending_summary_and_diff_match_edit_shape() {
let hash_call = call("hash_edit", json!({"input":"[a.txt#ABCD]\nSWAP 1:\n1:new"}));
assert!(should_show_pending_tool(&hash_call));
assert_eq!(tool_display_label(&hash_call), "hash_edit");
assert_eq!(pending_tool_status(&hash_call), ToolStatus::Running);
assert_eq!(
pending_tool_summary_metadata(&hash_call),
vec![("bytes".to_string(), "26".to_string())]
);
let diff = "diff --git a/a.txt b/a.txt\n@@ -1,1 +1,1 @@\n-old\n+new";
let result = result_with_diff(
"hash_edit",
true,
"applied hash edit",
json!({"path":"a.txt","input_bytes":26}),
diff,
);
let summary = tool_summary_metadata(&hash_call, &result);
assert_eq!(summary, vec![("bytes".to_string(), "26".to_string())]);
let block = format_tool_activity_detail_block(&hash_call, &result);
assert!(
block.contains("Output preview\napplied hash edit"),
"{block}"
);
assert!(
block.contains("Diff preview\ndiff --git a/a.txt b/a.txt"),
"{block}"
);
}
#[test]
fn repo_map_display_label_and_summary_metadata_are_bounded() {
let repo_call = call(
"repo_map",
json!({"path":"src","query":"agent","maxTokens":512}),
);
assert!(should_show_pending_tool(&repo_call));
assert_eq!(tool_display_label(&repo_call), "repo_map src query=agent");
assert_eq!(
pending_tool_summary_metadata(&repo_call),
vec![
("path".to_string(), "src".to_string()),
("query".to_string(), "agent".to_string())
]
);
let summary = tool_summary_metadata(
&repo_call,
&result(
"repo_map",
true,
"src/lib.rs:\n └─ fn run (line 1)",
json!({"path":"src","query":"agent","files_scanned":2,"files_parsed":1,"symbols":3,"truncated":false}),
),
);
assert_eq!(
summary,
vec![
("path".to_string(), "src".to_string()),
("files_scanned".to_string(), "2".to_string()),
("files_parsed".to_string(), "1".to_string()),
("symbols".to_string(), "3".to_string()),
("truncated".to_string(), "false".to_string()),
]
);
let block = format_tool_block(
&repo_call,
&result(
"repo_map",
true,
"src/lib.rs:\n └─ fn run (line 1)",
json!({"files_scanned":2,"files_parsed":1,"symbols":3,"references":4,"truncated":false}),
),
);
assert!(block.contains("FILES_SCANNED: 2"), "{block}");
assert!(block.contains("SYMBOLS: 3"), "{block}");
assert!(block.contains("OUTPUT:\nsrc/lib.rs"), "{block}");
}
#[test]
fn subagents_tool_block_omits_wrapper_and_keeps_safe_summary() {
let block = format_tool_block(
&call(
"subagents",
json!({"tasks":[{"intent":"inspect"}],"concurrency":1}),
),
&result(
"subagents",
true,
"child output",
json!({"summary":{"total":1,"completed":1,"failed":0,"total_tokens":12345}}),
),
);
assert!(!block.contains("--- TOOL START: subagents ---"), "{block}");
assert!(!block.contains("--- TOOL END: subagents ---"), "{block}");
assert!(block.contains("STATUS: success"), "{block}");
assert!(block.contains("TASK_COUNT: 1"), "{block}");
assert!(block.contains("TASK_IDS: g1"), "{block}");
assert!(block.contains("TOTAL_TOKENS: 12345"), "{block}");
assert!(
block.contains("summary: total=1 completed=1 failed=0 total_tokens=12345"),
"{block}"
);
assert!(!block.contains("child output"), "{block}");
}
#[test]
fn subagents_tool_block_omits_aggregate_tokens_when_summary_usage_unknown() {
let block = format_tool_block(
&call(
"subagents",
json!({"tasks":[{"intent":"known"},{"intent":"unknown"}],"concurrency":2}),
),
&result(
"subagents",
true,
"child output",
json!({"summary":{"total":2,"completed":2,"failed":0}}),
),
);
assert!(
block.contains("summary: total=2 completed=2 failed=0"),
"{block}"
);
assert!(!block.contains("TOTAL_TOKENS:"), "{block}");
assert!(!block.contains("total_tokens="), "{block}");
assert!(!block.contains("child output"), "{block}");
}
#[test]
fn subagents_safe_output_parses_json_without_raw_child_newlines() {
let content = json!({
"summary": {"total": 1, "completed": 1, "failed": 0, "total_tokens": 42},
"results": [{
"id": "g1",
"status": "completed",
"intent": "inspect",
"agent": "researcher",
"identity": "tars-research-investigation",
"cwd": "/tmp/work",
"session_id": "session-1",
"session_path": "/tmp/work/session.jsonl",
"total_tokens": 42,
"changed_files": [],
"output": "line 1\nline 2",
"output_truncated": true,
"error": null
}]
})
.to_string();
let block = format_tool_activity_detail_block(
&call(
"subagents",
json!({"tasks":[{"intent":"inspect"}],"concurrency":1}),
),
&result("subagents", true, &content, json!({})),
);
assert!(
block.contains("summary: total=1 completed=1 failed=0 total_tokens=42"),
"{block}"
);
assert!(block.contains("g1=completed"), "{block}");
assert!(block.contains("output_truncated=true"), "{block}");
assert!(!block.contains("line 1"), "{block}");
assert!(!block.contains("\\n"), "{block}");
}
#[test]
fn formats_read_tool_block_with_paths_offset_limit_and_output() {
let block = format_tool_block(
&call("read", json!({"paths":["file.txt"],"offset":2,"limit":3})),
&result("read", true, "hello", json!({})),
);
assert!(!block.contains("TOOL START"));
assert!(block.contains("STATUS: success"));
assert!(block.contains("PATHS: file.txt"));
assert!(block.contains("OFFSET: 2"));
assert!(block.contains("LIMIT: 3"));
assert!(block.contains("OUTPUT:\nhello\n"));
assert!(!block.contains("TOOL END"));
assert!(!block.contains("activity preview truncated"));
}
#[test]
fn formats_multi_file_read_tool_block_with_path_values() {
let block = format_tool_block(
&call("read", json!({"paths":["a.txt", "b.txt"], "limit": 2})),
&result("read", true, "--- FILE: a.txt ---\na", json!({})),
);
assert!(block.contains("PATHS: a.txt, b.txt"), "{block}");
assert!(block.contains("LIMIT: 2"), "{block}");
assert!(!block.contains("PATH: a.txt"), "{block}");
assert!(!block.contains("TOOL START"), "{block}");
assert!(!block.contains("TOOL END"), "{block}");
}
#[test]
fn formats_bash_tool_block_with_command_timeout_exit_status_stdout_stderr() {
let block = format_tool_block(
&call("bash", json!({"command":"echo hi","timeout":9})),
&result(
"bash",
true,
"stdout:\nhi\nstderr:\n",
json!({"exit_code":0,"timed_out":false}),
),
);
assert!(block.contains("COMMAND: echo hi"));
assert!(block.contains("TIMEOUT: 9"));
assert!(block.contains("EXIT_CODE: 0"));
assert!(block.contains("TIMED_OUT: false"));
assert!(block.contains("stdout:\nhi"));
}
#[test]
fn formats_bash_cleanup_warning_without_leaking_secret_text() {
let secret = "sk-cleanupSecret123456";
let block = format_tool_block(
&call("bash", json!({"command":"printf hi"})),
&result(
"bash",
false,
"stdout:\nhi\nstderr:\n[process cleanup incomplete: TERM signal command failed]",
json!({
"exit_code": null,
"timed_out": true,
"cleanup_warning": format!("process cleanup incomplete: probe failed {secret}"),
}),
),
);
assert!(block.contains("CLEANUP_WARNING: process cleanup incomplete"));
assert!(block.contains("<redacted>"));
assert!(!block.contains(secret), "{block}");
}
#[test]
fn bash_cmd_alias_uses_contract_display_label_and_runtime_result_name() {
let block = format_tool_block(
&call("shell", json!({"cmd":"echo hi", "timeout":3})),
&result(
"bash",
true,
"stdout:\nhi\nstderr:\n",
json!({"exit_code":0,"timed_out":false}),
),
);
assert!(!block.contains("TOOL START"));
assert!(block.contains("COMMAND: echo hi"));
assert!(block.contains("TIMEOUT: 3"));
assert!(block.contains("EXIT_CODE: 0"));
assert!(block.contains("TIMED_OUT: false"));
assert!(!block.contains("CMD:"), "{block}");
}
#[test]
fn formats_failed_tool_block_with_failure_status() {
let block = format_tool_block(
&call("unknown", json!({"path":"x"})),
&result("unknown", false, "boom", json!({})),
);
assert!(block.contains("STATUS: failure"));
assert!(block.contains("OUTPUT:\nboom"));
}
#[test]
fn formats_write_without_argument_payloads() {
let write = format_tool_block(
&call("write", json!({"path":"a.txt","content":"secret payload"})),
&result("write", true, "wrote 14 bytes", json!({})),
);
assert!(write.contains("CONTENT_BYTES: 14"));
assert!(!write.contains("secret payload"));
}
#[test]
fn write_tool_result_detail_includes_written_data_for_success() {
let block = format_tool_activity_detail_block(
&call("write", json!({"path":"a.txt","content":"actual payload"})),
&result("write", true, "wrote 14 bytes", json!({})),
);
assert!(block.contains("Destination File: a.txt"));
assert!(block.contains("Written Data:\n actual payload"));
}
#[test]
fn write_tool_result_detail_omits_attempted_payload_for_failure() {
let attempted_payload = "attempted payload that was not written";
let block = format_tool_activity_detail_block(
&call("write", json!({"path":"a.txt","content":attempted_payload})),
&result("write", false, "failed before writing", json!({})),
);
assert!(block.contains("Status: failure"));
assert!(block.contains("Path: a.txt"));
assert!(!block.contains("Destination File:"), "{block}");
assert!(!block.contains("Written Data:"), "{block}");
assert!(!block.contains(attempted_payload), "{block}");
}
#[test]
fn write_tool_result_detail_redacts_secret_written_data_for_success() {
let secret = "sk-writtenSecret123456";
let block = format_tool_activity_detail_block(
&call("write", json!({"path":"secret.txt","content":secret})),
&result("write", true, "wrote secret", json!({})),
);
assert!(block.contains("Destination File: secret.txt"));
assert!(block.contains("Written Data:\n <redacted>"));
assert!(!block.contains(secret));
}
#[test]
fn formats_list_files_metadata_with_truncation() {
let block = format_tool_block(
&call("list_files", json!({"path":"dir"})),
&result(
"list_files",
true,
"a.txt\nb.txt",
json!({"path":"dir","files":2,"directories":1,"truncated":false}),
),
);
assert!(block.contains("PATH: dir"), "{block}");
assert!(block.contains("FILES: 2"), "{block}");
assert!(block.contains("DIRECTORIES: 1"), "{block}");
assert!(block.contains("TRUNCATED: false"), "{block}");
}
#[test]
fn formats_grep_find_and_subagents_metadata() {
let grep = format_tool_block(
&call(
"grep",
json!({"pattern":"fn main","path":"src","limit":5,"context":2}),
),
&result(
"grep",
true,
"src/main.rs:1:fn main",
json!({"engine":"native"}),
),
);
assert!(grep.contains("PATTERN: fn main"));
assert!(grep.contains("CONTEXT: 2"));
assert!(grep.contains("ENGINE: native"));
assert!(!grep.contains("EXIT_CODE"));
let find = format_tool_block(
&call(
"find",
json!({"query":"lib","path":"src","kind":"mixed","limit":5,"offset":1}),
),
&result(
"find",
true,
"src/lib.rs",
json!({"engine":"fff","total_matched":7,"truncated":true,"index_ready":true}),
),
);
assert!(find.contains("QUERY: lib"));
assert!(find.contains("PATH: src"));
assert!(find.contains("KIND: mixed"));
assert!(find.contains("LIMIT: 5"));
assert!(find.contains("OFFSET: 1"));
assert!(find.contains("ENGINE: fff"));
assert!(find.contains("TOTAL_MATCHED: 7"));
assert!(find.contains("TRUNCATED: true"));
assert!(find.contains("INDEX_READY: true"));
let sub = format_tool_block(
&call(
"subagents",
json!({"tasks":[{"intent":"a","identity":"frontend-dev"},{"intent":"b"}],"concurrency":2}),
),
&result("subagents", true, "done", json!({})),
);
assert!(sub.contains("TASK_COUNT: 2"));
assert!(sub.contains("TASK_IDS: g1, g2"));
assert!(sub.contains("IDENTITIES: g1=frontend-dev"));
assert!(sub.contains("CONCURRENCY: 2"));
}
#[test]
fn subagents_tool_block_omits_identity_line_when_no_identity_selected() {
let block = format_tool_block(
&call(
"subagents",
json!({"tasks":[{"intent":"a"},{"intent":"b"}],"concurrency":2}),
),
&result("subagents", true, "done", json!({})),
);
assert!(block.contains("TASK_COUNT: 2"));
assert!(!block.contains("IDENTITIES:"));
}
#[test]
fn subagents_tool_block_does_not_leak_child_secret_payloads() {
let secret = "sk-childSecret123456";
let block = format_tool_block(
&call(
"subagents",
json!({"tasks":[{"intent":"inspect"}],"concurrency":1}),
),
&result(
"subagents",
false,
&format!(
r#"{{"results":[{{"id":"g1","output":"{secret}","error":"token={secret}"}}]}}"#
),
json!({"summary":{"total":1,"completed":0,"failed":1}}),
),
);
assert!(block.contains("TASK_COUNT: 1"));
assert!(block.contains("summary: total=1 completed=0 failed=1"));
assert!(!block.contains(secret), "{block}");
assert!(!block.contains("token="), "{block}");
}
#[test]
fn tool_display_redacts_secret_values() {
let secret = "sk-testSecret123456";
let block = format_tool_block(
&call("bash", json!({"command": format!("echo {secret}")})),
&result(
"bash",
true,
&format!("stdout:\n{secret}\nstderr:\n"),
json!({}),
),
);
assert!(block.contains("<redacted>"));
assert!(!block.contains(secret));
}
#[test]
fn search_tool_blocks_show_safe_query_and_result_metadata() {
let web = format_tool_block(
&call(
"web_search",
json!({"queries":["rust"], "includeContent": true}),
),
&result(
"web_search",
true,
"# rust\nEXA_API_KEY=sk-webSecret123456",
json!({"query_count":1,"total_results":2,"include_content":true,"truncated":false}),
),
);
assert!(web.contains("QUERY: rust"));
assert!(web.contains("QUERY_COUNT: 1"));
assert!(web.contains("RESULTS: 2"));
assert!(web.contains("INCLUDE_CONTENT: true"));
assert!(web.contains("TRUNCATED: false"));
assert!(web.contains("<redacted>"));
assert!(!web.contains("sk-webSecret"));
let code = format_tool_block(
&call("code_search", json!({"query":"reqwest timeout"})),
&result(
"code_search",
true,
"context",
json!({"results":3,"truncated":true}),
),
);
assert!(code.contains("QUERY: reqwest timeout"));
assert!(code.contains("RESULTS: 3"));
assert!(code.contains("TRUNCATED: true"));
}
#[test]
fn browser_tool_block_shows_safe_metadata() {
let block = format_tool_block(
&call(
"browser",
json!({"command":"snapshot -i","mode":"connect","connectUrl":"http://user:secret@localhost:9222?token=sk-liveSecret123456"}),
),
&result(
"browser",
false,
"dev-browser failed with token=sk-resultSecret123456",
json!({
"action":"snapshot",
"mode":"connect",
"connect_url":"http://<redacted>@localhost:9222?<redacted>",
"ignore_https_errors":false,
"exit_code":7,
"timed_out":false,
"truncated":false,
"screenshot_path":"/tmp/screen.png"
}),
),
);
assert!(block.contains("COMMAND: snapshot -i"));
assert!(block.contains("MODE: connect"));
assert!(block.contains("ACTION: snapshot"));
assert!(block.contains("CONNECT_URL: http://<redacted>@localhost:9222?<redacted>"));
assert!(block.contains("EXIT_CODE: 7"));
assert!(block.contains("TIMED_OUT: false"));
assert!(block.contains("TRUNCATED: false"));
assert!(block.contains("SCREENSHOT_PATH: /tmp/screen.png"));
assert!(block.contains("<redacted>"));
assert!(!block.contains("sk-liveSecret"));
assert!(!block.contains("sk-resultSecret"));
}
#[test]
fn unknown_tool_metadata_redacts_secret_like_values_under_safe_scalar_keys() {
let secret = "sk-safeScalarSecret123456";
let block = format_tool_block(
&call(
"custom_tool",
json!({"path":"safe.txt","note":secret,"enabled":true,"count":3}),
),
&result("custom_tool", true, "ok", json!({})),
);
assert!(block.contains("PATH: safe.txt"), "{block}");
assert!(block.contains("NOTE: <redacted>"), "{block}");
assert!(block.contains("ENABLED: true"), "{block}");
assert!(block.contains("COUNT: 3"), "{block}");
assert!(!block.contains(secret), "{block}");
}
#[test]
fn unknown_tool_metadata_filters_credential_like_scalar_keys_case_insensitively() {
let block = format_tool_block(
&call(
"custom_tool",
json!({
"path": "safe.txt",
"count": 3,
"Authorization": "Bearer secret-1",
"apiKey": "secret-2",
"accessToken": "secret-3",
"ACCESS_TOKEN": "secret-4",
"token": "secret-5",
"secret": "secret-6",
"password": "secret-7",
"bearer": "secret-8",
"credential": "secret-9"
}),
),
&result("custom_tool", true, "ok", json!({})),
);
assert!(block.contains("PATH: safe.txt"));
assert!(block.contains("COUNT: 3"));
for leaked in [
"AUTHORIZATION:",
"APIKEY:",
"ACCESSTOKEN:",
"ACCESS_TOKEN:",
"TOKEN:",
"SECRET:",
"PASSWORD:",
"BEARER:",
"CREDENTIAL:",
"secret-",
] {
assert!(!block.contains(leaked), "leaked {leaked} in {block}");
}
}
}