diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index f2b28552..966749e9 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -721,6 +721,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') + console.log('tool call!!', toolCall) resMessageIsDonePromise(toolCall) // resolve with tool calls }, onError: (error) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts index 8fb20d3d..dc6b66c9 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -13,7 +13,7 @@ import { createSaxParser } from './sax.js' // =============== reasoning =============== // could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true -export const extractReasoningOnTextWrapper = ( +export const extractReasoningWrapper = ( onText: OnText, onFinalMessage: OnFinalMessage, thinkTags: [string, string] ): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => { let latestAddIdx = 0 // exclusive index in fullText_ @@ -122,6 +122,10 @@ export const extractReasoningOnTextWrapper = ( } const newOnFinalMessage: OnFinalMessage = (params) => { + + // treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage) + newOnText({ ...params }) + const { fullText, fullReasoning } = getOnFinalMessageParams() onFinalMessage({ ...params, fullText, fullReasoning }) } @@ -145,7 +149,7 @@ type ToolsState = { currentToolCall: RawToolCallObj, } -export const extractToolsOnTextWrapper = ( +export const extractToolsWrapper = ( onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode ): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => { const tools = availableTools(chatMode) @@ -288,16 +292,44 @@ export const extractToolsOnTextWrapper = ( const newOnFinalMessage: OnFinalMessage = (params) => { + // treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage) + newOnText({ ...params }) + console.log('final message!!!', trueFullText) console.log('----- returning ----\n', fullText) console.log('----- tools ----\n', JSON.stringify(currentToolCalls, null, 2)) - onFinalMessage({ ...params, fullText, toolCall: currentToolCalls[0] }) - } + fullText = fullText.trimEnd() + const toolCall = currentToolCalls[0] + if (toolCall) { + // trim off all whitespace at and before first \n and after last \n for each param + for (const paramName in toolCall.rawParams) { + const orig = toolCall.rawParams[paramName] + if (orig === undefined) continue + toolCall.rawParams[paramName] = trimBeforeAndAfterNewLines(orig) + } + } + onFinalMessage({ ...params, fullText, toolCall: currentToolCalls.length > 0 ? currentToolCalls[0] : undefined }) + } return { newOnText, newOnFinalMessage }; } +// trim all whitespace up until the first newline, and all whitespace after the last newline +const trimBeforeAndAfterNewLines = (s: string) => { + if (!s) return s; + const firstNewLineIndex = s.indexOf('\n'); + if (firstNewLineIndex !== -1 && s.substring(0, firstNewLineIndex).trim() === '') { + s = s.substring(firstNewLineIndex + 1, Infinity) + } + + const lastNewLineIndex = s.lastIndexOf('\n'); + if (lastNewLineIndex !== -1 && s.substring(lastNewLineIndex + 1, Infinity).trim() === '') { + s = s.substring(0, lastNewLineIndex) + } + + return s +} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 3f49d017..edcc1a9a 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -11,7 +11,7 @@ import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, On import { ChatMode, defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js'; -import { extractReasoningOnTextWrapper, extractToolsOnTextWrapper } from './extractGrammar.js'; +import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js'; type InternalCommonMessageParams = { @@ -156,14 +156,14 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const { needsManualParse: needsManualReasoningParse, nameOfFieldInDelta: nameOfReasoningFieldInDelta } = providerReasoningIOSettings?.output ?? {} const manuallyParseReasoning = needsManualReasoningParse && canIOReasoning && openSourceThinkTags if (manuallyParseReasoning) { - const { newOnText, newOnFinalMessage } = extractReasoningOnTextWrapper(onText, onFinalMessage, openSourceThinkTags) + const { newOnText, newOnFinalMessage } = extractReasoningWrapper(onText, onFinalMessage, openSourceThinkTags) onText = newOnText onFinalMessage = newOnFinalMessage } // manually parse out tool results if (chatMode) { - const { newOnText, newOnFinalMessage } = extractToolsOnTextWrapper(onText, onFinalMessage, chatMode) + const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode) onText = newOnText onFinalMessage = newOnFinalMessage } @@ -281,7 +281,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM // manually parse out tool results if (chatMode) { - const { newOnText, newOnFinalMessage } = extractToolsOnTextWrapper(onText, onFinalMessage, chatMode) + const { newOnText, newOnFinalMessage } = extractToolsWrapper(onText, onFinalMessage, chatMode) onText = newOnText onFinalMessage = newOnFinalMessage } @@ -290,8 +290,8 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM let fullText = '' let fullReasoning = '' - let fullToolName = '' - let fullToolParams = '' + // let fullToolName = '' + // let fullToolParams = '' // there are no events for tool_use, it comes in at the end stream.on('streamEvent', e => { @@ -313,10 +313,10 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM fullReasoning += '[redacted_thinking]' onText({ fullText, fullReasoning, }) } - else if (e.content_block.type === 'tool_use') { - fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block - onText({ fullText, fullReasoning, }) - } + // else if (e.content_block.type === 'tool_use') { + // fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block + // onText({ fullText, fullReasoning, }) + // } } // delta @@ -329,10 +329,10 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM fullReasoning += e.delta.thinking onText({ fullText, fullReasoning, }) } - else if (e.delta.type === 'input_json_delta') { // tool use - fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming - onText({ fullText, fullReasoning, }) - } + // else if (e.delta.type === 'input_json_delta') { // tool use + // fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming + // onText({ fullText, fullReasoning, }) + // } } })