use super::*;
pub(super) fn append_metadata_lines(lines: &mut Vec<String>, call: &ToolCall, result: &ToolResult) {
match ToolCapability::from_dispatch_name(&call.name) {
Some(ToolCapability::Read) => {
push_read_arg_string(lines, label::PATH, &call.arguments, arg_key::PATH);
push_read_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);
append_bash_protection_detail(lines, &result.metadata);
if let Some(summary) = bash_file_tracking_summary(&result.metadata) {
lines.push(format!("JEV: {summary}"));
}
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_grep_patterns(lines, &call.arguments);
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::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::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::Web) => {
push_arg_string(lines, "OPERATION", &call.arguments, "operation");
push_arg_string(lines, "URL", &call.arguments, "url");
push_arg_string(lines, "REFERENCE", &call.arguments, "reference");
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::MagiControl) => {
push_arg_string(lines, "ACTION", &call.arguments, arg_key::ACTION);
}
None => append_safe_scalar_arguments(lines, &call.arguments),
}
}
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_read_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}: {}", sanitize_read_target(value)));
}
}
fn push_read_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(sanitize_read_target)
.collect::<Vec<_>>()
})
.unwrap_or_default();
if !values.is_empty() {
lines.push(format!("{label}: {}", values.join(", ")));
}
}
fn push_grep_patterns(lines: &mut Vec<String>, args: &Value) {
let patterns = args
.get(arg_key::PATTERNS)
.and_then(Value::as_array)
.map(|values| {
values
.iter()
.filter_map(Value::as_str)
.map(redact_sensitive_text)
.collect::<Vec<_>>()
})
.unwrap_or_default();
if !patterns.is_empty() {
lines.push(format!("{}: {}", label::PATTERN, patterns.join(", ")));
} else {
push_arg_string(lines, label::PATTERN, args, arg_key::PATTERN);
}
}
pub(super) fn grep_pattern_label(call: &ToolCall) -> String {
let patterns = call
.arguments
.get(arg_key::PATTERNS)
.and_then(Value::as_array)
.map(|values| values.iter().filter_map(Value::as_str).collect::<Vec<_>>())
.filter(|patterns| !patterns.is_empty())
.map(|patterns| patterns.join(", "))
.or_else(|| string_arg(&call.arguments, arg_key::PATTERN));
patterns
.map(|patterns| format!("{} {patterns}", call.name))
.unwrap_or_else(|| call.name.clone())
}
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)
}