Skip to content

fix: make tool-use nudge conditional on detected tool intent#1

Open
sangwa wants to merge 3 commits into
mainfrom
fix/conversational-messages
Open

fix: make tool-use nudge conditional on detected tool intent#1
sangwa wants to merge 3 commits into
mainfrom
fix/conversational-messages

Conversation

@sangwa
Copy link
Copy Markdown

@sangwa sangwa commented Feb 19, 2026

The agentic loop previously nudged the LLM to use tools on every text response, making casual conversation impossible ("hi, how are you" would trigger up to 3 retry iterations with "please use tools" injected).

  • Add llm_mentions_tool_intent() heuristic that checks whether the LLM's text response mentions an intent phrase ("I'll use", "let me call", etc.) near a known tool name within an 80-char window
  • Only nudge when the heuristic fires, letting plain conversational responses pass through immediately
  • Soften the nudge prompt to give the LLM an exit if no tool was needed
  • Save the pre-nudge response and fall back to it if the post-nudge response is also text (false-positive recovery)

Summary by CodeRabbit

  • Bug Fixes

    • Detects when the AI mentions intent to use a tool without calling it and performs a one-time nudge instead of looping.
    • Restores the original AI response if a nudge was a false positive; preserves tool results when tools are actually executed.
  • Tests

    • Added tests covering detection of textual tool-intent, case and distance edge cases, and related behaviors.

The agentic loop previously nudged the LLM to use tools on every text
response, making casual conversation impossible ("hi, how are you"
would trigger up to 3 retry iterations with "please use tools" injected).

- Add `llm_mentions_tool_intent()` heuristic that checks whether the
  LLM's text response mentions an intent phrase ("I'll use", "let me
  call", etc.) near a known tool name within an 80-char window
- Only nudge when the heuristic fires, letting plain conversational
  responses pass through immediately
- Soften the nudge prompt to give the LLM an exit if no tool was needed
- Save the pre-nudge response and fall back to it if the post-nudge
  response is also text (false-positive recovery)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sangwa sangwa requested a review from joeldrotleff February 19, 2026 18:49
@sangwa sangwa self-assigned this Feb 19, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 19, 2026

No actionable comments were generated in the recent review. 🎉


Walkthrough

The agent loop now detects textual intent to use tools and performs a single nudge when the LLM mentions tool usage but didn’t call any tools. It collects tool names before adding tool definitions to the LLM context, and saves the original LLM text in pre_nudge_response before nudging so the original can be returned if the nudge was a false positive. If tools run after a nudge, the saved pre_nudge_response is discarded and the final response reflects tool results.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant LLM as LLM
    participant Agent as Agent Loop
    participant Detector as Tool Intent Detector
    participant Tools as Tool Executor

    User->>LLM: Sends prompt
    LLM-->>Agent: Returns response text
    Agent->>Agent: Check pre_nudge_response
    alt pre_nudge_response exists
        Agent-->>User: Return saved original response
    else no pre_nudge_response
        Agent->>Detector: Analyze response for intent (llm_mentions_tool_intent)
        Detector-->>Agent: Intent detected? (bool)
        Agent->>Agent: Check if any tools executed
        alt No tools executed & intent detected
            Agent->>Agent: Save response as pre_nudge_response
            Agent->>LLM: Send nudged prompt (one-time)
            LLM-->>Agent: Return post-nudge response
            Agent->>Tools: Execute tools if called
            Tools-->>Agent: Return tool results
            Agent->>Agent: Discard pre_nudge_response if tools ran
            Agent-->>User: Return final response (with tool results)
        else Tools executed or no intent
            Agent-->>User: Return current response
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit nudged the LLM with care,
Kept the first reply tucked in its lair,
One gentle prod to call a tool true,
If nothing ran, the old answer came through—
A hop, a smile, the loop stays fair. 🐇

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: make tool-use nudge conditional on detected tool intent' directly and clearly describes the main change—making the tool nudging behavior conditional on detecting actual tool intent rather than applying it unconditionally.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/conversational-messages

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/agent/agent_loop.rs`:
- Around line 1115-1126: The current RespondResult::Text branch unconditionally
returns pre_nudge_response when present, which discards cases where the nudge
led to actual tool execution; change the logic inside the RespondResult::Text
match arm to only fall back to pre_nudge_response.take() when no tools were
executed (e.g., check a field like output.tool_calls.is_empty() or
output.tools_used == 0 or call a method like output.was_tool_called()),
otherwise let the tool-augmented result continue processing; update the
condition that wraps pre_nudge_response.take() to include this "no tools
executed" check while keeping the debug message and returning
AgenticLoopResult::Response(original) only in that case.

In `@src/util.rs`:
- Around line 71-76: The slice for the intent-matching window can panic on UTF-8
boundaries because window_end is computed as a byte index; update the
calculation to use floor_char_boundary to ensure window_end is a valid char
boundary before slicing. Locate the loop over INTENT_PHRASES where
lower.find(phrase) is used and modify the computation of window_end (and any use
of window_start/window_end for slicing) to call
lower.floor_char_boundary((window_start + 80).min(lower.len())) so the resulting
&lower[window_start..window_end] slice is safe.

Comment thread src/agent/agent_loop.rs
…UTF-8 slicing

When the tool-use nudge led to actual tool calls, pre_nudge_response was
not cleared, causing the final tool-augmented text to be discarded in
favor of the stale original. Clear the stash in the ToolCalls branch.

Also use floor_char_boundary in llm_mentions_tool_intent to prevent a
panic when the 80-byte search window lands mid-character in multi-byte
UTF-8 text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/util.rs`:
- Around line 71-76: The code incorrectly calls floor_char_boundary as a method
on lower; change the call to the standalone function by invoking
floor_char_boundary(&lower, (window_start + 80).min(lower.len())) (or whatever
the helper signature requires) instead of lower.floor_char_boundary(...), so in
the loop with INTENT_PHRASES use window_end = floor_char_boundary(&lower,
(window_start + 80).min(lower.len())); to get a valid char boundary.

Comment thread src/util.rs
…consistency

Replace std method call `lower.floor_char_boundary(...)` with the
standalone `floor_char_boundary()` polyfill defined in the same module,
maintaining backward compatibility and consistency across the codebase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant