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.
This commit is contained in:
2026-05-21 17:45:00 +02:00
parent c2c2d51da5
commit 3d9be7dfdb
2 changed files with 37 additions and 90 deletions
+16 -40
View File
@@ -11837,7 +11837,7 @@ ${writePreviews.map((a) => `- ${a.description}`).join("\n")}`,
); );
} else { } else {
this.updateMessageById(assistantMessageId, { this.updateMessageById(assistantMessageId, {
content: followUpContent || fullResponse, content: followUpContent || fullResponse || "(No response)",
isStreaming: false, isStreaming: false,
isThinking: false isThinking: false
}); });
@@ -12182,39 +12182,29 @@ ${actualMessage}` : actualMessage;
if (isToolCapable && toolCalls.length === 0) { if (isToolCapable && toolCalls.length === 0) {
if (modelMentionedActions || userWantsVaultOps) { if (modelMentionedActions || userWantsVaultOps) {
shouldFallbackToReadTools = true; shouldFallbackToReadTools = true;
const priorResponse = fullResponse;
fullResponse = ""; fullResponse = "";
this.updateMessageById(assistantMessageId, { this.updateMessageById(assistantMessageId, {
content: "", content: "",
isStreaming: false, isStreaming: false,
isThinking: false isThinking: false
}); });
let attempts = 0; if (modelMentionedActions) {
const maxAttempts = 3; this.showActivityIndicator("Thinking\u2026");
let currentMessages = [...messagesWithMemory];
let currentResponse = "";
while (attempts < maxAttempts && toolCalls.length === 0) {
attempts++;
this.showActivityIndicator(
attempts === 1 ? "Thinking\u2026" : `Retrying (${attempts}/${maxAttempts})\u2026`
);
const nudgeMessages = [ const nudgeMessages = [
...currentMessages, ...messagesWithMemory,
...currentResponse.trim() ? [{ role: "assistant", content: currentResponse }] : [], ...priorResponse.trim() ? [{ role: "assistant", content: priorResponse }] : [],
{ {
role: "user", 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 { try {
const nudgeStream = activeClient.streamChat(nudgeMessages, tools); const nudgeStream = activeClient.streamChat(nudgeMessages, tools);
currentResponse = ""; let nudgeResponse = "";
for await (const chunk of nudgeStream) { for await (const chunk of nudgeStream) {
if (chunk.content) { if (chunk.content) nudgeResponse += chunk.content;
currentResponse += chunk.content; if (chunk.tool_calls) toolCalls = [...toolCalls, ...chunk.tool_calls];
}
if (chunk.tool_calls) {
toolCalls = [...toolCalls, ...chunk.tool_calls];
}
} }
if (toolCalls.length > 0) { if (toolCalls.length > 0) {
this.showActivityIndicator("Using tools\u2026"); this.showActivityIndicator("Using tools\u2026");
@@ -12222,27 +12212,23 @@ ${actualMessage}` : actualMessage;
toolCalls, toolCalls,
nudgeMessages, nudgeMessages,
tools, tools,
currentResponse, nudgeResponse,
assistantMessageId assistantMessageId
); );
break;
} }
} catch { } catch {
break;
} }
currentMessages = nudgeMessages;
} }
if (toolCalls.length === 0) { if (toolCalls.length === 0) {
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools); const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
if (autoToolCalls.length > 0) { if (autoToolCalls.length > 0) {
this.showActivityIndicator("Using tools\u2026"); this.showActivityIndicator("Using tools\u2026");
toolCalls = autoToolCalls; toolCalls = autoToolCalls;
fullResponse = "";
await this.processToolCalls( await this.processToolCalls(
autoToolCalls, autoToolCalls,
messagesWithMemory, messagesWithMemory,
tools, tools,
fullResponse, "",
assistantMessageId assistantMessageId
); );
} }
@@ -12361,25 +12347,15 @@ ${actualMessage}` : actualMessage;
"files", "files",
"continue", "continue",
"go ahead", "go ahead",
"start", "proceed",
"do it", "do it",
"execute", "execute",
"run", "implement",
"proceed", "apply",
"next",
"now",
"yes",
"ok",
"okay",
"sure",
"please",
"step",
"merge", "merge",
"clean", "clean",
"fix", "fix",
"update", "update"
"implement",
"apply"
]; ];
return operationPhrases.some((phrase) => lower.includes(phrase)); return operationPhrases.some((phrase) => lower.includes(phrase));
} }
+21 -50
View File
@@ -1092,7 +1092,7 @@ export class ChatView extends ItemView {
); );
} else { } else {
this.updateMessageById(assistantMessageId, { this.updateMessageById(assistantMessageId, {
content: followUpContent || fullResponse, content: followUpContent || fullResponse || '(No response)',
isStreaming: false, isStreaming: false,
isThinking: false, isThinking: false,
}); });
@@ -1520,6 +1520,7 @@ export class ChatView extends ItemView {
shouldFallbackToReadTools = true; shouldFallbackToReadTools = true;
// Suppress the model's "Let me..." text — clear it from the DOM immediately // Suppress the model's "Let me..." text — clear it from the DOM immediately
const priorResponse = fullResponse;
fullResponse = ''; fullResponse = '';
this.updateMessageById(assistantMessageId, { this.updateMessageById(assistantMessageId, {
content: '', content: '',
@@ -1527,43 +1528,27 @@ export class ChatView extends ItemView {
isThinking: false, isThinking: false,
}); });
// Try aggressive retry loop first (hidden from user) // Single nudge — only when the model actually expressed intent to act.
let attempts = 0; // Lying to models that never mentioned action ("you said you would...") confuses
const maxAttempts = 3; // them and wastes a full LLM round-trip without benefit.
let currentMessages: OllamaMessage[] = [...messagesWithMemory]; if (modelMentionedActions) {
let currentResponse = ''; this.showActivityIndicator('Thinking…');
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
const nudgeMessages: OllamaMessage[] = [ const nudgeMessages: OllamaMessage[] = [
...currentMessages, ...messagesWithMemory,
...(currentResponse.trim() ...(priorResponse.trim() ? [{ role: 'assistant' as const, content: priorResponse }] : []),
? [{ role: 'assistant' as const, content: currentResponse }]
: []),
{ {
role: 'user', role: 'user',
content: 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 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.',
}, },
]; ];
try { try {
const nudgeStream = activeClient.streamChat(nudgeMessages, tools); const nudgeStream = activeClient.streamChat(nudgeMessages, tools);
currentResponse = ''; let nudgeResponse = '';
for await (const chunk of nudgeStream) { for await (const chunk of nudgeStream) {
if (chunk.content) { if (chunk.content) nudgeResponse += chunk.content;
currentResponse += chunk.content; if (chunk.tool_calls) toolCalls = [...toolCalls, ...chunk.tool_calls];
}
if (chunk.tool_calls) {
toolCalls = [...toolCalls, ...chunk.tool_calls];
}
} }
if (toolCalls.length > 0) { if (toolCalls.length > 0) {
@@ -1572,31 +1557,27 @@ export class ChatView extends ItemView {
toolCalls, toolCalls,
nudgeMessages, nudgeMessages,
tools, tools,
currentResponse, nudgeResponse,
assistantMessageId assistantMessageId
); );
break;
} }
} catch { } catch {
// Stream failed during retry — stop retrying and fall back to automatic tools // Stream failed — fall through to auto tool calls
break;
} }
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) { if (toolCalls.length === 0) {
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools); const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
if (autoToolCalls.length > 0) { if (autoToolCalls.length > 0) {
this.showActivityIndicator('Using tools…'); this.showActivityIndicator('Using tools…');
toolCalls = autoToolCalls; toolCalls = autoToolCalls;
fullResponse = '';
await this.processToolCalls( await this.processToolCalls(
autoToolCalls, autoToolCalls,
messagesWithMemory, messagesWithMemory,
tools, tools,
fullResponse, '',
assistantMessageId assistantMessageId
); );
} }
@@ -1733,25 +1714,15 @@ export class ChatView extends ItemView {
'files', 'files',
'continue', 'continue',
'go ahead', 'go ahead',
'start', 'proceed',
'do it', 'do it',
'execute', 'execute',
'run', 'implement',
'proceed', 'apply',
'next',
'now',
'yes',
'ok',
'okay',
'sure',
'please',
'step',
'merge', 'merge',
'clean', 'clean',
'fix', 'fix',
'update', 'update',
'implement',
'apply',
]; ];
return operationPhrases.some((phrase) => lower.includes(phrase)); return operationPhrases.some((phrase) => lower.includes(phrase));
} }