fix(agent): preserve native tool-call fallbacks and history fidelity

This commit is contained in:
Chummy 2026-02-17 17:49:31 +08:00
parent f322360248
commit f75f73a50d
2 changed files with 112 additions and 66 deletions

View file

@ -489,77 +489,88 @@ pub(crate) async fn run_tool_call_loop(
let llm_started_at = Instant::now();
// Choose between native tool-call API and prompt-based tool use.
let (response_text, parsed_text, tool_calls, assistant_history_content) = if use_native_tools {
match provider
.chat_with_tools(history, &tool_definitions, model, temperature)
.await
{
Ok(resp) => {
observer.record_event(&ObserverEvent::LlmResponse {
provider: provider_name.to_string(),
model: model.to_string(),
duration: llm_started_at.elapsed(),
success: true,
error_message: None,
});
let response_text = resp.text_or_empty().to_string();
let mut calls = parse_structured_tool_calls(&resp.tool_calls);
let mut parsed_text = String::new();
let (response_text, parsed_text, tool_calls, assistant_history_content) =
if use_native_tools {
match provider
.chat_with_tools(history, &tool_definitions, model, temperature)
.await
{
Ok(resp) => {
observer.record_event(&ObserverEvent::LlmResponse {
provider: provider_name.to_string(),
model: model.to_string(),
duration: llm_started_at.elapsed(),
success: true,
error_message: None,
});
let response_text = resp.text_or_empty().to_string();
let mut calls = parse_structured_tool_calls(&resp.tool_calls);
let mut parsed_text = String::new();
if calls.is_empty() {
let (fallback_text, fallback_calls) = parse_tool_calls(&response_text);
if !fallback_text.is_empty() {
parsed_text = fallback_text;
if calls.is_empty() {
let (fallback_text, fallback_calls) = parse_tool_calls(&response_text);
if !fallback_text.is_empty() {
parsed_text = fallback_text;
}
calls = fallback_calls;
}
calls = fallback_calls;
let assistant_history_content = if resp.tool_calls.is_empty() {
response_text.clone()
} else {
build_assistant_history_with_tool_calls(
&response_text,
&resp.tool_calls,
)
};
(response_text, parsed_text, calls, assistant_history_content)
}
Err(e) => {
observer.record_event(&ObserverEvent::LlmResponse {
provider: provider_name.to_string(),
model: model.to_string(),
duration: llm_started_at.elapsed(),
success: false,
error_message: Some(crate::providers::sanitize_api_error(
&e.to_string(),
)),
});
return Err(e);
}
let assistant_history_content = if resp.tool_calls.is_empty() {
response_text.clone()
} else {
build_assistant_history_with_tool_calls(&response_text, &resp.tool_calls)
};
(response_text, parsed_text, calls, assistant_history_content)
}
Err(e) => {
observer.record_event(&ObserverEvent::LlmResponse {
provider: provider_name.to_string(),
model: model.to_string(),
duration: llm_started_at.elapsed(),
success: false,
error_message: Some(crate::providers::sanitize_api_error(&e.to_string())),
});
return Err(e);
} else {
match provider
.chat_with_history(history, model, temperature)
.await
{
Ok(resp) => {
observer.record_event(&ObserverEvent::LlmResponse {
provider: provider_name.to_string(),
model: model.to_string(),
duration: llm_started_at.elapsed(),
success: true,
error_message: None,
});
let response_text = resp;
let assistant_history_content = response_text.clone();
let (parsed_text, calls) = parse_tool_calls(&response_text);
(response_text, parsed_text, calls, assistant_history_content)
}
Err(e) => {
observer.record_event(&ObserverEvent::LlmResponse {
provider: provider_name.to_string(),
model: model.to_string(),
duration: llm_started_at.elapsed(),
success: false,
error_message: Some(crate::providers::sanitize_api_error(
&e.to_string(),
)),
});
return Err(e);
}
}
}
} else {
match provider.chat_with_history(history, model, temperature).await {
Ok(resp) => {
observer.record_event(&ObserverEvent::LlmResponse {
provider: provider_name.to_string(),
model: model.to_string(),
duration: llm_started_at.elapsed(),
success: true,
error_message: None,
});
let response_text = resp;
let assistant_history_content = response_text.clone();
let (parsed_text, calls) = parse_tool_calls(&response_text);
(response_text, parsed_text, calls, assistant_history_content)
}
Err(e) => {
observer.record_event(&ObserverEvent::LlmResponse {
provider: provider_name.to_string(),
model: model.to_string(),
duration: llm_started_at.elapsed(),
success: false,
error_message: Some(crate::providers::sanitize_api_error(&e.to_string())),
});
return Err(e);
}
}
};
};
let display_text = if parsed_text.is_empty() {
response_text.clone()

View file

@ -712,4 +712,39 @@ mod tests {
assert_eq!(response.tool_calls[0].id, "call_789");
assert_eq!(response.tool_calls[0].name, "file_read");
}
#[test]
fn convert_messages_parses_assistant_tool_call_payload() {
let messages = vec![ChatMessage {
role: "assistant".into(),
content: r#"{"content":"Using tool","tool_calls":[{"id":"call_abc","name":"shell","arguments":"{\"command\":\"pwd\"}"}]}"#
.into(),
}];
let converted = OpenRouterProvider::convert_messages(&messages);
assert_eq!(converted.len(), 1);
assert_eq!(converted[0].role, "assistant");
assert_eq!(converted[0].content.as_deref(), Some("Using tool"));
let tool_calls = converted[0].tool_calls.as_ref().unwrap();
assert_eq!(tool_calls.len(), 1);
assert_eq!(tool_calls[0].id.as_deref(), Some("call_abc"));
assert_eq!(tool_calls[0].function.name, "shell");
assert_eq!(tool_calls[0].function.arguments, r#"{"command":"pwd"}"#);
}
#[test]
fn convert_messages_parses_tool_result_payload() {
let messages = vec![ChatMessage {
role: "tool".into(),
content: r#"{"tool_call_id":"call_xyz","content":"done"}"#.into(),
}];
let converted = OpenRouterProvider::convert_messages(&messages);
assert_eq!(converted.len(), 1);
assert_eq!(converted[0].role, "tool");
assert_eq!(converted[0].tool_call_id.as_deref(), Some("call_xyz"));
assert_eq!(converted[0].content.as_deref(), Some("done"));
assert!(converted[0].tool_calls.is_none());
}
}