move leaked tool call recovery into build_response_message

Tool call parsing was only in runner.rs, so subconscious agents
(poc-memory agent run) never recovered leaked tool calls from
models that emit <tool_call> as content text (e.g. Qwen via Crane).

Move the recovery into build_response_message where both code paths
share it. Leaked tool calls are promoted to structured tool_calls
and the content is cleaned, so all consumers see them uniformly.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-03-29 20:57:59 -04:00
parent 39b07311e6
commit 2a64d8e11f
2 changed files with 40 additions and 38 deletions

View file

@ -329,7 +329,8 @@ impl Agent {
empty_retries = 0;
}
// Structured tool calls from the API
// Tool calls (structured from API, or recovered from content
// by build_response_message if the model leaked them as XML).
if let Some(ref tool_calls) = msg.tool_calls {
if !tool_calls.is_empty() {
self.push_message(msg.clone());
@ -341,34 +342,8 @@ impl Agent {
}
}
// No structured tool calls — check for leaked tool calls
// (Qwen sometimes outputs <tool_call> XML as text).
let text = msg.content_text().to_string();
let leaked = crate::agent::parsing::parse_leaked_tool_calls(&text);
if !leaked.is_empty() {
let _ = ui_tx.send(UiMessage::Debug(format!(
"recovered {} leaked tool call(s) from text",
leaked.len()
)));
// Strip tool call XML and thinking tokens from the message
// so they don't clutter the conversation history.
let cleaned = crate::agent::parsing::strip_leaked_artifacts(&text);
let mut clean_msg = msg.clone();
clean_msg.content = if cleaned.trim().is_empty() {
None
} else {
Some(MessageContent::Text(cleaned))
};
self.push_message(clean_msg);
for call in &leaked {
self.dispatch_tool_call(call, Some("recovered"), ui_tx, &mut ds)
.await;
}
continue;
}
// Genuinely text-only response
let text = msg.content_text().to_string();
let _ = ui_tx.send(UiMessage::Activity(String::new()));
self.push_message(msg);