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:
Argenis 2026-03-16 18:38:44 -04:00 committed by GitHub
parent 013fca6ad2
commit c3a3cfc9a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 19 additions and 22 deletions

View File

@ -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
}

View File

@ -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]

View File

@ -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()