fix(agent): prevent duplicate tool schema injection in XML dispatcher (#3744)
Remove duplicate tool listing from XmlToolDispatcher::prompt_instructions() since tool listing is already handled by ToolsSection in prompt.rs. The method now only emits the XML protocol envelope. Also fix UTF-8 char boundary panics in memory consolidation truncation by using char_indices() instead of manual byte-boundary scanning. Fixes #3643 Supersedes #3678 Co-authored-by: TJUEZ <TJUEZ@users.noreply.github.com>
This commit is contained in:
parent
013fca6ad2
commit
c3a3cfc9a6
@ -128,7 +128,7 @@ impl ToolDispatcher for XmlToolDispatcher {
|
||||
ConversationMessage::Chat(ChatMessage::user(format!("[Tool results]\n{content}")))
|
||||
}
|
||||
|
||||
fn prompt_instructions(&self, tools: &[Box<dyn Tool>]) -> String {
|
||||
fn prompt_instructions(&self, _tools: &[Box<dyn Tool>]) -> String {
|
||||
let mut instructions = String::new();
|
||||
instructions.push_str("## Tool Use Protocol\n\n");
|
||||
instructions
|
||||
@ -136,17 +136,6 @@ impl ToolDispatcher for XmlToolDispatcher {
|
||||
instructions.push_str(
|
||||
"```\n<tool_call>\n{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n```\n\n",
|
||||
);
|
||||
instructions.push_str("### Available Tools\n\n");
|
||||
|
||||
for tool in tools {
|
||||
let _ = writeln!(
|
||||
instructions,
|
||||
"- **{}**: {}\n Parameters: `{}`",
|
||||
tool.name(),
|
||||
tool.description(),
|
||||
tool.parameters_schema()
|
||||
);
|
||||
}
|
||||
|
||||
instructions
|
||||
}
|
||||
|
||||
@ -1282,8 +1282,12 @@ fn xml_dispatcher_generates_tool_instructions() {
|
||||
|
||||
assert!(instructions.contains("## Tool Use Protocol"));
|
||||
assert!(instructions.contains("<tool_call>"));
|
||||
assert!(instructions.contains("echo"));
|
||||
assert!(instructions.contains("Echoes the input"));
|
||||
// Tool listing is handled by ToolsSection in prompt.rs, not by the
|
||||
// dispatcher. prompt_instructions() must only emit the protocol envelope.
|
||||
assert!(
|
||||
!instructions.contains("echo"),
|
||||
"dispatcher should not duplicate tool listing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -45,10 +45,12 @@ pub async fn consolidate_turn(
|
||||
// Truncate very long turns to avoid wasting tokens on consolidation.
|
||||
// Use char-boundary-safe slicing to prevent panic on multi-byte UTF-8 (e.g. CJK text).
|
||||
let truncated = if turn_text.len() > 4000 {
|
||||
let mut end = 4000;
|
||||
while end > 0 && !turn_text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
let end = turn_text
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.take_while(|&i| i <= 4000)
|
||||
.last()
|
||||
.unwrap_or(0);
|
||||
format!("{}…", &turn_text[..end])
|
||||
} else {
|
||||
turn_text.clone()
|
||||
@ -99,10 +101,12 @@ fn parse_consolidation_response(raw: &str, fallback_text: &str) -> Consolidation
|
||||
// Fallback: use truncated turn text as history entry.
|
||||
// Use char-boundary-safe slicing to prevent panic on multi-byte UTF-8.
|
||||
let summary = if fallback_text.len() > 200 {
|
||||
let mut end = 200;
|
||||
while end > 0 && !fallback_text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
let end = fallback_text
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.take_while(|&i| i <= 200)
|
||||
.last()
|
||||
.unwrap_or(0);
|
||||
format!("{}…", &fallback_text[..end])
|
||||
} else {
|
||||
fallback_text.to_string()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user