use crate::llm::tools::{collect_tool_schemas, validate_tool_args};
use crate::value::VmValue;
use crate::llm::tools::name_recovery::recover_tool_name_by_unique_args;
use super::TextToolParseResult;
pub(crate) fn parse_adaptive_tool_calls(
text: &str,
tools_val: Option<&VmValue>,
) -> TextToolParseResult {
let mut result = super::parse_text_tool_calls_with_tools(text, tools_val);
if result.calls.is_empty() {
let fenced = super::parse_fenced_json_tool_calls(text);
if !fenced.calls.is_empty() {
result = fenced;
}
}
let schemas = collect_tool_schemas(tools_val, None);
let allowed: Vec<String> = schemas.iter().map(|s| s.name.clone()).collect();
for (name, arguments) in parse_tool_name_as_key_candidates(text) {
let resolved = if allowed.iter().any(|a| a == &name) {
if validate_tool_args(&name, &arguments, &schemas).is_ok() {
Some(name.clone())
} else {
None
}
} else {
recover_tool_name_by_unique_args(&arguments, &schemas, &allowed)
};
let Some(resolved_name) = resolved else {
result.violations.push(format!(
"protocol_violation: an adaptive tool-name-as-key call `{name}` did not map \
unambiguously to a presented tool and was rejected."
));
continue;
};
let candidate = serde_json::json!({
"id": format!("tc_{}", result.calls.len()),
"name": resolved_name,
"arguments": arguments,
});
if !call_already_present(&result.calls, &candidate) {
result.calls.push(candidate);
}
}
result
}
fn call_already_present(calls: &[serde_json::Value], candidate: &serde_json::Value) -> bool {
calls.iter().any(|existing| {
existing.get("name") == candidate.get("name")
&& existing.get("arguments") == candidate.get("arguments")
})
}
fn parse_tool_name_as_key_candidates(text: &str) -> Vec<(String, serde_json::Value)> {
let trimmed = super::syntax::strip_thinking_tags(text);
let trimmed = trimmed.as_ref().trim();
let Ok(serde_json::Value::Object(map)) = serde_json::from_str::<serde_json::Value>(trimmed)
else {
return Vec::new();
};
if map.len() != 1 {
return Vec::new();
}
let (key, value) = map.iter().next().expect("len==1");
if !value.is_object() {
return Vec::new();
}
if matches!(
key.as_str(),
"name" | "arguments" | "args" | "tool" | "tool_call"
) {
return Vec::new();
}
vec![(key.clone(), value.clone())]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::VmValue;
fn echo_marker_tools() -> VmValue {
let value_param = VmValue::dict([
("type", VmValue::string("string")),
("description", VmValue::string("The marker value to echo.")),
]);
let params = VmValue::dict([("value", value_param)]);
let tool = VmValue::dict([
("name", VmValue::string("echo_marker")),
(
"description",
VmValue::string("Echo the probe marker exactly."),
),
("parameters", params),
]);
VmValue::dict([("tools", VmValue::List(vec![tool].into()))])
}
fn call_names(result: &TextToolParseResult) -> Vec<String> {
result
.calls
.iter()
.map(|c| {
c.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string()
})
.collect()
}
#[test]
fn recovers_tool_name_as_key_dialect() {
let tools = echo_marker_tools();
let out =
parse_adaptive_tool_calls(r#"{"echo_marker": {"value": "MK-7Q3Z"}}"#, Some(&tools));
assert_eq!(call_names(&out), vec!["echo_marker".to_string()]);
assert_eq!(
out.calls[0]
.get("arguments")
.and_then(|a| a.get("value"))
.and_then(|v| v.as_str()),
Some("MK-7Q3Z")
);
}
#[test]
fn rejects_tool_name_as_key_for_unknown_tool() {
let tools = echo_marker_tools();
let out =
parse_adaptive_tool_calls(r#"{"delete_everything": {"path": "/"}}"#, Some(&tools));
assert!(out.calls.is_empty(), "unknown tool must not be dispatched");
assert!(out
.violations
.iter()
.any(|v| v.contains("did not map unambiguously")));
}
#[test]
fn fails_closed_without_presented_tools() {
let out = parse_adaptive_tool_calls(r#"{"echo_marker": {"value": "MK"}}"#, None);
assert!(out.calls.is_empty());
}
#[test]
fn still_parses_canonical_tagged_calls() {
let tools = echo_marker_tools();
let out = parse_adaptive_tool_calls(
"<tool_call>echo_marker({ value: \"MK-7Q3Z\" })</tool_call>",
Some(&tools),
);
assert_eq!(call_names(&out), vec!["echo_marker".to_string()]);
}
#[test]
fn credits_back_to_back_tagged_calls() {
let tools = echo_marker_tools();
let out = parse_adaptive_tool_calls(
"<tool_call>echo_marker({ value: \"a\" })</tool_call>\n\
<tool_call>echo_marker({ value: \"b\" })</tool_call>",
Some(&tools),
);
assert_eq!(out.calls.len(), 2, "both tagged calls recovered");
}
#[test]
fn does_not_double_count_when_two_lanes_agree() {
let tools = echo_marker_tools();
let out = parse_adaptive_tool_calls(r#"{"echo_marker": {"value": "x"}}"#, Some(&tools));
assert_eq!(out.calls.len(), 1);
}
}