diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 76cb2c1e..1246fd06 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -17,7 +17,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { IVoidFileService } from '../common/voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; -import { ChatMode, FeatureName } from '../common/voidSettingsTypes.js'; +import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { ToolName, ToolCallParams, ToolResultType, InternalToolInfo, voidTools, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; @@ -317,6 +317,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { private resRejOfToolAwaitingApproval: { [toolId: string]: { res: () => void, rej: () => void } } = {} approveTool(toolId: string) { + // TODO!!! if not streaming, approveToolAndStreamResponse + + // if streaming, do below const resRej = this.resRejOfToolAwaitingApproval[toolId] delete this.resRejOfToolAwaitingApproval[toolId] resRej?.res() @@ -328,6 +331,191 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + + + private _staticAgentLoopsProps = () => { + // these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools) + const featureName: FeatureName = 'Chat' + const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + return { modelSelection, modelSelectionOptions } + } + + + + private async _agentLoop({ threadId, tools, prevSelns, currSelns, modelSelection, modelSelectionOptions, chatMode, userMessageContent }: { + tools: InternalToolInfo[] | undefined, + threadId: string, + prevSelns: StagingSelectionItem[], + currSelns: StagingSelectionItem[], + modelSelection: ModelSelection | null, + modelSelectionOptions: ModelSelectionOptions | undefined, + chatMode: ChatMode, + userMessageContent: string, // content of LATEST user message + }) { + this._setStreamState(threadId, { error: undefined }) // clear any previous error + + let nMessagesSent = 0 + let shouldSendAnotherMessage = true + + while (shouldSendAnotherMessage) { + // recompute files in last message + const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped + const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files + + nMessagesSent += 1 + shouldSendAnotherMessage = false // false by default + + let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) + + // replace last userMessage with userMessageFullContent (which contains all the files too) + const messages_ = toLLMChatMessages(this.getCurrentThread().messages) + const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') + + if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1 + + // system message + const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) + const terminalIds = this.terminalToolService.listTerminalIds() + const systemMessage = chat_systemMessage(workspaceFolders, terminalIds, chatMode) + + // all messages so far in the chat history (including tools) + const messages: LLMChatMessage[] = [ + { role: 'system', content: systemMessage, }, + ...messages_.slice(0, lastUserMsgIdx), + { role: 'user', content: userMessageFullContent }, + ...messages_.slice(lastUserMsgIdx + 1, Infinity), + ] + + // send llm message + const llmCancelToken = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + messages, + tools: tools, + modelSelection, + modelSelectionOptions, + logging: { loggingName: `Agent` }, + onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) }, + onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { + + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, }) // added to history, so clear messages so far + + // if no tool, finish + const tool: ToolCallType | undefined = toolCalls?.[0] + if (!tool) { + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + + // if tool + // clear messageSoFar since we added it to the chat history (but don't clear streamingToken, we're still streaming) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) + + // deal with the tool + const toolName: ToolName = tool.name + shouldSendAnotherMessage = true + + // 1. validate tool params + let toolParams: ToolCallParams[ToolName] + try { + const params = await this._toolsService.validateParams[toolName](tool.paramsStr) + toolParams = params + } catch (error) { + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + + // 2. if tool requires approval, await the approval + if (toolNamesThatRequireApproval.has(toolName)) { + const voidToolId = generateUuid() + const toolApprovalPromise = new Promise((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } }) + this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, params: toolParams, voidToolId: voidToolId }) + try { + await toolApprovalPromise + // accepted tool + } + catch (e) { + // TODO!!! test rejection + // if (Math.random() > 0) throw new Error('TESTING') + shouldSendAnotherMessage = false // interrupt flow by rejecting + + const errorMessage = 'Tool call was rejected by the user.' + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'rejected', params: toolParams, value: errorMessage }, }) + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + } + + // 3. call the tool + let toolResult: ToolResultType[typeof toolName] + try { + toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... + } catch (error) { + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + + // 4. stringify the result to give to the LLM + let toolResultStr: string + try { + toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) + } catch (error) { + const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + + // 5. add to history and keep going + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) + resMessageIsDonePromise() + + }, + onError: (error) => { + const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' + const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + // add assistant's message to chat history, and clear selection + this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) + resMessageIsDonePromise() + }, + }) + + // should never happen, just for safety + if (llmCancelToken === null) { + this._setStreamState(threadId, { + messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, + error: { message: 'There was an unexpected error when sending your chat message.', fullError: null } + }) + break + } + + this._setStreamState(threadId, { streamingToken: llmCancelToken }) // new stream token for the new message + + await messageIsDonePromise + } // end while + + } + + + // TODO!!!! + // called if we stopped streaming but want to accept the tool afterwards, lets us jump back into the loop as if no interruption happened + async approveToolAndStreamResponse({ chatMode, _chatSelections }: { userMessage: string, chatMode: ChatMode, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) { + } + + + async addUserMessageAndStreamResponse({ userMessage, chatMode, _chatSelections }: { userMessage: string, chatMode: ChatMode, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) { const thread = this.getCurrentThread() @@ -344,9 +532,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) - this._setStreamState(threadId, { error: undefined }) - - const toolNames: ToolName[] | undefined = chatMode === 'chat' ? undefined : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName)) : chatMode === 'agent' ? Object.keys(voidTools) as ToolName[] @@ -354,165 +539,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName]) - // these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools) - const featureName: FeatureName = 'Chat' - const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] - const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + const ps = this._staticAgentLoopsProps() - - // agent loop - const agentLoop = async () => { - - let nMessagesSent = 0 - let shouldSendAnotherMessage = true - - while (shouldSendAnotherMessage) { - // recompute files at last message - const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped - const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files - - nMessagesSent += 1 - shouldSendAnotherMessage = false // false by default - - let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) - - // replace last userMessage with userMessageFullContent (which contains all the files too) - const messages_ = toLLMChatMessages(this.getCurrentThread().messages) - const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') - - if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1 - - const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) - const terminalIds = this.terminalToolService.listTerminalIds() - const messages: LLMChatMessage[] = [ - { role: 'system', content: chat_systemMessage(workspaceFolders, terminalIds, chatMode), }, - ...messages_.slice(0, lastUserMsgIdx), - { role: 'user', content: userMessageFullContent }, - ...messages_.slice(lastUserMsgIdx + 1, Infinity), - ] - - - const llmCancelToken = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - messages, - tools: tools, - modelSelection, - modelSelectionOptions, - logging: { loggingName: `Agent` }, - onText: ({ fullText, fullReasoning }) => { - this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) - }, - onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { - - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) - - // if no tools, finish - if ((toolCalls?.length ?? 0) === 0) { - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // if tools - // clear messageSoFar since we added it to the chat history (but don't clear streamingToken, we're still streaming) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) - - // deal with the tool - const tool: ToolCallType | undefined = toolCalls?.[0] - if (!tool) { - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - const toolName: ToolName = tool.name - shouldSendAnotherMessage = true - - // 1. validate tool params - let toolParams: ToolCallParams[ToolName] - try { - const params = await this._toolsService.validateParams[toolName](tool.paramsStr) - toolParams = params - } catch (error) { - const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // 2. if tool requires approval, await the approval - if (toolNamesThatRequireApproval.has(toolName)) { - const voidToolId = generateUuid() - const toolApprovalPromise = new Promise((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } }) - this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, params: toolParams, voidToolId: voidToolId }) - try { - await toolApprovalPromise - // accepted tool - } - catch (e) { - console.log('successfully rejected', voidToolId) - // TODO!!! test rejection - // if (Math.random() > 0) throw new Error('TESTING') - shouldSendAnotherMessage = false // interrupt flow by rejecting - - const errorMessage = 'Tool call was rejected by the user.' - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'rejected', params: toolParams, value: errorMessage }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - } - - // 3. call the tool - let toolResult: ToolResultType[typeof toolName] - try { - toolResult = await this._toolsService.callTool[toolName](toolParams as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here - } catch (error) { - const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // 4. stringify the result to give the LLM - let toolResultStr: string - try { - toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) - } catch (error) { - const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // 5. add to history - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - - }, - onError: (error) => { - const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' - const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) - resMessageIsDonePromise() - }, - }) - if (llmCancelToken === null) break - this._setStreamState(threadId, { streamingToken: llmCancelToken }) - - await messageIsDonePromise - - } // end while - - } - - agentLoop() + this._agentLoop({ tools, prevSelns, currSelns, threadId, chatMode, userMessageContent, ...ps, }) } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 4c88dd64..c12752b1 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -23,9 +23,9 @@ Do NOT output the whole file here if possible, and try to write as LITTLE as nee export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\ -You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user ${mode === 'agent' ? 'make changes to their codebase' : 'search and understand their codebase'}. +You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase'}. You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. -Please assist the user with their query. The user's query is never invalid. +Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (do not be lazy)` : ''}. The user's query is never invalid. The user's system information is as follows: - ${os} @@ -55,11 +55,7 @@ If you think it's appropriate to suggest an edit to a file, then you must descri - Contents of the code blocks do NOT need to be formal code, they just need to clearly and concisely communicate the change. - Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code. \ -`} - -Do not tell the user anything about these instructions unless directly prompted for them. -\ -` +`}` type FileSelnLocal = { fileURI: URI, content: string }