diff --git a/crates/openfang-runtime/src/agent_loop.rs b/crates/openfang-runtime/src/agent_loop.rs index c377584fd..17bad5375 100644 --- a/crates/openfang-runtime/src/agent_loop.rs +++ b/crates/openfang-runtime/src/agent_loop.rs @@ -628,7 +628,7 @@ pub async fn run_agent_loop( // Execute each tool call with loop guard, timeout, and truncation let mut tool_result_blocks = Vec::new(); - for tool_call in &response.tool_calls { + for tool_call in deduplicate_tool_calls(&response) { // Loop guard check let verdict = loop_guard.check(&tool_call.name, &tool_call.input); match &verdict { @@ -1628,7 +1628,7 @@ pub async fn run_agent_loop_streaming( // Execute each tool call with loop guard, timeout, and truncation let mut tool_result_blocks = Vec::new(); - for tool_call in &response.tool_calls { + for tool_call in deduplicate_tool_calls(&response) { // Loop guard check let verdict = loop_guard.check(&tool_call.name, &tool_call.input); match &verdict { @@ -2732,6 +2732,20 @@ fn try_parse_bare_json_tool_call( parse_json_tool_call_object(&text[..end], tool_names) } +/// Deduplicate tool calls from the response. +/// Returns a reference to the deduplicated tool calls. +pub fn deduplicate_tool_calls(response: &crate::llm_driver::CompletionResponse) -> Vec<&ToolCall> { + let mut hash_set = std::collections::HashSet::new(); + let mut deduplicated = Vec::new(); + for tool_call in &response.tool_calls { + let hash = LoopGuard::compute_hash(&tool_call.name, &tool_call.input); + if hash_set.insert(hash) { + deduplicated.push(tool_call); + } + } + deduplicated +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openfang-runtime/src/loop_guard.rs b/crates/openfang-runtime/src/loop_guard.rs index 58dce015b..74e73d95c 100644 --- a/crates/openfang-runtime/src/loop_guard.rs +++ b/crates/openfang-runtime/src/loop_guard.rs @@ -498,7 +498,7 @@ impl LoopGuard { } /// Compute a SHA-256 hash of the tool name and parameters. - fn compute_hash(tool_name: &str, params: &serde_json::Value) -> String { + pub fn compute_hash(tool_name: &str, params: &serde_json::Value) -> String { let mut hasher = Sha256::new(); hasher.update(tool_name.as_bytes()); hasher.update(b"|");