mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-03 01:59:23 +00:00
fix(llm): nudge LLM when it expresses tool intent without calling tools (#653)
* fix(llm): nudge LLM when it expresses tool intent without calling tools
Non-Anthropic models (especially GLM-5 via NEAR AI) frequently output
text like "Let me search for X" without including tool_calls, creating
a frustrating loop where the user waits but nothing happens.
Add llm_signals_tool_intent() detection that matches intent phrases
("let me search", "I'll fetch") while excluding conversational phrases
("let me explain", "let me know") and content inside code blocks.
When detected, inject a nudge message telling the model to actually
call the tool. Cap at 2 consecutive nudges to avoid infinite loops.
Applied to all three agentic loops: dispatcher (interactive chat),
agent/worker (background jobs), and worker/runtime (sandbox containers).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(nudge): address PR #653 review comments
1. Update doc comment to match implementation (code blocks only, not quotes)
2. Use match_indices() instead of find() to check all prefix occurrences
3. Add !available_tools.is_empty() guard in dispatcher nudge check
4. Reset consecutive_tool_intent_nudges on non-intent text responses
5. Add regression test for shadowed prefix detection
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(nudge): address second round of PR #653 review comments
1. Strip double-quoted strings in tool-intent detection to avoid false
positives on quoted prose like `"Let me search the database"`.
2. Only reset consecutive_tool_intent_nudges when text does NOT signal
intent — preserves the 2-nudge cap when intent is detected but cap
is already reached.
3. Fix assertion message in nudge_cap test to report correct call index.
4. Add regression test for quoted strings outside code blocks.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
3f22f4321d
commit
8fbb782090
@@ -138,6 +138,8 @@ impl Agent {
|
||||
let force_text_at = max_tool_iterations;
|
||||
let nudge_at = max_tool_iterations.saturating_sub(1);
|
||||
let mut iteration = 0;
|
||||
const MAX_TOOL_INTENT_NUDGES: u32 = 2;
|
||||
let mut consecutive_tool_intent_nudges: u32 = 0;
|
||||
loop {
|
||||
iteration += 1;
|
||||
// Hard ceiling one past the forced-text iteration (should never be reached
|
||||
@@ -294,6 +296,24 @@ impl Agent {
|
||||
|
||||
match output.result {
|
||||
RespondResult::Text(text) => {
|
||||
// Nudge the LLM if it expressed tool intent without calling tools.
|
||||
// This is common with non-Anthropic models (e.g. GLM-5 via NEAR AI)
|
||||
// that output "Let me search…" but don't issue tool_calls.
|
||||
if !force_text
|
||||
&& !context.available_tools.is_empty()
|
||||
&& consecutive_tool_intent_nudges < MAX_TOOL_INTENT_NUDGES
|
||||
&& crate::llm::llm_signals_tool_intent(&text)
|
||||
{
|
||||
consecutive_tool_intent_nudges += 1;
|
||||
tracing::info!(
|
||||
iteration,
|
||||
"LLM expressed tool intent without calling a tool, nudging"
|
||||
);
|
||||
context_messages.push(ChatMessage::assistant(&text));
|
||||
context_messages.push(ChatMessage::user(crate::llm::TOOL_INTENT_NUDGE));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip internal "[Called tool ...]" text that can leak when
|
||||
// provider flattening (e.g. NEAR AI) converts tool_calls to
|
||||
// plain text and the LLM echoes it back.
|
||||
@@ -304,6 +324,7 @@ impl Agent {
|
||||
tool_calls,
|
||||
content,
|
||||
} => {
|
||||
consecutive_tool_intent_nudges = 0;
|
||||
// Add the assistant message with tool_calls to context.
|
||||
// OpenAI protocol requires this before tool-result messages.
|
||||
context_messages.push(ChatMessage::assistant_with_tool_calls(
|
||||
|
||||
+25
-5
@@ -305,6 +305,8 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
let mut iteration = 0;
|
||||
const MAX_CONSECUTIVE_RATE_LIMITS: usize = 10;
|
||||
let mut consecutive_rate_limits = 0usize;
|
||||
const MAX_TOOL_INTENT_NUDGES: u32 = 2;
|
||||
let mut consecutive_tool_intent_nudges: u32 = 0;
|
||||
|
||||
// Initial tool definitions for planning (will be refreshed in loop)
|
||||
reason_ctx.available_tools = self.tools().tool_definitions().await;
|
||||
@@ -502,17 +504,34 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
}),
|
||||
);
|
||||
|
||||
// Give it one more chance to select a tool
|
||||
if iteration > 3 && iteration % 5 == 0 {
|
||||
reason_ctx.messages.push(ChatMessage::user(
|
||||
"Are you stuck? Do you need help completing this job?",
|
||||
));
|
||||
// Nudge the LLM if it expressed tool intent without calling tools
|
||||
let signals_intent = !reason_ctx.available_tools.is_empty()
|
||||
&& crate::llm::llm_signals_tool_intent(&response);
|
||||
if signals_intent && consecutive_tool_intent_nudges < MAX_TOOL_INTENT_NUDGES
|
||||
{
|
||||
consecutive_tool_intent_nudges += 1;
|
||||
tracing::info!(
|
||||
job_id = %self.job_id,
|
||||
"LLM expressed tool intent without calling a tool, nudging"
|
||||
);
|
||||
reason_ctx
|
||||
.messages
|
||||
.push(ChatMessage::user(crate::llm::TOOL_INTENT_NUDGE));
|
||||
} else if !signals_intent {
|
||||
consecutive_tool_intent_nudges = 0;
|
||||
if iteration > 3 && iteration % 5 == 0 {
|
||||
// Generic fallback nudge
|
||||
reason_ctx.messages.push(ChatMessage::user(
|
||||
"Are you stuck? Do you need help completing this job?",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
RespondResult::ToolCalls {
|
||||
tool_calls,
|
||||
content,
|
||||
} => {
|
||||
consecutive_tool_intent_nudges = 0;
|
||||
// Model returned tool calls - execute them
|
||||
tracing::debug!(
|
||||
"Job {} respond_with_tools returned {} tool calls",
|
||||
@@ -558,6 +577,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
}
|
||||
}
|
||||
} else if selections.len() == 1 {
|
||||
consecutive_tool_intent_nudges = 0;
|
||||
// Single tool: execute directly
|
||||
let selection = &selections[0];
|
||||
tracing::debug!(
|
||||
|
||||
Reference in New Issue
Block a user