fix(agent): rebase alias-tag parser and align channel send API
This commit is contained in:
parent
4243d8ec86
commit
40ab5c3507
1 changed files with 27 additions and 3 deletions
|
|
@ -330,7 +330,6 @@ fn parse_tool_calls_from_json_value(value: &serde_json::Value) -> Vec<ParsedTool
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOL_CALL_OPEN_TAGS: [&str; 3] = ["<tool_call>", "<toolcall>", "<tool-call>"];
|
const TOOL_CALL_OPEN_TAGS: [&str; 3] = ["<tool_call>", "<toolcall>", "<tool-call>"];
|
||||||
const TOOL_CALL_CLOSE_TAGS: [&str; 3] = ["</tool_call>", "</toolcall>", "</tool-call>"];
|
|
||||||
|
|
||||||
fn find_first_tag<'a>(haystack: &str, tags: &'a [&'a str]) -> Option<(usize, &'a str)> {
|
fn find_first_tag<'a>(haystack: &str, tags: &'a [&'a str]) -> Option<(usize, &'a str)> {
|
||||||
tags.iter()
|
tags.iter()
|
||||||
|
|
@ -338,6 +337,15 @@ fn find_first_tag<'a>(haystack: &str, tags: &'a [&'a str]) -> Option<(usize, &'a
|
||||||
.min_by_key(|(idx, _)| *idx)
|
.min_by_key(|(idx, _)| *idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn matching_tool_call_close_tag(open_tag: &str) -> Option<&'static str> {
|
||||||
|
match open_tag {
|
||||||
|
"<tool_call>" => Some("</tool_call>"),
|
||||||
|
"<toolcall>" => Some("</toolcall>"),
|
||||||
|
"<tool-call>" => Some("</tool-call>"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract JSON values from a string.
|
/// Extract JSON values from a string.
|
||||||
///
|
///
|
||||||
/// # Security Warning
|
/// # Security Warning
|
||||||
|
|
@ -426,8 +434,12 @@ fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>) {
|
||||||
text_parts.push(before.trim().to_string());
|
text_parts.push(before.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(close_tag) = matching_tool_call_close_tag(open_tag) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
let after_open = &remaining[start + open_tag.len()..];
|
let after_open = &remaining[start + open_tag.len()..];
|
||||||
if let Some((close_idx, close_tag)) = find_first_tag(after_open, &TOOL_CALL_CLOSE_TAGS) {
|
if let Some(close_idx) = after_open.find(close_tag) {
|
||||||
let inner = &after_open[..close_idx];
|
let inner = &after_open[..close_idx];
|
||||||
let mut parsed_any = false;
|
let mut parsed_any = false;
|
||||||
let json_values = extract_json_values(inner);
|
let json_values = extract_json_values(inner);
|
||||||
|
|
@ -454,7 +466,7 @@ fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>) {
|
||||||
// (e.g., in emails, files, or web pages) could include JSON that mimics a
|
// (e.g., in emails, files, or web pages) could include JSON that mimics a
|
||||||
// tool call. Tool calls MUST be explicitly wrapped in either:
|
// tool call. Tool calls MUST be explicitly wrapped in either:
|
||||||
// 1. OpenAI-style JSON with a "tool_calls" array
|
// 1. OpenAI-style JSON with a "tool_calls" array
|
||||||
// 2. ZeroClaw <invoke>...</invoke> tags
|
// 2. ZeroClaw tool-call tags (<tool_call>, <toolcall>, <tool-call>)
|
||||||
// This ensures only the LLM's intentional tool calls are executed.
|
// This ensures only the LLM's intentional tool calls are executed.
|
||||||
|
|
||||||
// Remaining text after last tool call
|
// Remaining text after last tool call
|
||||||
|
|
@ -1541,6 +1553,18 @@ I will now call the tool with this payload:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_tool_calls_does_not_cross_match_alias_tags() {
|
||||||
|
let response = r#"<toolcall>
|
||||||
|
{"name": "shell", "arguments": {"command": "date"}}
|
||||||
|
</tool_call>"#;
|
||||||
|
|
||||||
|
let (text, calls) = parse_tool_calls(response);
|
||||||
|
assert!(calls.is_empty());
|
||||||
|
assert!(text.contains("<toolcall>"));
|
||||||
|
assert!(text.contains("</tool_call>"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_tool_calls_rejects_raw_tool_json_without_tags() {
|
fn parse_tool_calls_rejects_raw_tool_json_without_tags() {
|
||||||
// SECURITY: Raw JSON without explicit wrappers should NOT be parsed
|
// SECURITY: Raw JSON without explicit wrappers should NOT be parsed
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue