Strengthen tool-calling discipline with retries and automatic fallback

Expand CRITICAL RULE instructions across edit, organize, and research agent
modes to explicitly prohibit planning language, reasoning, apologies, and
permission-seeking. Replace generic follow-up text with empty content when
tool calls are emitted.

Add a retry loop (up to 3 attempts) that suppresses text-only responses
and re-prompts the model to emit tool_calls immediately. If the model
still fails to call tools after retries, fall back to automatically
generated read tool calls based on the user's message intent.
This commit is contained in:
2026-05-21 15:09:40 +02:00
parent 949e7d77ff
commit 99a644ba96
3 changed files with 175 additions and 112 deletions
+75 -54
View File
@@ -984,7 +984,7 @@ export class ChatView extends ItemView {
const followUp: OllamaMessage = {
role: 'assistant',
content: 'I have processed your request using the following tools. Here are the results:',
content: '',
tool_calls: toolCalls,
};
@@ -1405,44 +1405,85 @@ export class ChatView extends ItemView {
// Auto-nudge for tool-capable modes if assistant didn't emit tools but seems to intend to
let shouldFallbackToReadTools = false;
const toolCapableModes: AgentMode[] = ['edit', 'organize', 'research'];
if (
toolCalls.length === 0 &&
toolCapableModes.includes(this.currentAgentMode) &&
fullResponse.trim().length > 0
) {
const isToolCapable = toolCapableModes.includes(this.currentAgentMode);
if (isToolCapable && toolCalls.length === 0 && fullResponse.trim().length > 0) {
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
// If the model output action-text without tools, suppress it and retry up to 3 times
if (shouldFallbackToReadTools) {
this.showActivityIndicator('Thinking…');
const nudgeMessages: OllamaMessage[] = [
...messagesWithMemory,
{ role: 'assistant', content: fullResponse },
{
role: 'user',
content:
'You indicated you would take action but did not emit any tool_calls. Please emit the required tool_calls now. Do not output explanatory text.',
},
];
const nudgeStream = activeClient.streamChat(nudgeMessages, tools);
fullResponse = '';
toolCalls = [];
for await (const chunk of nudgeStream) {
if (chunk.content) {
fullResponse += chunk.content;
this.updateMessageById(assistantMessageId, {
content: fullResponse,
isStreaming: true,
isThinking: false,
});
}
if (chunk.tool_calls) {
toolCalls = [...toolCalls, ...chunk.tool_calls];
let attempts = 0;
const maxAttempts = 3;
let currentMessages: OllamaMessage[] = [...messagesWithMemory];
let currentResponse = fullResponse;
while (attempts < maxAttempts && toolCalls.length === 0) {
attempts++;
this.showActivityIndicator(
attempts === 1 ? 'Thinking…' : `Retrying (${attempts}/${maxAttempts})…`
);
// Replace the assistant's text-only response with a forced instruction
const nudgeMessages: OllamaMessage[] = [
...currentMessages,
{ role: 'assistant', content: currentResponse },
{
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.',
},
];
try {
const nudgeStream = activeClient.streamChat(nudgeMessages, tools);
currentResponse = '';
for await (const chunk of nudgeStream) {
if (chunk.content) {
currentResponse += chunk.content;
}
if (chunk.tool_calls) {
toolCalls = [...toolCalls, ...chunk.tool_calls];
}
}
if (toolCalls.length > 0) {
this.showActivityIndicator('Using tools…');
await this.processToolCalls(
toolCalls,
nudgeMessages,
tools,
currentResponse,
assistantMessageId
);
break;
}
} catch {
// Stream failed during retry — stop retrying and fall back to automatic tools
break;
}
// If still no tools, continue the loop with the new response as context
currentMessages = nudgeMessages;
}
if (toolCalls.length > 0) {
}
}
// Final fallback: if the model NEVER emitted tools but clearly intended to, force automatic read tools
if (isToolCapable && toolCalls.length === 0) {
if (!shouldFallbackToReadTools) {
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
}
if (shouldFallbackToReadTools) {
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
if (autoToolCalls.length > 0) {
this.showActivityIndicator('Using tools…');
toolCalls = autoToolCalls;
fullResponse = '';
await this.processToolCalls(
toolCalls,
nudgeMessages,
autoToolCalls,
messagesWithMemory,
tools,
fullResponse,
assistantMessageId
@@ -1451,27 +1492,7 @@ export class ChatView extends ItemView {
}
}
if (!shouldFallbackToReadTools) {
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
}
if (toolCalls.length === 0 && shouldFallbackToReadTools) {
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
);
}
}
// Update assistant message immutably — only if no tool calls were processed
// Update assistant message — only if no tool calls were processed
if (toolCalls.length === 0) {
this.updateMessageById(assistantMessageId, {
content: fullResponse,