Repository navigation
fix(worker): prevent orphaned tool_results and fix parallel merging - #1069
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request refines the handling of LLM tool interactions by addressing two critical bugs. It ensures that the LLM's reasoning is consistently preserved across turns and correctly merges parallel tool execution results to maintain compatibility with external APIs. The changes are a focused re-implementation of core fixes from a previous, broader pull request, emphasizing stability and correctness. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces two important fixes for tool handling: merging consecutive tool results to support APIs like Anthropic, and preserving the LLM's reasoning text when tool calls are made. The changes are well-implemented and include corresponding regression tests. I've identified a potential issue where reasoning context could be lost if the first of several parallel tool selections has empty reasoning, and I've provided a suggestion to make this more robust. Additionally, tests should be updated to reflect this improved logic and ensure comprehensive coverage, aligning with best practices for test maintenance.
| let reasoning_text = s | ||
| .first() | ||
| .map(|sel| sel.reasoning.clone()) | ||
| .filter(|r| !r.is_empty()); |
There was a problem hiding this comment.
The current implementation only considers the reasoning from the first ToolSelection. If the first selection has empty reasoning but a subsequent one has non-empty reasoning, this context will be lost. To make this more robust, you should iterate through all selections and use the first non-empty reasoning string found.
| let reasoning_text = s | |
| .first() | |
| .map(|sel| sel.reasoning.clone()) | |
| .filter(|r| !r.is_empty()); | |
| let reasoning_text = s | |
| .iter() | |
| .find_map(|sel| (!sel.reasoning.is_empty()).then_some(sel.reasoning.clone())); |
| fn test_reasoning_text_extraction_from_selections() { | ||
| // Simulate what call_llm does: extract reasoning from first selection | ||
| let selections = [ | ||
| ToolSelection { | ||
| tool_name: "search".into(), | ||
| parameters: serde_json::json!({}), | ||
| reasoning: "I need to search for relevant information".into(), | ||
| alternatives: vec![], | ||
| tool_call_id: "call_1".into(), | ||
| }, | ||
| ToolSelection { | ||
| tool_name: "fetch".into(), | ||
| parameters: serde_json::json!({}), | ||
| reasoning: "I need to search for relevant information".into(), | ||
| alternatives: vec![], | ||
| tool_call_id: "call_2".into(), | ||
| }, | ||
| ]; | ||
|
|
||
| let reasoning_text = selections | ||
| .first() | ||
| .map(|sel| sel.reasoning.clone()) | ||
| .filter(|r| !r.is_empty()); | ||
|
|
||
| assert_eq!( | ||
| reasoning_text.as_deref(), | ||
| Some("I need to search for relevant information"), | ||
| "Reasoning text should be extracted from first selection" | ||
| ); | ||
|
|
||
| // Empty reasoning should result in None | ||
| let empty_selections = [ToolSelection { | ||
| tool_name: "echo".into(), | ||
| parameters: serde_json::json!({}), | ||
| reasoning: String::new(), | ||
| alternatives: vec![], | ||
| tool_call_id: "call_3".into(), | ||
| }]; | ||
|
|
||
| let empty_reasoning = empty_selections | ||
| .first() | ||
| .map(|sel| sel.reasoning.clone()) | ||
| .filter(|r| !r.is_empty()); | ||
|
|
||
| assert!( | ||
| empty_reasoning.is_none(), | ||
| "Empty reasoning should not be included as content" | ||
| ); | ||
| } |
There was a problem hiding this comment.
This test currently simulates the logic of only taking reasoning from the first selection. To align with the suggested improvement of finding the first non-empty reasoning from any selection, this test should be updated.
Specifically:
- Update the logic inside the test to use
find_mapas suggested for the implementation. - Add a new test case to verify that reasoning is correctly extracted when the first selection has empty reasoning but a subsequent one has non-empty reasoning.
Example of the new test case:
let selections_with_first_empty = [
ToolSelection {
tool_name: "search".into(),
parameters: serde_json::json!({}),
reasoning: "".into(),
alternatives: vec![],
tool_call_id: "call_1".into(),
},
ToolSelection {
tool_name: "fetch".into(),
parameters: serde_json::json!({}),
reasoning: "This reasoning should be picked up".into(),
alternatives: vec![],
tool_call_id: "call_2".into(),
},
];
let reasoning_text = selections_with_first_empty
.iter()
.find_map(|sel| (!sel.reasoning.is_empty()).then_some(sel.reasoning.clone()));
assert_eq!(
reasoning_text.as_deref(),
Some("This reasoning should be picked up")
);References
- When refactoring, ensure that tests for generic functionality are updated to cover the new data structures, preventing loss of test coverage.
Two fixes for tool result handling in the Worker: 1. Preserve reasoning text from select_tools() in the RespondResult content field so it appears in the assistant_with_tool_calls message pushed by execute_tool_calls. Without this, the LLM's reasoning context was lost when using the select_tools path. 2. Merge consecutive tool_result messages into a single User message in rig_adapter's convert_messages(). When parallel tools execute, each produces a separate ChatMessage with role: Tool. Without merging, these become consecutive User messages which Anthropic rejects. Now consecutive tool results are merged into one User message with multiple ToolResult content items. Includes regression tests for both fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
c27245f to
c0c95ec
Compare
The previous code only checked the first ToolSelection's reasoning, missing cases where the first selection has empty reasoning but subsequent ones do not. Switch to find_map to get the first non-empty reasoning across all selections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…earai#1069) * fix(worker): prevent orphaned tool_results and fix parallel merging Two fixes for tool result handling in the Worker: 1. Preserve reasoning text from select_tools() in the RespondResult content field so it appears in the assistant_with_tool_calls message pushed by execute_tool_calls. Without this, the LLM's reasoning context was lost when using the select_tools path. 2. Merge consecutive tool_result messages into a single User message in rig_adapter's convert_messages(). When parallel tools execute, each produces a separate ChatMessage with role: Tool. Without merging, these become consecutive User messages which Anthropic rejects. Now consecutive tool results are merged into one User message with multiple ToolResult content items. Includes regression tests for both fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(worker): use find_map for first non-empty reasoning extraction The previous code only checked the first ToolSelection's reasoning, missing cases where the first selection has empty reasoning but subsequent ones do not. Switch to find_map to get the first non-empty reasoning across all selections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…earai#1069) * fix(worker): prevent orphaned tool_results and fix parallel merging Two fixes for tool result handling in the Worker: 1. Preserve reasoning text from select_tools() in the RespondResult content field so it appears in the assistant_with_tool_calls message pushed by execute_tool_calls. Without this, the LLM's reasoning context was lost when using the select_tools path. 2. Merge consecutive tool_result messages into a single User message in rig_adapter's convert_messages(). When parallel tools execute, each produces a separate ChatMessage with role: Tool. Without merging, these become consecutive User messages which Anthropic rejects. Now consecutive tool results are merged into one User message with multiple ToolResult content items. Includes regression tests for both fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(worker): use find_map for first non-empty reasoning extraction The previous code only checked the first ToolSelection's reasoning, missing cases where the first selection has empty reasoning but subsequent ones do not. Switch to find_map to get the first non-empty reasoning across all selections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Clean reimplementation of the core fixes from #635, scoped to only the two bugs identified in the PR title. The Telegram, scheduler, state machine, and empty-response changes from #635 are excluded per maintainer feedback about PR scope.
Reasoning text preserved:
JobDelegate::call_llm()now extracts the reasoning text fromselect_tools()results and passes it ascontentinRespondResult::ToolCalls. This ensures theassistant_with_tool_callsmessage includes the LLM's reasoning, preventing context loss on subsequent turns.Parallel tool results merged:
rig_adapter::convert_messages()now merges consecutiveRole::Toolmessages into a singleRigMessage::Userwith multipleToolResultcontent items. Without this, parallel tool execution produced consecutive User messages that Anthropic's API rejects.Addresses review feedback from #635:
.unwrap()or.expect()in production codeSupersedes #635 (core bug fixes only).
Test plan
cargo checkpassescargo test --lib worker-- all 47 tests pass including new regression testscargo test --lib rig_adapter-- all 29 tests pass including new regression testscargo clippy --all --tests-- zero warningsGenerated with Claude Code