Fix refactor recovery and metrics

This commit is contained in:
Georg Bauer
2026-07-29 18:47:14 +02:00
parent 607aaa2e7a
commit 83905690e6
2 changed files with 103 additions and 87 deletions

View File

@@ -1,5 +1,7 @@
use serde_json::{Map, Value};
const INCOMPLETE_TOOL_CALL: &str = "invalid or incomplete DSML tool call";
#[derive(Clone, Copy)]
pub(crate) struct Syntax {
pub(crate) tool_start: &'static str,
@@ -64,31 +66,40 @@ pub(crate) fn parse_tool_calls(text: &str) -> Result<(String, Vec<(String, Value
if !outer_call_complete && cursor == raw.len() && !calls.is_empty() {
break;
}
if !raw[cursor..].starts_with(syntax.invoke_start) {
return Err("invalid or incomplete DSML tool call".into());
}
let tag_end = raw[cursor..]
.find('>')
.map(|offset| cursor + offset + 1)
.ok_or_else(|| "invalid or incomplete DSML tool call".to_owned())?;
let name = attribute(&raw[cursor..tag_end], "name")
.ok_or_else(|| "invalid or incomplete DSML tool call".to_owned())?;
cursor = tag_end;
let mut arguments = Map::new();
loop {
skip_whitespace(raw, &mut cursor);
if raw[cursor..].starts_with(syntax.invoke_end) {
cursor += syntax.invoke_end.len();
let call = (|| {
if !raw[cursor..].starts_with(syntax.invoke_start) {
return Err(INCOMPLETE_TOOL_CALL.into());
}
let tag_end = raw[cursor..]
.find('>')
.map(|offset| cursor + offset + 1)
.ok_or_else(|| INCOMPLETE_TOOL_CALL.to_owned())?;
let name = attribute(&raw[cursor..tag_end], "name")
.ok_or_else(|| INCOMPLETE_TOOL_CALL.to_owned())?;
cursor = tag_end;
let mut arguments = Map::new();
loop {
skip_whitespace(raw, &mut cursor);
if raw[cursor..].starts_with(syntax.invoke_end) {
cursor += syntax.invoke_end.len();
break;
}
let (name, value) = parse_parameter(raw, &mut cursor, syntax)?
.ok_or_else(|| INCOMPLETE_TOOL_CALL.to_owned())?;
arguments.insert(name, value);
}
Ok((name, Value::Object(arguments)))
})();
match call {
Ok(call) => calls.push(call),
Err(error) if !calls.is_empty() && error == INCOMPLETE_TOOL_CALL => {
break;
}
let (name, value) = parse_parameter(raw, &mut cursor, syntax)?
.ok_or_else(|| "invalid or incomplete DSML tool call".to_owned())?;
arguments.insert(name, value);
Err(error) => return Err(error),
}
calls.push((name, Value::Object(arguments)));
}
if calls.is_empty() {
return Err("invalid or incomplete DSML tool call".into());
return Err(INCOMPLETE_TOOL_CALL.into());
}
Ok((content, calls))
}
@@ -225,4 +236,21 @@ mod tests {
assert_eq!(calls[0].0, "read");
assert_eq!(calls[0].1["path"], "src/main.rs");
}
#[test]
fn recovers_complete_invokes_before_an_incomplete_one() {
let raw = "<tool_calls><invoke name=\"first\"></invoke><invoke name=\"second\"><parameter name=\"value\">truncated";
let (_, calls) = parse_tool_calls(raw).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "first");
let invalid = "<tool_calls><invoke name=\"first\"></invoke><invoke name=\"second\"><parameter name=\"value\" string=\"false\">invalid</parameter></invoke></tool_calls>";
assert!(
parse_tool_calls(invalid)
.unwrap_err()
.starts_with("invalid DSML tool arguments")
);
}
}