From eaef335db6ce9830b9bb49898301f2d89296c30c Mon Sep 17 00:00:00 2001 From: AI-Reviewer-QS Date: Sun, 15 Feb 2026 05:39:25 +0800 Subject: [PATCH] fix: propagate real tool_call_id instead of hardcoded placeholder (#73) The worker (both agent/worker.rs and worker/runtime.rs) was passing the literal string "tool_call_id" to ChatMessage::tool_result instead of the actual tool call ID from the LLM response. This breaks OpenAI-compatible providers that match tool results to their corresponding calls by ID. - Add tool_call_id field to ToolSelection struct - Propagate ToolCall.id through select_tools() into ToolSelection - Replace all hardcoded "tool_call_id" usages with selection.tool_call_id - Generate unique IDs for plan-based synthetic selections - Add test verifying tool_call_id is preserved Co-authored-by: Yi LIU Co-authored-by: Illia Polosukhin --- src/agent/worker.rs | 28 +++++++++++++++++++++++++--- src/llm/reasoning.rs | 7 +++++++ src/worker/runtime.rs | 5 +++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/agent/worker.rs b/src/agent/worker.rs index 39c5cd8d..565a3d22 100644 --- a/src/agent/worker.rs +++ b/src/agent/worker.rs @@ -299,6 +299,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."# parameters: tc.arguments.clone(), reasoning: String::new(), alternatives: vec![], + tool_call_id: tc.id.clone(), }; self.process_tool_result(reason_ctx, &selection, result) @@ -565,7 +566,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."# ); reason_ctx.messages.push(ChatMessage::tool_result( - "tool_call_id", + &selection.tool_call_id, &selection.tool_name, wrapped, )); @@ -597,7 +598,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."# } reason_ctx.messages.push(ChatMessage::tool_result( - "tool_call_id", + &selection.tool_call_id, &selection.tool_name, format!("Error: {}", e), )); @@ -647,12 +648,15 @@ Report when the job is complete or if you encounter issues you cannot resolve."# .execute_tool(&action.tool_name, &action.parameters) .await; - // Create a synthetic ToolSelection for process_tool_result + // Create a synthetic ToolSelection for process_tool_result. + // Plan actions don't originate from an LLM tool_call response so + // there is no real tool_call_id; generate a unique one. let selection = ToolSelection { tool_name: action.tool_name.clone(), parameters: action.parameters.clone(), reasoning: action.reasoning.clone(), alternatives: vec![], + tool_call_id: format!("plan_{}_{}", self.job_id, i), }; // Process the result @@ -774,8 +778,26 @@ impl From for Result { #[cfg(test)] mod tests { + use crate::llm::ToolSelection; use crate::util::llm_signals_completion; + #[test] + fn test_tool_selection_preserves_call_id() { + let selection = ToolSelection { + tool_name: "memory_search".to_string(), + parameters: serde_json::json!({"query": "test"}), + reasoning: "Need to search memory".to_string(), + alternatives: vec![], + tool_call_id: "call_abc123".to_string(), + }; + + assert_eq!(selection.tool_call_id, "call_abc123"); + assert_ne!( + selection.tool_call_id, "tool_call_id", + "tool_call_id must not be the hardcoded placeholder string" + ); + } + #[test] fn test_completion_positive_signals() { assert!(llm_signals_completion("The job is complete.")); diff --git a/src/llm/reasoning.rs b/src/llm/reasoning.rs index 41d4db1c..461aa469 100644 --- a/src/llm/reasoning.rs +++ b/src/llm/reasoning.rs @@ -113,6 +113,12 @@ pub struct ToolSelection { pub reasoning: String, /// Alternative tools considered. pub alternatives: Vec, + /// The tool call ID from the LLM response. + /// + /// OpenAI-compatible providers assign each tool call a unique ID that must + /// be echoed back in the corresponding tool result message. Without this, + /// the provider cannot match results to their originating calls. + pub tool_call_id: String, } /// Token usage from a single LLM call. @@ -244,6 +250,7 @@ impl Reasoning { parameters: tool_call.arguments, reasoning: reasoning.clone(), alternatives: vec![], + tool_call_id: tool_call.id, }) .collect(); diff --git a/src/worker/runtime.rs b/src/worker/runtime.rs index 71f11177..a1da32f2 100644 --- a/src/worker/runtime.rs +++ b/src/worker/runtime.rs @@ -313,6 +313,7 @@ Work independently to complete this job. Report when done."#, parameters: tc.arguments.clone(), reasoning: String::new(), alternatives: vec![], + tool_call_id: tc.id.clone(), }; self.process_result(reason_ctx, &selection, result); } @@ -422,7 +423,7 @@ Work independently to complete this job. Report when done."#, ); reason_ctx.messages.push(ChatMessage::tool_result( - "tool_call_id", + &selection.tool_call_id, &selection.tool_name, wrapped, )); @@ -436,7 +437,7 @@ Work independently to complete this job. Report when done."#, Err(e) => { tracing::warn!("Tool {} failed: {}", selection.tool_name, e); reason_ctx.messages.push(ChatMessage::tool_result( - "tool_call_id", + &selection.tool_call_id, &selection.tool_name, format!("Error: {}", e), ));