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:
parent
39b07311e6
commit
2a64d8e11f
2 changed files with 40 additions and 38 deletions
|
|
@ -319,22 +319,49 @@ impl SseReader {
|
|||
/// Build a response Message from accumulated content and tool calls.
|
||||
/// Shared by both backends — the wire format differs but the internal
|
||||
/// representation is the same.
|
||||
///
|
||||
/// If no structured tool calls came from the API but the content
|
||||
/// contains leaked tool call XML (e.g. `<tool_call>...</tool_call>`
|
||||
/// from models that emit tool calls as text), parse them out and
|
||||
/// promote them to structured tool_calls. This way all consumers
|
||||
/// see tool calls uniformly regardless of backend.
|
||||
pub(crate) fn build_response_message(
|
||||
content: String,
|
||||
tool_calls: Vec<ToolCall>,
|
||||
) -> Message {
|
||||
// If the API returned structured tool calls, use them as-is.
|
||||
if !tool_calls.is_empty() {
|
||||
return Message {
|
||||
role: Role::Assistant,
|
||||
content: if content.is_empty() { None }
|
||||
else { Some(MessageContent::Text(content)) },
|
||||
tool_calls: Some(tool_calls),
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
timestamp: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for leaked tool calls in content text.
|
||||
let leaked = crate::agent::parsing::parse_leaked_tool_calls(&content);
|
||||
if !leaked.is_empty() {
|
||||
let cleaned = crate::agent::parsing::strip_leaked_artifacts(&content);
|
||||
return Message {
|
||||
role: Role::Assistant,
|
||||
content: if cleaned.trim().is_empty() { None }
|
||||
else { Some(MessageContent::Text(cleaned)) },
|
||||
tool_calls: Some(leaked),
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
timestamp: None,
|
||||
};
|
||||
}
|
||||
|
||||
Message {
|
||||
role: Role::Assistant,
|
||||
content: if content.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(MessageContent::Text(content))
|
||||
},
|
||||
tool_calls: if tool_calls.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(tool_calls)
|
||||
},
|
||||
content: if content.is_empty() { None }
|
||||
else { Some(MessageContent::Text(content)) },
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
timestamp: None,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue