use crate::{
output::{
ActivityStatus, ToolActivityDetail, ToolStatus, is_credential_like_key,
redact_sensitive_text, sanitize_display_controls, sanitize_display_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;
use std::sync::Arc;
pub(crate) fn tool_activity_detail_pending(
call: &ToolCall,
label: impl Into<String>,
status: ActivityStatus,
) -> ToolActivityDetail {
ToolActivityDetail {
tool_name: Arc::from(sanitize_display_text(&call.name)),
label: Arc::from(sanitize_display_text(&label.into())),
params: sanitize_detail_json(&call.arguments),
metadata: serde_json::json!({}),
status,
output: Arc::from(""),
applied_diff: None,
}
}
pub(crate) fn tool_activity_detail(
call: &ToolCall,
result: &ToolResult,
label: impl Into<String>,
status: ActivityStatus,
) -> ToolActivityDetail {
ToolActivityDetail {
tool_name: Arc::from(sanitize_display_text(&call.name)),
label: Arc::from(sanitize_display_text(&label.into())),
params: sanitize_detail_json(&call.arguments),
metadata: sanitize_detail_json(&result.metadata),
status,
output: Arc::from(sanitize_detail_output(&result.content)),
applied_diff: result
.display
.edit_diff
.as_deref()
.map(sanitize_detail_output)
.map(Arc::from),
}
}
fn sanitize_detail_json(value: &Value) -> Value {
match value {
Value::Object(object) => {
let mut sanitized = serde_json::Map::new();
for (key, value) in object {
let key = sanitize_display_text(key);
let value = if is_credential_like_key(&key) {
Value::String("<redacted>".to_string())
} else {
sanitize_detail_json(value)
};
let key = unique_sanitized_key(&sanitized, key);
sanitized.insert(key, value);
}
Value::Object(sanitized)
}
Value::Array(values) => Value::Array(values.iter().map(sanitize_detail_json).collect()),
Value::String(text) => Value::String(sanitize_detail_output(text)),
other => other.clone(),
}
}
fn unique_sanitized_key(object: &serde_json::Map<String, Value>, key: String) -> String {
if !object.contains_key(&key) {
return key;
}
let mut index = 2usize;
loop {
let candidate = format!("{key} [{index}]");
if !object.contains_key(&candidate) {
return candidate;
}
index += 1;
}
}
fn sanitize_detail_output(text: &str) -> String {
sanitize_detail_text(text, 0)
}
fn sanitize_detail_text(text: &str, depth: usize) -> String {
let mut replacements = Vec::<(usize, usize, String)>::new();
collect_detail_replacements(text, depth, &mut replacements);
if replacements.is_empty() {
return sanitize_display_text(text);
}
let mut output = String::with_capacity(text.len());
let mut copied_through = 0;
for (start, end, replacement) in replacements {
if start < copied_through {
continue;
}
output.push_str(&sanitize_display_text(&text[copied_through..start]));
output.push_str(&replacement);
copied_through = end;
}
output.push_str(&sanitize_display_text(&text[copied_through..]));
output
}
fn collect_detail_replacements(
text: &str,
depth: usize,
replacements: &mut Vec<(usize, usize, String)>,
) {
const MAX_JSON_STRING_DEPTH: usize = 32;
if depth >= MAX_JSON_STRING_DEPTH {
replacements.push((0, text.len(), "<redacted>".to_string()));
return;
}
let mut scan_from = 0;
while scan_from < text.len() {
let Some(relative_start) = text[scan_from..].find('"') else {
break;
};
let start = scan_from + relative_start;
let Some((token_end, decoded)) = parse_json_string_at(text, start) else {
scan_from = start + 1;
continue;
};
let after_token = skip_json_whitespace(text, token_end);
if text.as_bytes().get(after_token) == Some(&b':') {
let key = sanitize_display_text(&decoded);
if is_credential_like_key(&key) {
let value_start = skip_diff_prefixed_value_start(text, start, after_token + 1);
let value_end = json_value_end(text, value_start).unwrap_or(text.len());
let mut replacement = sanitize_display_controls(&text[start..value_start]);
replacement.push_str("\"<redacted>\"");
replacements.push((start, value_end, replacement));
scan_from = value_end.max(value_start + 1).min(text.len());
} else {
scan_from = token_end;
}
continue;
}
let mut nested = Vec::new();
collect_detail_replacements(&decoded, depth + 1, &mut nested);
if !nested.is_empty()
&& let Some(boundaries) = json_string_boundary_map(&text[start..token_end], &decoded)
{
for (nested_start, nested_end, replacement) in nested {
let Ok(start_index) =
boundaries.binary_search_by_key(&nested_start, |(decoded, _)| *decoded)
else {
continue;
};
let Ok(end_index) =
boundaries.binary_search_by_key(&nested_end, |(decoded, _)| *decoded)
else {
continue;
};
replacements.push((
start + boundaries[start_index].1,
start + boundaries[end_index].1,
encode_json_string_fragment(&replacement),
));
}
}
scan_from = token_end;
}
}
fn parse_json_string_at(text: &str, start: usize) -> Option<(usize, String)> {
let mut values = serde_json::Deserializer::from_str(&text[start..]).into_iter::<String>();
let value = values.next()?.ok()?;
let consumed = values.byte_offset();
(consumed > 0).then_some((start + consumed, value))
}
fn skip_json_whitespace(text: &str, mut index: usize) -> usize {
while matches!(
text.as_bytes().get(index),
Some(b' ' | b'\t' | b'\r' | b'\n')
) {
index += 1;
}
index
}
fn skip_diff_prefixed_value_start(text: &str, key_start: usize, index: usize) -> usize {
let line_start = text[..key_start].rfind('\n').map_or(0, |offset| offset + 1);
let prefix = text
.as_bytes()
.get(line_start)
.copied()
.filter(|prefix| matches!(prefix, b'+' | b'-'));
let mut value_start = skip_json_whitespace(text, index);
while prefix == text.as_bytes().get(value_start).copied()
&& (value_start == 0 || text.as_bytes().get(value_start - 1) == Some(&b'\n'))
{
value_start = skip_json_whitespace(text, value_start + 1);
}
value_start
}
fn json_string_boundary_map(token: &str, decoded: &str) -> Option<Vec<(usize, usize)>> {
let raw = token.strip_prefix('"')?.strip_suffix('"')?;
let mut boundaries = vec![(0, 1)];
let mut raw_offset = 0;
let mut decoded_offset = 0;
while raw_offset < raw.len() {
let consumed = if raw.as_bytes()[raw_offset] == b'\\' {
json_escape_len(&raw[raw_offset..])?
} else {
raw[raw_offset..].chars().next()?.len_utf8()
};
let raw_end = raw_offset + consumed;
let piece =
serde_json::from_str::<String>(&format!("\"{}\"", &raw[raw_offset..raw_end])).ok()?;
decoded_offset += piece.len();
raw_offset = raw_end;
boundaries.push((decoded_offset, raw_offset + 1));
}
(decoded_offset == decoded.len()).then_some(boundaries)
}
fn json_escape_len(text: &str) -> Option<usize> {
match text.as_bytes().get(1).copied()? {
b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't' => Some(2),
b'u' => {
let first = u16::from_str_radix(text.get(2..6)?, 16).ok()?;
if (0xD800..=0xDBFF).contains(&first) {
text.get(6..12)
.filter(|trail| trail.starts_with("\\u"))
.map(|_| 12)
} else {
Some(6)
}
}
_ => None,
}
}
fn encode_json_string_fragment(text: &str) -> String {
serde_json::to_string(text)
.ok()
.and_then(|encoded| {
encoded
.strip_prefix('"')
.and_then(|inner| inner.strip_suffix('"'))
.map(str::to_string)
})
.unwrap_or_else(|| "<redacted>".to_string())
}
fn json_value_end(text: &str, start: usize) -> Option<usize> {
if start >= text.len() {
return None;
}
let mut values = serde_json::Deserializer::from_str(&text[start..]).into_iter::<Value>();
if matches!(values.next(), Some(Ok(_))) && values.byte_offset() > 0 {
return Some(start + values.byte_offset());
}
if matches!(text.as_bytes().get(start), Some(b'{' | b'[')) {
return balanced_json_container_end(text, start).or(Some(text.len()));
}
let relative_end = text[start..]
.char_indices()
.find(|(_, character)| {
character.is_whitespace() || matches!(character, ',' | ';' | '}' | ']')
})
.map_or(text.len() - start, |(offset, _)| offset);
Some(start + relative_end.max(1))
}
fn balanced_json_container_end(text: &str, start: usize) -> Option<usize> {
let mut stack = Vec::new();
let mut in_string = false;
let mut escaped = false;
for (offset, character) in text[start..].char_indices() {
if in_string {
if escaped {
escaped = false;
} else if character == '\\' {
escaped = true;
} else if character == '"' {
in_string = false;
}
continue;
}
match character {
'"' => in_string = true,
'{' => stack.push('}'),
'[' => stack.push(']'),
'}' | ']' => {
if stack.pop() != Some(character) {
return None;
}
if stack.is_empty() {
return Some(start + offset + character.len_utf8());
}
}
_ => {}
}
}
None
}
pub(crate) fn format_tool_block(call: &ToolCall, result: &ToolResult) -> String {
format_tool_block_with_options(call, result)
}
fn format_tool_block_with_options(call: &ToolCall, result: &ToolResult) -> 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);
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 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 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())]);
}
#[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 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 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}");
}
}
}
#[test]
fn structured_detail_matrix_preserves_exact_params_metadata_and_output() {
let names = [
"read",
"view_image",
"bash",
"shell",
"browser",
"hash_edit",
"write",
"grep",
"ffgrep",
"find",
"fffind",
"list_files",
"repo_map",
"subagents",
"web_search",
"code_search",
"ast_grep",
"diagnostics",
"references",
"mcp__server__dynamic",
];
for name in names {
let params = serde_json::json!({"nested":{"name":name,"count":7},"enabled":true});
let metadata = serde_json::json!({"items":[name, "final-metadata"],"count":3});
let call = ToolCall {
id: "matrix".to_string(),
name: name.to_string(),
arguments: params.clone(),
};
let result = ToolResult {
tool_name: name.to_string(),
success: true,
content: format!("{name}-final-sentinel"),
metadata: metadata.clone(),
display: crate::tools::ToolResultDisplay::default(),
};
let detail = tool_activity_detail(&call, &result, "label", ActivityStatus::Success);
assert_eq!(detail.params, params, "params for {name}");
assert_eq!(detail.metadata, metadata, "metadata for {name}");
assert_eq!(detail.output.as_ref(), format!("{name}-final-sentinel"));
assert_eq!(detail.tool_name.as_ref(), name);
}
}
#[test]
fn display_safety_classifies_sanitized_credential_keys() {
let call = ToolCall {
id: "sanitized-key".to_string(),
name: "dynamic".to_string(),
arguments: serde_json::json!({"api_\u{1b}[31mkey":"must-not-display"}),
};
let detail = tool_activity_detail_pending(&call, "dynamic", ActivityStatus::Running);
assert_eq!(detail.params, serde_json::json!({"api_key":"<redacted>"}));
}
#[test]
fn round_four_redacts_credential_and_bearer_detail_values() {
let call = ToolCall {
id: "credential-detail".to_string(),
name: "dynamic".to_string(),
arguments: serde_json::json!({"credential":"opaque-credential"}),
};
let result = ToolResult {
tool_name: "dynamic".to_string(),
success: true,
content: r#"{"bearer":"opaque-bearer"}"#.to_string(),
metadata: serde_json::json!({"bearer":"metadata-bearer"}),
display: crate::tools::ToolResultDisplay::default(),
};
let detail = tool_activity_detail(&call, &result, "dynamic", ActivityStatus::Success);
assert_eq!(
detail.params,
serde_json::json!({"credential":"<redacted>"})
);
assert_eq!(detail.metadata, serde_json::json!({"bearer":"<redacted>"}));
assert!(!detail.output.contains("opaque-bearer"));
assert!(detail.output.contains("<redacted>"));
}
#[test]
fn round_six_structurally_redacts_nested_json_output_credentials() {
let call = ToolCall {
id: "nested-credential-detail".to_string(),
name: "dynamic".to_string(),
arguments: serde_json::json!({}),
};
let result = ToolResult {
tool_name: "dynamic".to_string(),
success: true,
content:
"{\n \"credential\": {\n \"value\": \"opaque-secret\"\n },\n \"visible\": true\n}"
.to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
};
let detail = tool_activity_detail(&call, &result, "dynamic", ActivityStatus::Success);
let output: Value = serde_json::from_str(&detail.output).unwrap();
assert_eq!(output["credential"], "<redacted>");
assert_eq!(output["visible"], true);
assert!(!detail.output.contains("opaque-secret"));
}
#[test]
fn round_seven_redacts_embedded_json_in_output_and_diff() {
let output_secret = "embedded-output-secret";
let diff_secret = "embedded-diff-secret";
let call = ToolCall {
id: "embedded-credential-detail".to_string(),
name: "hash_edit".to_string(),
arguments: serde_json::json!({}),
};
let result = ToolResult {
tool_name: "hash_edit".to_string(),
success: true,
content: format!(
"Result:\n{{\"credential\": {{\n\"value\":\"{output_secret}\"}}, \"visible\": true}}\nComplete"
),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay {
edit_diff: Some(format!(
"+config = {{\"bearer\": {{\n\"value\":\"{diff_secret}\"}}, \"enabled\": true}}"
)),
},
};
let detail = tool_activity_detail(&call, &result, "hash edit", ActivityStatus::Success);
assert!(!detail.output.contains(output_secret));
assert!(detail.output.contains("Result:"));
assert!(detail.output.contains("Complete"));
let diff = detail.applied_diff.unwrap();
assert!(!diff.contains(diff_secret));
assert!(diff.contains("+config ="));
assert!(diff.contains("\"enabled\": true"));
}
#[test]
fn round_eight_redacts_prefixed_json_and_common_credential_keys() {
let secrets = [
("aws_secret_access_key", "aws-secret"),
("aws_session_token", "aws-session"),
("session_token", "session-secret"),
("secret_key", "generic-secret"),
];
let call = ToolCall {
id: "prefixed-credentials".to_string(),
name: "hash_edit".to_string(),
arguments: serde_json::json!({
"aws_secret_access_key": secrets[0].1,
"aws_session_token": secrets[1].1,
"session_token": secrets[2].1,
"secret_key": secrets[3].1,
"input_tokens": 42
}),
};
let result = ToolResult {
tool_name: "hash_edit".to_string(),
success: true,
content: format!("{{\"aws_secret_access_key\":\"{}\"}}", secrets[0].1),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay {
edit_diff: Some(
"+{\"credential\": {\n+\"value\":\"diff-nested-secret\"\n+}}".to_string(),
),
},
};
let detail = tool_activity_detail(&call, &result, "hash edit", ActivityStatus::Success);
for (key, secret) in secrets {
assert_eq!(detail.params[key], "<redacted>");
assert!(!detail.output.contains(secret));
}
assert_eq!(detail.params["input_tokens"], 42);
let diff = detail.applied_diff.unwrap();
assert!(!diff.contains("diff-nested-secret"));
assert!(diff.lines().all(|line| line.starts_with('+')));
}
#[test]
fn round_nine_redacts_nested_json_strings_and_property_diff_fragments() {
let call = ToolCall {
id: "nested-json-string".to_string(),
name: "hash_edit".to_string(),
arguments: serde_json::json!({
"config": r#"{"credential":{"value":"nested-param-secret"}}"#
}),
};
let result = ToolResult {
tool_name: "hash_edit".to_string(),
success: true,
content: "updated config".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay {
edit_diff: Some(
"+ \"credential\": {\n+ \"value\":\"property-diff-secret\"\n+ }".to_string(),
),
},
};
let detail = tool_activity_detail(&call, &result, "hash edit", ActivityStatus::Success);
let nested = detail.params["config"].as_str().unwrap();
assert!(!nested.contains("nested-param-secret"));
assert_eq!(
serde_json::from_str::<Value>(nested).unwrap()["credential"],
"<redacted>"
);
let diff = detail.applied_diff.unwrap();
assert!(!diff.contains("property-diff-secret"));
assert!(diff.contains("credential"));
assert!(diff.contains("<redacted>"));
assert!(diff.lines().all(|line| line.starts_with('+')));
}
#[test]
fn round_ten_redacts_unprefixed_json_property_fragments() {
let call = ToolCall {
id: "unprefixed-json-property".to_string(),
name: "dynamic".to_string(),
arguments: serde_json::json!({}),
};
let result = ToolResult {
tool_name: "dynamic".to_string(),
success: true,
content: "Config:\n\"credential\": {\n \"value\": \"unprefixed-secret\"\n}\nDone"
.to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
};
let detail = tool_activity_detail(&call, &result, "dynamic", ActivityStatus::Success);
assert!(!detail.output.contains("unprefixed-secret"));
assert!(detail.output.contains("Config:"));
assert!(detail.output.contains("credential"));
assert!(detail.output.contains("<redacted>"));
assert!(detail.output.contains("Done"));
}
#[test]
fn round_eleven_redacts_inline_multiline_properties_without_reformatting() {
let exact_json = "{ \"number\": 1e+02, \"dup\": 1, \"dup\": 2 }";
let call = ToolCall {
id: "inline-multiline-property".to_string(),
name: "dynamic".to_string(),
arguments: serde_json::json!({"config": exact_json}),
};
let result = ToolResult {
tool_name: "dynamic".to_string(),
success: true,
content: format!(
"Config: \"credential\": {{\n\"value\":\"inline-secret\"\n}}\n{exact_json}"
),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
};
let detail = tool_activity_detail(&call, &result, "dynamic", ActivityStatus::Success);
assert_eq!(detail.params["config"], exact_json);
assert!(!detail.output.contains("inline-secret"));
assert!(
detail
.output
.contains("Config: \"credential\": \"<redacted>\"")
);
assert!(detail.output.ends_with(exact_json));
}
#[test]
fn round_eleven_preserves_exact_benign_json_and_invalid_property_text() {
let exact_json = "{\n \"number\" : 1e+02,\n \"dup\": 1, \"dup\": 2\n}";
let invalid_properties = "\"label\": not-json\n".repeat(4_096);
let call = ToolCall {
id: "exact-json-detail".to_string(),
name: "dynamic".to_string(),
arguments: serde_json::json!({}),
};
for content in [exact_json, invalid_properties.as_str()] {
let result = ToolResult {
tool_name: "dynamic".to_string(),
success: true,
content: content.to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
};
let detail = tool_activity_detail(&call, &result, "dynamic", ActivityStatus::Success);
assert_eq!(detail.output.as_ref(), content);
}
}
#[test]
fn round_twelve_redacts_split_diff_values_after_line_prefixes() {
for prefix in ['+', '-'] {
let call = ToolCall {
id: format!("split-diff-{prefix}"),
name: "hash_edit".to_string(),
arguments: serde_json::json!({}),
};
let diff = format!(
"{prefix}\"credential\":\n{prefix}{{\n{prefix}\"value\":\"split-diff-secret\"\n{prefix}}}"
);
let result = ToolResult {
tool_name: "hash_edit".to_string(),
success: true,
content: "updated".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay {
edit_diff: Some(diff),
},
};
let detail = tool_activity_detail(&call, &result, "hash edit", ActivityStatus::Success);
let sanitized = detail.applied_diff.unwrap();
assert!(!sanitized.contains("split-diff-secret"));
assert!(sanitized.contains(&format!("{prefix}\"<redacted>\"")));
assert!(sanitized.lines().all(|line| line.starts_with(prefix)));
}
}
#[test]
fn round_twelve_preserves_unrelated_escapes_in_nested_json_tokens() {
let call = ToolCall {
id: "nested-token-escapes".to_string(),
name: "dynamic".to_string(),
arguments: serde_json::json!({}),
};
let content =
r#""{\"credential\":\"nested-token-secret\",\"path\":\"a\/b\",\"letter\":\"\u0061\"}""#;
let expected = r#""{\"credential\":\"<redacted>\",\"path\":\"a\/b\",\"letter\":\"\u0061\"}""#;
let result = ToolResult {
tool_name: "dynamic".to_string(),
success: true,
content: content.to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
};
let detail = tool_activity_detail(&call, &result, "dynamic", ActivityStatus::Success);
assert_eq!(detail.output.as_ref(), expected);
}
#[test]
fn round_thirteen_normalizes_sensitive_prefix_controls() {
let call = ToolCall {
id: "credential-prefix-controls".to_string(),
name: "dynamic".to_string(),
arguments: serde_json::json!({}),
};
let result = ToolResult {
tool_name: "dynamic".to_string(),
success: true,
content: "{\"credential\":\r\"prefix-secret\"}".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
};
let detail = tool_activity_detail(&call, &result, "dynamic", ActivityStatus::Success);
assert!(!detail.output.contains('\r'));
assert!(!detail.output.contains("prefix-secret"));
assert_eq!(detail.output.as_ref(), "{\"credential\":\n\"<redacted>\"}");
}
#[test]
fn round_thirteen_preserves_values_when_sanitized_keys_collide() {
let mut arguments = serde_json::Map::new();
arguments.insert("field".to_string(), serde_json::json!("first"));
arguments.insert("field\u{1}".to_string(), serde_json::json!("second"));
let call = ToolCall {
id: "sanitized-key-collision".to_string(),
name: "dynamic".to_string(),
arguments: Value::Object(arguments),
};
let detail = tool_activity_detail_pending(&call, "dynamic", ActivityStatus::Running);
assert_eq!(detail.params["field"], "first");
assert_eq!(detail.params["field [2]"], "second");
assert_eq!(detail.params.as_object().unwrap().len(), 2);
}