diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index 9a21395..47d02a6 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -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() diff --git a/src/providers/openrouter.rs b/src/providers/openrouter.rs index 8e84524..2896c07 100644 --- a/src/providers/openrouter.rs +++ b/src/providers/openrouter.rs @@ -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()); + } }