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:
+75
-54
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user