use super::super::ToolDefinition;
use super::args::{ARG_KEY, ARG_VALUE, ARG_VALUE_END, convert_args};
use super::judge::{is_name_char, reject_reason, typed_arguments};
const THINK_END: &str = "</think>";
const TOOL_CALL: &str = "<tool_call>";
const TOOL_CALL_END: &str = "</tool_call>";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
Nested,
Abandoned,
}
pub(super) fn recover_intended_call(
outer: &str,
raw_args: &str,
tools: &[ToolDefinition],
) -> Option<String> {
let (shape, name, inner_args) =
find_nested(raw_args).or_else(|| find_abandoned(raw_args, tools))?;
let inner_args = inner_args.strip_suffix(TOOL_CALL_END).unwrap_or(inner_args);
if reject_reason(name, &convert_args(inner_args), tools).is_some() {
return None;
}
let arguments = typed_arguments(inner_args, tools, name);
let subject = if outer.is_empty() {
"this call".to_string()
} else {
format!("this {outer} call")
};
let how = match shape {
Shape::Nested => {
"was opened inside your reasoning, so its arguments took in the rest of the \
reasoning and the call you wrote after it"
}
Shape::Abandoned => {
"was left unfinished: another call was started inside one of its argument values"
}
};
tracing::warn!(
outer,
recovered = name,
?shape,
"glm47 tool call holds another call; refusing it with the recovered call as a hint"
);
Some(format!(
"Refused, nothing was run: {subject} {how}. The call you meant appears to be \
{name} with arguments {arguments}; issue that call by itself. (To write tool-call \
tags literally inside an argument, split them, e.g. '</th' + 'ink>'.)"
))
}
fn find_nested(raw: &str) -> Option<(Shape, &str, &str)> {
let mut found = None;
let mut from = 0;
while let Some(rel) = raw[from..].find(THINK_END) {
let at = from + rel;
from = at + THINK_END.len();
if let Some(hit) = nested_call_at(raw, from) {
found = Some(hit);
}
}
found.map(|(name, args)| (Shape::Nested, name, args))
}
fn nested_call_at(raw: &str, pos: usize) -> Option<(&str, &str)> {
let rest = raw[pos..]
.trim_start()
.strip_prefix(TOOL_CALL)?
.trim_start();
let name_len = rest.find(|c: char| !is_name_char(c)).unwrap_or(rest.len());
let name = &rest[..name_len];
if !super::judge::is_name_shaped(name) {
return None;
}
let args = rest[name_len..].trim_start();
args.starts_with(ARG_KEY).then_some((name, args))
}
fn find_abandoned<'r>(raw: &'r str, tools: &[ToolDefinition]) -> Option<(Shape, &'r str, &'r str)> {
let mut best: Option<(usize, &'r str)> = None;
for tool in tools {
let name = tool.function.name.as_str();
if name.is_empty() {
continue;
}
let needle = format!("{name}{ARG_KEY}");
let mut from = 0;
while let Some(rel) = raw[from..].find(&needle) {
let at = from + rel;
from = at + 1;
let starts_mid_name = raw[..at].chars().next_back().is_none_or(is_name_char);
if starts_mid_name || !inside_value(raw, at) {
continue;
}
if best.is_none_or(|(b, _)| at < b) {
best = Some((at, &raw[at..at + name.len()]));
}
break;
}
}
best.map(|(at, name)| (Shape::Abandoned, name, &raw[at + name.len()..]))
}
fn inside_value(raw: &str, pos: usize) -> bool {
let before = &raw[..pos];
match (before.rfind(ARG_VALUE), before.rfind(ARG_VALUE_END)) {
(Some(open), Some(close)) => open > close,
(Some(_), None) => true,
(None, _) => false,
}
}