use super::*;
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::Web | ToolCapability::ViewImage) => {}
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));
}
}
_ => {}
}
metadata
}
pub(crate) fn bash_protection_summary(metadata: &Value) -> Option<String> {
let protection = metadata.get("bash_protection")?;
let decision = protection.get("decision")?.as_str()?;
match (
protection.get("score").and_then(Value::as_f64),
protection.get("confidence").and_then(Value::as_f64),
) {
(Some(score), Some(confidence)) => Some(format!(
"risk {score:.2}/2 · confidence {:.0}% · {decision}{}",
confidence * 100.0,
if protection.get("cached").and_then(Value::as_bool) == Some(true) {
" (cached)"
} else {
""
}
)),
_ => Some(decision.to_string()),
}
}
pub(crate) fn bash_file_tracking_summary(metadata: &Value) -> Option<String> {
let tracking = metadata.get("bash_file_tracking");
let activity = metadata
.get("inferred_file_activity")
.and_then(Value::as_array);
if tracking.is_none() && activity.is_none() {
return None;
}
let status = tracking
.and_then(|value| value.get("status"))
.and_then(Value::as_str)
.unwrap_or("completed");
let omitted = match status {
"completed" => None,
"command_limit" => Some("skipped (command limit)"),
"canceled" => Some("skipped (canceled)"),
"busy" => Some("skipped (worker busy)"),
"unavailable" => Some("skipped (worker unavailable)"),
"timed_out" => Some("timed out"),
"error" => Some("assessment failed"),
"redacted" => Some("skipped (sensitive command)"),
"unsupported" => Some("skipped (unsupported command)"),
"no_candidates" => Some("skipped (no literal paths)"),
"command_failed" => Some("skipped (command did not succeed)"),
_ => return None,
};
if let Some(outcome) = omitted {
return Some(format!("file tracking · {outcome}"));
}
let mut files = std::collections::BTreeMap::<&str, u8>::new();
let mut confidence: Option<f64> = None;
let mut local = tracking
.and_then(|value| value.get("source"))
.and_then(Value::as_str)
== Some("shell");
for item in activity.into_iter().flatten().take(32) {
let Some(path) = item
.get("path")
.and_then(Value::as_str)
.filter(|path| !path.is_empty())
else {
continue;
};
let reasons = match item.get("operation").and_then(Value::as_str) {
Some("read") => 1,
Some("write") => 2,
Some("edit") => 4,
Some("read_write") => 3,
_ => continue,
};
*files.entry(path).or_default() |= reasons;
local |= item.get("source").and_then(Value::as_str) == Some("shell");
if let Some(value) = item
.get("confidence")
.and_then(Value::as_f64)
.filter(|value| (0.0..=1.0).contains(value))
{
confidence = Some(confidence.map_or(value, |minimum| minimum.min(value)));
}
}
let mut summary = if files.is_empty() {
"file tracking · no files inferred".to_string()
} else {
let count = |bit| {
files
.values()
.filter(|reasons| **reasons & bit != 0)
.count()
};
format!(
"file tracking · {} file{} inferred · R {} / W {} / E {}",
files.len(),
if files.len() == 1 { "" } else { "s" },
count(1),
count(2),
count(4)
)
};
if let Some(confidence) = confidence {
summary.push_str(&format!(" · min confidence {:.0}%", confidence * 100.0));
}
if local {
summary.push_str(" · local");
}
Some(summary)
}
pub(crate) fn prompt_injection_protection_summary(metadata: &Value) -> Option<String> {
let protection = metadata.get("prompt_injection_protection")?;
let action = protection.get("action")?.as_str()?;
if action == "not_assessed" {
return None;
}
let mode = if protection.get("enforced").and_then(Value::as_bool) == Some(true) {
"enforced"
} else {
"observe-only"
};
let cached = if protection.get("cached").and_then(Value::as_bool) == Some(true) {
" · cached"
} else {
""
};
match (
protection
.get("instruction_probability")
.and_then(Value::as_f64),
protection
.get("injection_probability")
.and_then(Value::as_f64),
protection.get("impact_score").and_then(Value::as_f64),
protection.get("impact_confidence").and_then(Value::as_f64),
) {
(Some(instruction), Some(injection), Some(impact), Some(confidence)) => Some(format!(
"instructions {:.0}% · injection {:.0}% · impact {impact:.2}/2 · confidence {:.0}% · {action} · {mode}{cached}",
instruction * 100.0,
injection * 100.0,
confidence * 100.0,
)),
_ => Some(format!("prompt injection: {action} · {mode}{cached}")),
}
}
pub(crate) fn humanize_protection_summary(metadata: &Value) -> Option<String> {
let protection = metadata.get("humanize_protection")?;
let decision = protection.get("decision")?.as_str()?;
match (
protection
.get("content_probability")
.and_then(Value::as_f64),
protection
.get("humanized_probability")
.and_then(Value::as_f64),
) {
(Some(content), Some(humanized)) => Some(format!(
"prose {:.0}% · humanized {:.0}% · {decision}{}",
content * 100.0,
humanized * 100.0,
if protection.get("cached").and_then(Value::as_bool) == Some(true) {
" (cached)"
} else {
""
}
)),
(Some(content), None) => Some(format!(
"prose {:.0}% · {decision}{}",
content * 100.0,
if protection.get("cached").and_then(Value::as_bool) == Some(true) {
" (cached)"
} else {
""
}
)),
_ => Some(decision.to_string()),
}
}
pub(super) fn append_bash_protection_detail(lines: &mut Vec<String>, metadata: &Value) {
let Some(protection) = metadata.get("bash_protection") else {
return;
};
if let Some(level) = protection.get("protection_level").and_then(Value::as_str) {
lines.push(format!("JEV PROTECTION LEVEL: {level}"));
}
if let Some(summary) = bash_protection_summary(metadata) {
lines.push(format!("JEV: {summary}"));
}
if let Some(probabilities) = protection.get("probabilities").and_then(Value::as_object) {
let values = probabilities
.iter()
.filter_map(|(level, value)| {
value
.as_f64()
.map(|probability| format!("{level}={:.0}%", probability * 100.0))
})
.collect::<Vec<_>>();
if !values.is_empty() {
lines.push(format!("JEV PROBABILITIES: {}", values.join(", ")));
}
}
if let Some(policy) = protection.get("failure_policy").and_then(Value::as_str) {
lines.push(format!("JEV FAILURE POLICY: {policy}"));
}
}
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(),
));
if let Some(summary) = bash_protection_summary(&result.metadata) {
metadata.push(("jev".to_string(), summary));
}
if let Some(summary) = bash_file_tracking_summary(&result.metadata) {
metadata.push(("jev".to_string(), summary));
}
}
Some(ToolCapability::HashEdit) => {
push_summary_meta(
&mut metadata,
summary_label::BYTES,
&result.metadata,
"input_bytes",
);
if let Some(summary) = humanize_protection_summary(&result.metadata) {
metadata.push(("jev".to_string(), summary));
}
}
Some(ToolCapability::Write) => {
if let Some(summary) = humanize_protection_summary(&result.metadata) {
metadata.push(("jev".to_string(), summary));
}
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::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::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::Web) => {
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::MagiControl) => {}
None => {}
}
if let Some(summary) = prompt_injection_protection_summary(&result.metadata) {
metadata.push(("jev injection".to_string(), summary));
}
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(", "))
}
pub(super) fn string_arg(args: &Value, key: &str) -> Option<String> {
args.get(key).and_then(Value::as_str).map(str::to_string)
}
pub(super) fn task_count_label(count: usize) -> String {
let unit = if count == 1 { "task" } else { "tasks" };
format!("{count} {unit}")
}
pub(super) 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()
}