use serde_json::Value;
pub const UNPARSED_ARGUMENTS_KEY: &str = "__partial_json";
pub fn parse_tool_arguments(raw: &str) -> Value {
if raw.trim().is_empty() {
return Value::Object(Default::default());
}
match serde_json::from_str(raw) {
Ok(Value::Null) => Value::Object(Default::default()),
Ok(v @ Value::Object(_)) => v,
Ok(_) | Err(_) => {
let mut obj = serde_json::Map::new();
obj.insert(UNPARSED_ARGUMENTS_KEY.into(), Value::String(raw.into()));
Value::Object(obj)
}
}
}
pub(crate) fn finalize_tool_arguments(tool_name: &str, raw: &str) -> Value {
let args = parse_tool_arguments(raw);
if unparsed_tool_arguments(&args).is_some() {
tracing::warn!(
tool = %tool_name,
len = raw.len(),
"tool-call arguments are not a JSON object (truncated, malformed, or not an object); \
the call will be answered with an error instead of run"
);
}
args
}
pub fn unparsed_tool_arguments(arguments: &Value) -> Option<&str> {
let obj = arguments.as_object()?;
if obj.len() != 1 {
return None;
}
obj.get(UNPARSED_ARGUMENTS_KEY)?.as_str()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn empty_text_is_a_zero_argument_call() {
assert_eq!(parse_tool_arguments(""), json!({}));
assert_eq!(parse_tool_arguments(" \n"), json!({}));
assert_eq!(unparsed_tool_arguments(&json!({})), None);
}
#[test]
fn valid_json_passes_through() {
let v = parse_tool_arguments(r#"{"path":"a.txt"}"#);
assert_eq!(v, json!({"path": "a.txt"}));
assert_eq!(unparsed_tool_arguments(&v), None);
}
#[test]
fn malformed_text_is_marked_not_defaulted() {
let v = parse_tool_arguments(r#"{"paths":"#);
assert_eq!(v, json!({"__partial_json": r#"{"paths":"#}));
assert_eq!(unparsed_tool_arguments(&v), Some(r#"{"paths":"#));
}
#[test]
fn null_is_a_zero_argument_call() {
assert_eq!(parse_tool_arguments("null"), json!({}));
assert_eq!(parse_tool_arguments(" null "), json!({}));
}
#[test]
fn non_object_json_is_marked_not_passed_through() {
for raw in ["\"src\"", "42", "-1.5", "[1,2]", "[]", "true", "false"] {
let v = parse_tool_arguments(raw);
assert_eq!(
unparsed_tool_arguments(&v),
Some(raw),
"{raw} is valid JSON but not an object; it must not reach a tool"
);
}
}
#[test]
fn double_encoded_arguments_are_marked() {
let raw = r#""{\"path\":\"src\"}""#;
assert_eq!(
serde_json::from_str::<Value>(raw).unwrap(),
json!(r#"{"path":"src"}"#),
"precondition: this really is a JSON string holding an object"
);
let v = parse_tool_arguments(raw);
assert_eq!(unparsed_tool_arguments(&v), Some(raw));
}
#[test]
fn marker_key_among_other_keys_is_not_the_marker() {
let v = json!({"__partial_json": "x", "other": 1});
assert_eq!(unparsed_tool_arguments(&v), None);
}
}