use serde_json::Value;
#[cfg(any(feature = "anthropic", feature = "openai"))]
use serde_json::json;
#[cfg(any(feature = "anthropic", feature = "openai"))]
use tracing::warn;
#[cfg(any(feature = "anthropic", feature = "openai"))]
pub(crate) fn resolve_tool_args(name: &str, args_json: &str) -> (Value, Option<String>) {
if args_json.trim().is_empty() {
return (json!({}), None);
}
match serde_json::from_str(args_json) {
Ok(v) => (v, None),
Err(e) => {
let msg = format!("malformed tool arguments for '{name}': {e} (got: {args_json})");
warn!(error = %e, name = %name, "tool_call args not valid JSON; surfacing tool error");
(json!({}), Some(msg))
}
}
}
pub(crate) fn extract_canonical_path(args: &Value) -> Option<String> {
let path_str = args.get("path").and_then(|v| v.as_str())?;
let path = std::path::Path::new(path_str);
if let Ok(p) = dunce::canonicalize(path) {
return Some(p.display().to_string());
}
let parent = path.parent()?;
let file = path.file_name()?;
let parent = if parent.as_os_str().is_empty() {
std::path::Path::new(".")
} else {
parent
};
dunce::canonicalize(parent)
.ok()
.map(|p| p.join(file).display().to_string())
}
#[cfg(all(test, any(feature = "anthropic", feature = "openai")))]
mod tests {
use super::*;
#[test]
fn resolve_tool_args_valid_empty_and_malformed() {
let (args, err) = resolve_tool_args("view_file", r#"{"path":"main.rs"}"#);
assert!(err.is_none());
assert_eq!(args["path"], "main.rs");
for empty in ["", " "] {
let (args, err) = resolve_tool_args("list_subdomains", empty);
assert!(err.is_none(), "empty args must NOT be treated as malformed");
assert_eq!(args, json!({}));
}
let (args, err) = resolve_tool_args("edit_file", r#"{"path":"a.rs","content":"#);
assert!(err.unwrap().contains("malformed tool arguments for 'edit_file'"));
assert_eq!(args, json!({}));
}
}