From 3d9be7dfdb3cd83741d841a5aa1a1c190779993c Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Thu, 21 May 2026 17:45:00 +0200 Subject: [PATCH] Simplify tool call nudging and refine action detection Replace the 3-attempt retry loop with a single nudge when the model explicitly mentioned taking action. Remove misleading nudges for models that never expressed intent, which wasted round-trips and confused responses. Also streamline the userWantsVaultOps phrase list to remove weak triggers like "ok", "yes", and "please", and add fallback content for empty responses. --- main.js | 56 +++++++++++--------------------------- src/chat-view.ts | 71 ++++++++++++++---------------------------------- 2 files changed, 37 insertions(+), 90 deletions(-) diff --git a/main.js b/main.js index e2895b1..a8c7b46 100644 --- a/main.js +++ b/main.js @@ -11837,7 +11837,7 @@ ${writePreviews.map((a) => `- ${a.description}`).join("\n")}`, ); } else { this.updateMessageById(assistantMessageId, { - content: followUpContent || fullResponse, + content: followUpContent || fullResponse || "(No response)", isStreaming: false, isThinking: false }); @@ -12182,39 +12182,29 @@ ${actualMessage}` : actualMessage; if (isToolCapable && toolCalls.length === 0) { if (modelMentionedActions || userWantsVaultOps) { shouldFallbackToReadTools = true; + const priorResponse = fullResponse; fullResponse = ""; this.updateMessageById(assistantMessageId, { content: "", isStreaming: false, isThinking: false }); - let attempts = 0; - const maxAttempts = 3; - let currentMessages = [...messagesWithMemory]; - let currentResponse = ""; - while (attempts < maxAttempts && toolCalls.length === 0) { - attempts++; - this.showActivityIndicator( - attempts === 1 ? "Thinking\u2026" : `Retrying (${attempts}/${maxAttempts})\u2026` - ); + if (modelMentionedActions) { + this.showActivityIndicator("Thinking\u2026"); const nudgeMessages = [ - ...currentMessages, - ...currentResponse.trim() ? [{ role: "assistant", content: currentResponse }] : [], + ...messagesWithMemory, + ...priorResponse.trim() ? [{ role: "assistant", content: priorResponse }] : [], { role: "user", - content: attempts === 1 ? "You indicated you would take action but did not emit any tool_calls. Emit the required tool_calls now. Do not output explanatory text." : "You still have not emitted any tool_calls. Remember: when you need vault information, you MUST call tools immediately. Emit the tool_calls now. No text." + content: "You indicated you would take action but did not emit any tool_calls. Emit the required tool_calls now. Do not output explanatory text." } ]; try { const nudgeStream = activeClient.streamChat(nudgeMessages, tools); - currentResponse = ""; + let nudgeResponse = ""; for await (const chunk of nudgeStream) { - if (chunk.content) { - currentResponse += chunk.content; - } - if (chunk.tool_calls) { - toolCalls = [...toolCalls, ...chunk.tool_calls]; - } + if (chunk.content) nudgeResponse += chunk.content; + if (chunk.tool_calls) toolCalls = [...toolCalls, ...chunk.tool_calls]; } if (toolCalls.length > 0) { this.showActivityIndicator("Using tools\u2026"); @@ -12222,27 +12212,23 @@ ${actualMessage}` : actualMessage; toolCalls, nudgeMessages, tools, - currentResponse, + nudgeResponse, assistantMessageId ); - break; } } catch { - break; } - currentMessages = nudgeMessages; } if (toolCalls.length === 0) { const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools); if (autoToolCalls.length > 0) { this.showActivityIndicator("Using tools\u2026"); toolCalls = autoToolCalls; - fullResponse = ""; await this.processToolCalls( autoToolCalls, messagesWithMemory, tools, - fullResponse, + "", assistantMessageId ); } @@ -12361,25 +12347,15 @@ ${actualMessage}` : actualMessage; "files", "continue", "go ahead", - "start", + "proceed", "do it", "execute", - "run", - "proceed", - "next", - "now", - "yes", - "ok", - "okay", - "sure", - "please", - "step", + "implement", + "apply", "merge", "clean", "fix", - "update", - "implement", - "apply" + "update" ]; return operationPhrases.some((phrase) => lower.includes(phrase)); } diff --git a/src/chat-view.ts b/src/chat-view.ts index a8fb000..0df601f 100755 --- a/src/chat-view.ts +++ b/src/chat-view.ts @@ -1092,7 +1092,7 @@ export class ChatView extends ItemView { ); } else { this.updateMessageById(assistantMessageId, { - content: followUpContent || fullResponse, + content: followUpContent || fullResponse || '(No response)', isStreaming: false, isThinking: false, }); @@ -1520,6 +1520,7 @@ export class ChatView extends ItemView { shouldFallbackToReadTools = true; // Suppress the model's "Let me..." text — clear it from the DOM immediately + const priorResponse = fullResponse; fullResponse = ''; this.updateMessageById(assistantMessageId, { content: '', @@ -1527,43 +1528,27 @@ export class ChatView extends ItemView { isThinking: false, }); - // Try aggressive retry loop first (hidden from user) - let attempts = 0; - const maxAttempts = 3; - let currentMessages: OllamaMessage[] = [...messagesWithMemory]; - let currentResponse = ''; - - while (attempts < maxAttempts && toolCalls.length === 0) { - attempts++; - this.showActivityIndicator( - attempts === 1 ? 'Thinking…' : `Retrying (${attempts}/${maxAttempts})…` - ); - - // Build nudge messages — skip empty assistant content to avoid API issues + // Single nudge — only when the model actually expressed intent to act. + // Lying to models that never mentioned action ("you said you would...") confuses + // them and wastes a full LLM round-trip without benefit. + if (modelMentionedActions) { + this.showActivityIndicator('Thinking…'); const nudgeMessages: OllamaMessage[] = [ - ...currentMessages, - ...(currentResponse.trim() - ? [{ role: 'assistant' as const, content: currentResponse }] - : []), + ...messagesWithMemory, + ...(priorResponse.trim() ? [{ role: 'assistant' as const, content: priorResponse }] : []), { role: 'user', content: - attempts === 1 - ? 'You indicated you would take action but did not emit any tool_calls. Emit the required tool_calls now. Do not output explanatory text.' - : 'You still have not emitted any tool_calls. Remember: when you need vault information, you MUST call tools immediately. Emit the tool_calls now. No text.', + 'You indicated you would take action but did not emit any tool_calls. Emit the required tool_calls now. Do not output explanatory text.', }, ]; try { const nudgeStream = activeClient.streamChat(nudgeMessages, tools); - currentResponse = ''; + let nudgeResponse = ''; for await (const chunk of nudgeStream) { - if (chunk.content) { - currentResponse += chunk.content; - } - if (chunk.tool_calls) { - toolCalls = [...toolCalls, ...chunk.tool_calls]; - } + if (chunk.content) nudgeResponse += chunk.content; + if (chunk.tool_calls) toolCalls = [...toolCalls, ...chunk.tool_calls]; } if (toolCalls.length > 0) { @@ -1572,31 +1557,27 @@ export class ChatView extends ItemView { toolCalls, nudgeMessages, tools, - currentResponse, + nudgeResponse, assistantMessageId ); - break; } } catch { - // Stream failed during retry — stop retrying and fall back to automatic tools - break; + // Stream failed — fall through to auto tool calls } - - currentMessages = nudgeMessages; } - // If retries all failed, force automatic read tools immediately + // Auto tool calls: fire immediately when the user's request implies vault operations + // and the model (with or without nudging) still hasn't called any tools. if (toolCalls.length === 0) { const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools); if (autoToolCalls.length > 0) { this.showActivityIndicator('Using tools…'); toolCalls = autoToolCalls; - fullResponse = ''; await this.processToolCalls( autoToolCalls, messagesWithMemory, tools, - fullResponse, + '', assistantMessageId ); } @@ -1733,25 +1714,15 @@ export class ChatView extends ItemView { 'files', 'continue', 'go ahead', - 'start', + 'proceed', 'do it', 'execute', - 'run', - 'proceed', - 'next', - 'now', - 'yes', - 'ok', - 'okay', - 'sure', - 'please', - 'step', + 'implement', + 'apply', 'merge', 'clean', 'fix', 'update', - 'implement', - 'apply', ]; return operationPhrases.some((phrase) => lower.includes(phrase)); }