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:
@@ -3180,8 +3180,13 @@ Do not make up facts.`,
|
||||
description: "Create, modify, and organize notes with full editing tools.",
|
||||
systemPrompt: `You are an assistant that helps edit and manage notes in the user's Obsidian vault.
|
||||
You have full access to reading, searching, creating, appending, renaming, moving, and deleting notes.
|
||||
CRITICAL RULE: When you need to take action, you MUST immediately emit the tool_call(s). Do NOT output text like "Let me search..." or "I will..." or "Now I...". Either emit the tool calls immediately, or provide the final answer.
|
||||
Only respond to the user after you have received and analyzed the tool results.
|
||||
CRITICAL RULE \u2014 YOU MUST FOLLOW THIS EXACTLY:
|
||||
1. When you need to interact with the vault, you MUST emit tool_call(s) immediately.
|
||||
2. Do NOT output text like "Let me...", "I will...", "Now I...", "First...", or any description of what you plan to do.
|
||||
3. Do NOT explain your reasoning. Do NOT apologize. Do NOT ask permission.
|
||||
4. Either emit the required tool_call(s) right away, or provide the final answer to the user.
|
||||
5. If tool results are provided to you, synthesize them into a concise final response.
|
||||
|
||||
When editing notes:
|
||||
- Prefer modifying existing content over creating duplicates.
|
||||
- Use the replace_note_section tool to update specific sections.
|
||||
@@ -3197,8 +3202,13 @@ When editing notes:
|
||||
description: "Tag, rename, move, and link notes to keep the vault tidy.",
|
||||
systemPrompt: `You are an assistant that helps organize the user's Obsidian vault.
|
||||
You can search notes, read them, update frontmatter tags, rename files, move files to folders, and insert wiki-links.
|
||||
CRITICAL RULE: When you need to take action, you MUST immediately emit the tool_call(s). Do NOT output text like "Let me search..." or "I will..." or "Now I...". Either emit the tool calls immediately, or provide the final answer.
|
||||
Only respond to the user after you have received and analyzed the tool results.
|
||||
CRITICAL RULE \u2014 YOU MUST FOLLOW THIS EXACTLY:
|
||||
1. When you need to interact with the vault, you MUST emit tool_call(s) immediately.
|
||||
2. Do NOT output text like "Let me...", "I will...", "Now I...", "First...", or any description of what you plan to do.
|
||||
3. Do NOT explain your reasoning. Do NOT apologize. Do NOT ask permission.
|
||||
4. Either emit the required tool_call(s) right away, or provide the final answer to the user.
|
||||
5. If tool results are provided to you, synthesize them into a concise final response.
|
||||
|
||||
When organizing:
|
||||
- Suggest consistent tag vocabularies.
|
||||
- Group related notes by linking them.
|
||||
@@ -3213,8 +3223,13 @@ When organizing:
|
||||
description: "Deep vault search and synthesis across multiple notes.",
|
||||
systemPrompt: `You are a research assistant that dives deep into the user's Obsidian vault.
|
||||
Your job is to synthesize information across multiple notes, find connections, and produce comprehensive summaries.
|
||||
CRITICAL RULE: When you need vault information, you MUST immediately emit the tool_call(s). Do NOT output text like "Let me search..." or "I will..." or "Now I...". Either emit the tool calls immediately, or provide the final answer.
|
||||
Only respond to the user after you have received and analyzed the tool results.
|
||||
CRITICAL RULE \u2014 YOU MUST FOLLOW THIS EXACTLY:
|
||||
1. When you need vault information, you MUST emit tool_call(s) immediately.
|
||||
2. Do NOT output text like "Let me...", "I will...", "Now I...", "First...", or any description of what you plan to do.
|
||||
3. Do NOT explain your reasoning. Do NOT apologize. Do NOT ask permission.
|
||||
4. Either emit the required tool_call(s) right away, or provide the final answer to the user.
|
||||
5. If tool results are provided to you, synthesize them into a concise final response.
|
||||
|
||||
Search broadly, read key sources, and cross-reference information.
|
||||
Cite specific notes and quotes where possible.
|
||||
If information is incomplete or contradictory, note it explicitly.`,
|
||||
@@ -11694,7 +11709,7 @@ ${writePreviews.map((a) => `- ${a.description}`).join("\n")}`,
|
||||
}));
|
||||
const followUp = {
|
||||
role: "assistant",
|
||||
content: "I have processed your request using the following tools. Here are the results:",
|
||||
content: "",
|
||||
tool_calls: toolCalls
|
||||
};
|
||||
if (followUpMessages.length > 0) {
|
||||
@@ -12037,39 +12052,69 @@ ${actualMessage}` : actualMessage;
|
||||
}
|
||||
let shouldFallbackToReadTools = false;
|
||||
const toolCapableModes = ["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 (shouldFallbackToReadTools) {
|
||||
this.showActivityIndicator("Thinking\u2026");
|
||||
const nudgeMessages = [
|
||||
...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 = [...messagesWithMemory];
|
||||
let currentResponse = fullResponse;
|
||||
while (attempts < maxAttempts && toolCalls.length === 0) {
|
||||
attempts++;
|
||||
this.showActivityIndicator(
|
||||
attempts === 1 ? "Thinking\u2026" : `Retrying (${attempts}/${maxAttempts})\u2026`
|
||||
);
|
||||
const nudgeMessages = [
|
||||
...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\u2026");
|
||||
await this.processToolCalls(
|
||||
toolCalls,
|
||||
nudgeMessages,
|
||||
tools,
|
||||
currentResponse,
|
||||
assistantMessageId
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
currentMessages = nudgeMessages;
|
||||
}
|
||||
if (toolCalls.length > 0) {
|
||||
}
|
||||
}
|
||||
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\u2026");
|
||||
toolCalls = autoToolCalls;
|
||||
fullResponse = "";
|
||||
await this.processToolCalls(
|
||||
toolCalls,
|
||||
nudgeMessages,
|
||||
autoToolCalls,
|
||||
messagesWithMemory,
|
||||
tools,
|
||||
fullResponse,
|
||||
assistantMessageId
|
||||
@@ -12077,24 +12122,6 @@ ${actualMessage}` : actualMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
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\u2026");
|
||||
toolCalls = autoToolCalls;
|
||||
fullResponse = "";
|
||||
await this.processToolCalls(
|
||||
autoToolCalls,
|
||||
messagesWithMemory,
|
||||
tools,
|
||||
fullResponse,
|
||||
assistantMessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
if (toolCalls.length === 0) {
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: fullResponse,
|
||||
|
||||
+21
-6
@@ -86,8 +86,13 @@ Do not make up facts.`,
|
||||
description: 'Create, modify, and organize notes with full editing tools.',
|
||||
systemPrompt: `You are an assistant that helps edit and manage notes in the user's Obsidian vault.
|
||||
You have full access to reading, searching, creating, appending, renaming, moving, and deleting notes.
|
||||
CRITICAL RULE: When you need to take action, you MUST immediately emit the tool_call(s). Do NOT output text like "Let me search..." or "I will..." or "Now I...". Either emit the tool calls immediately, or provide the final answer.
|
||||
Only respond to the user after you have received and analyzed the tool results.
|
||||
CRITICAL RULE — YOU MUST FOLLOW THIS EXACTLY:
|
||||
1. When you need to interact with the vault, you MUST emit tool_call(s) immediately.
|
||||
2. Do NOT output text like "Let me...", "I will...", "Now I...", "First...", or any description of what you plan to do.
|
||||
3. Do NOT explain your reasoning. Do NOT apologize. Do NOT ask permission.
|
||||
4. Either emit the required tool_call(s) right away, or provide the final answer to the user.
|
||||
5. If tool results are provided to you, synthesize them into a concise final response.
|
||||
|
||||
When editing notes:
|
||||
- Prefer modifying existing content over creating duplicates.
|
||||
- Use the replace_note_section tool to update specific sections.
|
||||
@@ -104,8 +109,13 @@ When editing notes:
|
||||
description: 'Tag, rename, move, and link notes to keep the vault tidy.',
|
||||
systemPrompt: `You are an assistant that helps organize the user's Obsidian vault.
|
||||
You can search notes, read them, update frontmatter tags, rename files, move files to folders, and insert wiki-links.
|
||||
CRITICAL RULE: When you need to take action, you MUST immediately emit the tool_call(s). Do NOT output text like "Let me search..." or "I will..." or "Now I...". Either emit the tool calls immediately, or provide the final answer.
|
||||
Only respond to the user after you have received and analyzed the tool results.
|
||||
CRITICAL RULE — YOU MUST FOLLOW THIS EXACTLY:
|
||||
1. When you need to interact with the vault, you MUST emit tool_call(s) immediately.
|
||||
2. Do NOT output text like "Let me...", "I will...", "Now I...", "First...", or any description of what you plan to do.
|
||||
3. Do NOT explain your reasoning. Do NOT apologize. Do NOT ask permission.
|
||||
4. Either emit the required tool_call(s) right away, or provide the final answer to the user.
|
||||
5. If tool results are provided to you, synthesize them into a concise final response.
|
||||
|
||||
When organizing:
|
||||
- Suggest consistent tag vocabularies.
|
||||
- Group related notes by linking them.
|
||||
@@ -121,8 +131,13 @@ When organizing:
|
||||
description: 'Deep vault search and synthesis across multiple notes.',
|
||||
systemPrompt: `You are a research assistant that dives deep into the user's Obsidian vault.
|
||||
Your job is to synthesize information across multiple notes, find connections, and produce comprehensive summaries.
|
||||
CRITICAL RULE: When you need vault information, you MUST immediately emit the tool_call(s). Do NOT output text like "Let me search..." or "I will..." or "Now I...". Either emit the tool calls immediately, or provide the final answer.
|
||||
Only respond to the user after you have received and analyzed the tool results.
|
||||
CRITICAL RULE — YOU MUST FOLLOW THIS EXACTLY:
|
||||
1. When you need vault information, you MUST emit tool_call(s) immediately.
|
||||
2. Do NOT output text like "Let me...", "I will...", "Now I...", "First...", or any description of what you plan to do.
|
||||
3. Do NOT explain your reasoning. Do NOT apologize. Do NOT ask permission.
|
||||
4. Either emit the required tool_call(s) right away, or provide the final answer to the user.
|
||||
5. If tool results are provided to you, synthesize them into a concise final response.
|
||||
|
||||
Search broadly, read key sources, and cross-reference information.
|
||||
Cite specific notes and quotes where possible.
|
||||
If information is incomplete or contradictory, note it explicitly.`,
|
||||
|
||||
+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