diff --git a/main.js b/main.js index aebbf34..9c33fd6 100644 --- a/main.js +++ b/main.js @@ -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, diff --git a/src/agent-modes.ts b/src/agent-modes.ts index 461e872..7925e09 100644 --- a/src/agent-modes.ts +++ b/src/agent-modes.ts @@ -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.`, diff --git a/src/chat-view.ts b/src/chat-view.ts index 585d620..3cf2e75 100755 --- a/src/chat-view.ts +++ b/src/chat-view.ts @@ -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,