use serde_json::Value;
use super::validate::parse_json_pointer;
pub fn format_structured_error(schema: &Value, args: &Value, errors: &[String]) -> String {
let summary = errors.join("; ");
let args_str = serde_json::to_string(args).unwrap_or_default();
let truncated = if args_str.len() > 200 {
format!("{}…", crate::text::head(&args_str, 200))
} else {
args_str
};
let schema_hint = extract_schema_hint(schema, errors);
let concrete_hint =
enum_hint(schema, args, errors).unwrap_or_else(|| build_concrete_hint(errors));
format!(
"Tool input rejected: {summary}\n\
Expected: {schema_hint}\n\
Got: {truncated}\n\
Try: {concrete_hint}"
)
}
fn enum_hint(schema: &Value, args: &Value, errors: &[String]) -> Option<String> {
for err in errors {
let path_start = err.strip_prefix("at /")?;
let path = path_start.split(':').next().unwrap_or(path_start).trim();
let parts = parse_json_pointer(&format!("/{path}"));
let prop_schema = navigate_schema(schema, &parts)?;
let variants = prop_schema.get("enum").and_then(|v| v.as_array())?;
let valid: Vec<String> = variants
.iter()
.map(|v| match v {
Value::String(s) => s.clone(),
other => other.to_string(),
})
.collect();
if valid.is_empty() {
continue;
}
let got = args.pointer(&format!("/{path}"));
let mut hint = format!("Valid values: {}", valid.join(", "));
if let Some(Value::String(bad)) = got
&& let Some(near) = crate::agent::agent_loop::suggest::closest(bad, &valid)
{
hint.push_str(&format!(". Did you mean `{near}`?"));
}
return Some(hint);
}
None
}
fn extract_schema_hint(schema: &Value, errors: &[String]) -> String {
for err in errors {
if let Some(path_start) = err.strip_prefix("at /") {
let path = path_start.split(':').next().unwrap_or(path_start).trim();
let parts = parse_json_pointer(&format!("/{path}"));
if let Some(prop_schema) = navigate_schema(schema, &parts) {
return serde_json::to_string(prop_schema)
.unwrap_or_else(|_| "(schema unavailable)".into());
}
}
}
"(see tool schema)".into()
}
pub(super) fn navigate_schema<'a>(schema: &'a Value, parts: &[String]) -> Option<&'a Value> {
let mut current = schema;
for part in parts {
if part.parse::<usize>().is_ok() {
current = current.get("items")?;
} else {
current = current.get("properties")?.get(part)?;
}
}
Some(current)
}
pub(super) fn build_concrete_hint(errors: &[String]) -> String {
for err in errors {
let lower = err.to_lowercase();
if lower.contains("null") {
return "Remove the null value — the field is not required".into();
}
if lower.contains("array") && lower.contains("string") {
return "Wrap the value in square brackets to make it an array".into();
}
if lower.contains("array") && lower.contains("object") {
return "Replace {} with [] (empty array)".into();
}
if lower.contains("array") {
return "The value should be an array, e.g. wrap it in square brackets".into();
}
if lower.contains("missing") {
return "Make sure all required fields are present".into();
}
}
"Check the tool schema and retry with valid arguments".into()
}
pub fn is_path_field_name(key: &str) -> bool {
matches!(key, "path" | "file_path" | "filename" | "paths" | "dir")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn schema_with_enum() -> Value {
json!({
"type": "object",
"properties": {
"mode": { "type": "string", "enum": ["read", "write", "append"] }
}
})
}
#[test]
fn enum_violation_lists_valid_values_and_nearest() {
let schema = schema_with_enum();
let args = json!({ "mode": "writ" });
let errors = vec!["at /mode: \"writ\" is not one of [...]".to_string()];
let out = format_structured_error(&schema, &args, &errors);
assert!(out.contains("Valid values: read, write, append"), "{out}");
assert!(out.contains("Did you mean `write`?"), "{out}");
}
#[test]
fn enum_without_close_match_still_lists_values() {
let schema = schema_with_enum();
let args = json!({ "mode": "zzzzzz" });
let errors = vec!["at /mode: \"zzzzzz\" is not one of [...]".to_string()];
let out = format_structured_error(&schema, &args, &errors);
assert!(out.contains("Valid values: read, write, append"), "{out}");
assert!(
!out.contains("Did you mean"),
"no near match → no guess: {out}"
);
}
#[test]
fn non_enum_error_uses_generic_hint() {
let schema = json!({
"type": "object",
"properties": { "items": { "type": "array" } }
});
let args = json!({ "items": "x" });
let errors = vec!["at /items: \"x\" is not of type array".to_string()];
let out = format_structured_error(&schema, &args, &errors);
assert!(!out.contains("Valid values:"), "{out}");
assert!(
out.contains("array"),
"falls back to the generic array hint: {out}"
);
}
}