diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 29bf7796..6a78483b 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -12,7 +12,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { chat_userMessageContent, chat_systemMessage, } from '../common/prompt/prompts.js'; -import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; +import { getErrorMessage, LLMChatMessage, ParsedToolCallObj, ParsedToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; @@ -67,14 +67,18 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { // merge tools into user message for (const c of chatMessages) { - if (c.role === 'user') { - llmChatMessages.push({ role: c.role, content: c.content }) - } - else if (c.role === 'assistant') + if (c.role === 'assistant') llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning }) - else if (c.role === 'tool') - llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content }) - else if (c.role === 'decorative_canceled_tool') { // pass + // merge all tool/user messages into one big user message + else if (c.role === 'user' || c.role === 'tool') { + if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user') { + llmChatMessages.push({ role: 'user', content: c.content }) + } + else { + llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content + } + } + else if (c.role === 'interrupted_streaming_tool') { // pass } else if (c.role === 'checkpoint') { // pass } @@ -144,8 +148,7 @@ export type ThreadStreamState = { streamingToken?: string; messageSoFar?: string; reasoningSoFar?: string; - toolNameSoFar?: string; - toolParamsSoFar?: string; + toolCallSoFar?: ParsedToolCallObj; } } @@ -473,8 +476,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { role: 'tool', type: 'running_now', name: lastMsg.name, - paramsStr: lastMsg.paramsStr, - id: lastMsg.id, params: lastMsg.params, content: '(value not received yet...)', // this typically shouldn't ever get read result: null @@ -497,29 +498,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { } else return - const { name, paramsStr, id } = lastMsg + const { name } = lastMsg const errorMessage = this.errMsgs.rejected - this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, paramsStr: paramsStr, id, content: errorMessage, result: null }) + this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null }) this._setStreamState(threadId, {}, 'set') } - // private _rejectLatestStreamingTool(threadId: string) { - // const thread = this.state.allThreads[threadId] - // if (!thread) return // should never happen - - // const lastMessage = thread.messages[thread.messages.length - 1] - // if (lastMessage.role !== 'tool') return - // const { name, paramsStr, id, result } = lastMessage - // if (result.type !== 'running_now') return - // const { params } = result - - // const errorMessage = this.errMsgs.rejected - // this._swapOutLatestStreamingToolWithResult(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, }) - // this._setStreamState(threadId, {}, 'set') - - // } - stopRunning(threadId: string) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen @@ -536,16 +521,16 @@ class ChatThreadService extends Disposable implements IChatThreadService { // abort the stream first so it doesn't change any state const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - const toolInProgress = this.streamState[threadId]?.toolNameSoFar - console.log('toolInProgress', toolInProgress) + const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar + console.log('toolInProgress', toolCallSoFar) const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - if (toolInProgress) { - this._addMessageToThread(threadId, { role: 'decorative_canceled_tool', name: toolInProgress }) + if (toolCallSoFar) { + this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) } } @@ -616,26 +601,24 @@ class ChatThreadService extends Disposable implements IChatThreadService { // returns true when the tool call is waiting for user approval const handleToolCall = async ( - tool: ToolCallType, - opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] }, + toolName: ToolName, + opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: ParsedToolParamsObj }, ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { - const toolName: ToolName = tool.name - const toolParamsStr = tool.paramsStr - const toolId = tool.id // compute these below let toolParams: ToolCallParams[ToolName] let toolResult: ToolResultType[typeof toolName] let toolResultStr: string - if (!opts?.preapproved) { // skip this if pre-approved + if (!opts.preapproved) { // skip this if pre-approved // 1. validate tool params try { - const params = await this._toolsService.validateParams[toolName](toolParamsStr) + + const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams) toolParams = params } catch (error) { const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, }) + this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, }) return {} } // once validated, add checkpoint for edit @@ -646,14 +629,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (requiresApproval) { const autoApprove = this._settingsService.state.globalSettings.autoApprove // add a tool_request because we use it for UI if a tool is loading (this should be improved in the future) - this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) + this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams }) if (!autoApprove) { return { awaitingUserApproval: true } } } } else { - toolParams = opts.toolParams + toolParams = opts.validatedParams } // 3. call the tool @@ -674,7 +657,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { return { interrupted: true } } const errorMessage = getErrorMessage(error) - this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, }) + this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, }) return {} } @@ -683,12 +666,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) } catch (error) { const errorMessage = this.errMsgs.errWhenStringifying(error) - this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, }) + this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, }) return {} } // 5. add to history and keep going - this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, }) + this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, }) return {} }; @@ -706,7 +689,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // before enter loop, call tool if (callThisToolFirst) { - const { interrupted } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) + const { interrupted } = await handleToolCall(callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params }) if (interrupted) return } @@ -717,27 +700,28 @@ class ChatThreadService extends Disposable implements IChatThreadService { isRunningWhenEnd = undefined nMessagesSent += 1 - let resMessageIsDonePromise: (toolCalls?: ToolCallType[] | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) + let resMessageIsDonePromise: (toolCall?: ParsedToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) // send llm message this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge') const messages = await getLatestMessages() const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', + chatMode, messages, modelSelection, modelSelectionOptions, logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, - onText: ({ fullText, fullReasoning, fullToolName, fullToolParams }) => { - this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolNameSoFar: fullToolName, toolParamsSoFar: fullToolParams }, 'merge') + onText: ({ fullText, fullReasoning, toolCall }) => { + this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge') }, - onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { + onFinalMessage: async ({ fullText, toolCall, fullReasoning, anthropicReasoning }) => { this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) // added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolNameSoFar: undefined, toolParamsSoFar: undefined }, 'merge') + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') // resolve with tool calls - resMessageIsDonePromise(toolCalls) + resMessageIsDonePromise(toolCall) }, onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' @@ -763,14 +747,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { break } this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message - const toolCalls = await messageIsDonePromise // wait for message to complete + const toolCall = await messageIsDonePromise // wait for message to complete if (aborted) { return } this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done // call tool if there is one - const tool: ToolCallType | undefined = toolCalls?.[0] + const tool: ParsedToolCallObj | undefined = toolCall if (tool) { - const { awaitingUserApproval, interrupted } = await handleToolCall(tool) + const { awaitingUserApproval, interrupted } = await handleToolCall(tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams }) // stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools. // just detect tool interruption which is the same as chat interruption right now diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 2b4d2eae..7806cfca 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1400,6 +1400,7 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, modelSelection, modelSelectionOptions, + chatMode: null, // not chat onText: (params) => { const { fullText: fullText_ } = params const newText_ = fullText_.substring(fullTextSoFar.length, Infinity) @@ -1617,6 +1618,7 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, modelSelection, modelSelectionOptions, + chatMode: null, // not chat onText: (params) => { const { fullText } = params // blocks are [done done done ... {writingFinal|writingOriginal}] diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index bad7832e..f36435d7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1921,7 +1921,7 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes return null } - else if (role === 'decorative_canceled_tool') { + else if (role === 'interrupted_streaming_tool') { return
diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 1936be08..2a50aeca 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -16,6 +16,7 @@ import { IVoidCommandBarService } from './voidCommandBarService.js' import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js' import { IMarkerService } from '../../../../platform/markers/common/markers.js' import { timeout } from '../../../../base/common/async.js' +import { ParsedToolParamsObj } from '../common/sendLLMMessageTypes.js' // tool use for AI @@ -23,7 +24,7 @@ import { timeout } from '../../../../base/common/async.js' -type ValidateParams = { [T in ToolName]: (p: string) => Promise } +type ValidateParams = { [T in ToolName]: (p: ParsedToolParamsObj) => Promise } type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> } type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited) => string } @@ -38,25 +39,6 @@ export const TERMINAL_TIMEOUT_TIME = 15 export const TERMINAL_BG_WAIT_TIME = 1 - - - -const validateJSON = (s: string): { [s: string]: unknown } => { - try { - const o = JSON.parse(s) - if (typeof o !== 'object') throw new Error() - - if ('result' in o) { // openrouter sometimes wraps the result with { 'result': ... } - return o.result - } - - return o - } - catch (e) { - throw new Error(`Invalid LLM output format: Tool parameter was not a string of a valid JSON: "${s}".`) - } -} - const isFalsy = (u: unknown) => { return !u || u === 'null' || u === 'undefined' } @@ -172,9 +154,8 @@ export class ToolsService implements IToolsService { const queryBuilder = instantiationService.createInstance(QueryBuilder); this.validateParams = { - read_file: async (params: string) => { - const o = validateJSON(params) - const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = o + read_file: async (params: ParsedToolParamsObj) => { + const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = params const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) @@ -184,27 +165,24 @@ export class ToolsService implements IToolsService { return { uri, startLine, endLine, pageNumber } }, - ls_dir: async (params: string) => { - const o = validateJSON(params) - const { uri: uriStr, pageNumber: pageNumberUnknown } = o + ls_dir: async (params: ParsedToolParamsObj) => { + const { uri: uriStr, pageNumber: pageNumberUnknown } = params const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) return { rootURI: uri, pageNumber } }, - get_dir_structure: async (params: string) => { - const o = validateJSON(params) - const { uri: uriStr, } = o + get_dir_structure: async (params: ParsedToolParamsObj) => { + const { uri: uriStr, } = params const uri = validateURI(uriStr) return { rootURI: uri } }, - search_pathnames_only: async (params: string) => { - const o = validateJSON(params) + search_pathnames_only: async (params: ParsedToolParamsObj) => { const { query: queryUnknown, include: includeUnknown, pageNumber: pageNumberUnknown - } = o + } = params const queryStr = validateStr('query', queryUnknown) const pageNumber = validatePageNum(pageNumberUnknown) @@ -213,14 +191,13 @@ export class ToolsService implements IToolsService { return { queryStr, include, pageNumber } }, - search_files: async (params: string) => { - const o = validateJSON(params) + search_files: async (params: ParsedToolParamsObj) => { const { query: queryUnknown, searchInFolder: searchInFolderUnknown, isRegex: isRegexUnknown, pageNumber: pageNumberUnknown - } = o + } = params const queryStr = validateStr('query', queryUnknown) const pageNumber = validatePageNum(pageNumberUnknown) @@ -233,18 +210,16 @@ export class ToolsService implements IToolsService { // --- - create_file_or_folder: async (params: string) => { - const o = validateJSON(params) - const { uri: uriUnknown } = o + create_file_or_folder: async (params: ParsedToolParamsObj) => { + const { uri: uriUnknown } = params const uri = validateURI(uriUnknown) const uriStr = validateStr('uri', uriUnknown) const isFolder = checkIfIsFolder(uriStr) return { uri, isFolder } }, - delete_file_or_folder: async (params: string) => { - const o = validateJSON(params) - const { uri: uriUnknown, params: paramsStr } = o + delete_file_or_folder: async (params: ParsedToolParamsObj) => { + const { uri: uriUnknown, params: paramsStr } = params const uri = validateURI(uriUnknown) const isRecursive = validateRecursiveParamStr(paramsStr) const uriStr = validateStr('uri', uriUnknown) @@ -252,17 +227,15 @@ export class ToolsService implements IToolsService { return { uri, isRecursive, isFolder } }, - edit_file: async (params: string) => { - const o = validateJSON(params) - const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o + edit_file: async (params: ParsedToolParamsObj) => { + const { uri: uriStr, changeDescription: changeDescriptionUnknown } = params const uri = validateURI(uriStr) const changeDescription = validateStr('changeDescription', changeDescriptionUnknown) return { uri, changeDescription } }, - run_terminal_command: async (s: string) => { - const o = validateJSON(s) - const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o + run_terminal_command: async (params: ParsedToolParamsObj) => { + const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = params const command = validateStr('command', commandUnknown) const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown) const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true }) diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 915a3e7d..7cf2ec5d 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -10,8 +10,6 @@ import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js export type ToolMessage = { role: 'tool'; - paramsStr: string; // internal use - id: string; // apis require this tool use id content: string; // give this result to LLM (string of value) } & ( // in order of events: @@ -27,18 +25,10 @@ export type ToolMessage = { ) // user rejected export type DecorativeCanceledTool = { - role: 'decorative_canceled_tool'; + role: 'interrupted_streaming_tool'; name: string; } -// export type ToolRequestApproval = { -// role: 'tool_request'; -// name: T; // internal use -// params: ToolCallParams[T]; // internal use -// paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params) -// id: string; // proposed tool's id -// } - // checkpoints export type CheckpointEntry = { diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index e84c9437..218aee01 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -212,15 +212,17 @@ Available tools: ${availableToolsStr(tools)} Tool calling details: ${''/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */} +- Tool calling is optional. - To call a tool, just write its name followed by any parameters in XML format. For example: value1 value2 -- You must write all tool calls at the END of your response. The beginning of your response should be your normal response followed by tool calls at the END. -- You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them. -- Tool that you call will be executed immediately, and you will have access to the results in your next response.` +- You must write your tool call at the END of your response. The beginning of your response should be your normal response followed by the tool call at the END. +- You are only allowed to output one tool call per response. +- The tool call will be executed immediately, and you will have access to the results in your next response.` } +// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them. // ======================================================== chat (normal, gather, agent) ======================================================== diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 8378cbd0..881e6201 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import { ToolName } from './toolsServiceTypes.js' -import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' +import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' export const errorDetails = (fullError: Error | null): string | null => { @@ -40,16 +40,21 @@ export type LLMChatMessage = { } -export type ToolCallType = { - name: ToolName; - paramsStr: string; - id: string; +export type ParsedToolParamsObj = { + [paramName: string]: string; } +export type ParsedToolCallObj = { + name: ToolName; + rawParams: ParsedToolParamsObj; + doneParams: string[]; + isDone: boolean; +}; + export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any }) -export type OnText = (p: { fullText: string; fullReasoning: string; }) => void -export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id +export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: ParsedToolCallObj }) => void +export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCall?: ParsedToolCallObj; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id export type OnError = (p: { message: string; fullError: Error | null }) => void export type OnAbort = () => void export type AbortRef = { current: (() => void) | null } @@ -64,9 +69,11 @@ export type LLMFIMMessage = { type SendLLMType = { messagesType: 'chatMessages'; messages: LLMChatMessage[]; + chatMode: ChatMode | null; } | { messagesType: 'FIMMessage'; messages: LLMFIMMessage; + chatMode?: undefined; } // service types diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 47558e71..28fb0fb7 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -95,7 +95,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = { 'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } }, - 'Chat': { filter: (o, { chatMode }) => chatMode === 'normal' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } }, + 'Chat': { filter: o => true, emptyMessage: null, }, 'Ctrl+K': { filter: o => true, emptyMessage: null, }, 'Apply': { filter: o => true, emptyMessage: null, }, } 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 67430034..35a1ed2f 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -1,10 +1,11 @@ import { endsWithAnyPrefixOf } from '../../common/helpers/extractCodeFromResult.js' import { InternalToolInfo } from '../../common/prompt/prompts.js' -import { OnText } from '../../common/sendLLMMessageTypes.js' +import { OnText, ParsedToolCallObj } from '../../common/sendLLMMessageTypes.js' import sax from 'sax' +import { ToolName } from '../../common/toolsServiceTypes.js' -// =========================================== reasoning =========================================== +// =============== 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 = (onText: OnText, thinkTags: [string, string]): OnText => { @@ -115,19 +116,19 @@ export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [st } -// =========================================== tools =========================================== +// =============== tools =============== type ToolsState = { level: 'normal', } | { level: 'tool', toolName: string, - currentToolCall: ToolCall, + currentToolCall: ParsedToolCallObj, } | { level: 'param', toolName: string, paramName: string, - currentToolCall: ToolCall, + currentToolCall: ParsedToolCallObj, } export const extractToolsOnTextWrapper = (onText: OnText, availableTools: InternalToolInfo[]) => { @@ -137,16 +138,20 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern // detect , etc let fullText = ''; let trueFullText = '' - const currentToolCalls: ToolCall[] = []; // the answer + const currentToolCalls: ParsedToolCallObj[] = []; // the answer let state: ToolsState = { level: 'normal' } + + const getRawNewText = () => { + return trueFullText.substring(parser.startTagPosition, parser.position + 1) + } const parser = sax.parser(false); // when see open tag parser.onopentag = (node) => { - const rawNewText = trueFullText.substring(parser.startTagPosition, parser.position) + const rawNewText = getRawNewText() console.log('raw new text a', rawNewText) console.log('OPEN!', node.name) const tagName = node.name; @@ -155,7 +160,7 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern state = { level: 'tool', toolName: tagName, - currentToolCall: { name: tagName, parameters: {} } + currentToolCall: { name: tagName as ToolName, rawParams: {}, doneParams: [], isDone: false } } } else { @@ -190,12 +195,12 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern // ignore all text in a tool, all text should go in the param tags inside it } else if (state.level === 'param') { - state.currentToolCall.parameters[state.currentToolCall.name] += text + state.currentToolCall.rawParams[state.currentToolCall.name] += text } } parser.onclosetag = (tagName) => { - const rawNewText = trueFullText.substring(parser.startTagPosition, parser.position) + const rawNewText = getRawNewText() console.log('raw new text b', rawNewText) console.log('CLOSE!', tagName) if (state.level === 'normal') { @@ -203,6 +208,7 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern } else if (state.level === 'tool') { if (tagName === state.toolName) { // closed the tool + state.currentToolCall.isDone = true currentToolCalls.push(state.currentToolCall) state = { level: 'normal', @@ -214,6 +220,7 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern } else if (state.level === 'param') { if (tagName === state.paramName) { // closed the param + state.currentToolCall.doneParams.push(state.paramName) state = { level: 'tool', toolName: state.toolName, @@ -226,15 +233,14 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern const newOnText: OnText = (params) => { const newText = params.fullText.substring(fullText.length); - console.log('newText', newText) + console.log('newText', state.level, newText) trueFullText = params.fullText parser.write(newText) - console.log('state',) onText({ ...params, fullText, - toolCalls: currentToolCalls.length > 0 ? [...currentToolCalls] : undefined + toolCall: currentToolCalls.length > 0 ? currentToolCalls[0] : undefined }); }; 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 7d6274c2..b0d5544c 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 @@ -8,10 +8,11 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; -import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; +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 { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper, extractToolsOnTextWrapper } from './extractGrammar.js'; +import { availableTools } from '../../common/prompt/prompts.js'; type InternalCommonMessageParams = { @@ -26,7 +27,7 @@ type InternalCommonMessageParams = { _setAborter: (aborter: () => void) => void; } -type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; } +type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; chatMode: ChatMode | null; } type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; } export type ListParams_Internal = ModelListParams @@ -123,7 +124,7 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError -const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendChatParams_Internal) => { +const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, chatMode }: SendChatParams_Internal) => { const { modelName, supportsSystemMessage, @@ -159,8 +160,13 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage onText = extractReasoningOnTextWrapper(onText, openSourceThinkTags) } - if () - onText = extractToolsOnTextWrapper(onText,) + // manually parse out tool results + if (chatMode) { + const tools = availableTools(chatMode) + if (tools) { + onText = extractToolsOnTextWrapper(onText, tools) + } + } let fullReasoningSoFar = '' let fullTextSoFar = '' diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index c5e928c5..88bb1ad7 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -21,6 +21,7 @@ export const sendLLMMessage = ({ settingsOfProvider, modelSelection, modelSelectionOptions, + chatMode, }: SendLLMMessageParams, metricsService: IMetricsService @@ -107,7 +108,7 @@ export const sendLLMMessage = ({ } const { sendFIM, sendChat } = implementation if (messagesType === 'chatMessages') { - sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions }) + sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, chatMode }) return } if (messagesType === 'FIMMessage') {