diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index ff32ab430c2..87ff36dee6b 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -16,6 +16,11 @@ import { createQDeveloperStreamingClient } from '../../../../shared/clients/qDev import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWrittenCodeTracker' import { PromptMessage } from '../../../controllers/chat/model' +export type ToolUseWithError = { + toolUse: ToolUse + error: Error | undefined +} + export class ChatSession { private sessionId?: string /** @@ -24,7 +29,7 @@ export class ChatSession { * _context = Additional context to be passed to the LLM for generating the response */ private _readFiles: string[] = [] - private _toolUse: ToolUse | undefined + private _toolUseWithError: ToolUseWithError | undefined private _showDiffOnFileWrite: boolean = false private _context: PromptMessage['context'] private _pairProgrammingModeOn: boolean = true @@ -49,12 +54,12 @@ export class ChatSession { this._pairProgrammingModeOn = pairProgrammingModeOn } - public get toolUse(): ToolUse | undefined { - return this._toolUse + public get toolUseWithError(): ToolUseWithError | undefined { + return this._toolUseWithError } - public setToolUse(toolUse: ToolUse | undefined) { - this._toolUse = toolUse + public setToolUseWithError(toolUseWithError: ToolUseWithError | undefined) { + this._toolUseWithError = toolUseWithError } public get context(): PromptMessage['context'] { diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index fb3b81515a7..32ab4968aed 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -675,69 +675,6 @@ export class ChatController { telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' }) } - private async processUnavailableToolUseMessage(message: CustomFormActionMessage) { - const tabID = message.tabID - if (!tabID) { - return - } - this.editorContextExtractor - .extractContextForTrigger('ChatMessage') - .then(async (context) => { - const triggerID = randomUUID() - this.triggerEventsStorage.addTriggerEvent({ - id: triggerID, - tabID: message.tabID, - message: undefined, - type: 'chat_message', - context, - }) - const session = this.sessionStorage.getSession(tabID) - const toolUse = session.toolUse - if (!toolUse || !toolUse.input) { - return - } - session.setToolUse(undefined) - - const toolResults: ToolResult[] = [] - - toolResults.push({ - content: [{ text: 'This tool is not an available tool in this mode' }], - toolUseId: toolUse.toolUseId, - status: ToolResultStatus.ERROR, - }) - - await this.generateResponse( - { - message: '', - trigger: ChatTriggerType.ChatMessage, - query: undefined, - codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock, - fileText: context?.focusAreaContext?.extendedCodeBlock ?? '', - fileLanguage: context?.activeFileContext?.fileLanguage, - filePath: context?.activeFileContext?.filePath, - matchPolicy: context?.activeFileContext?.matchPolicy, - codeQuery: context?.focusAreaContext?.names, - userIntent: undefined, - customization: getSelectedCustomization(), - toolResults: toolResults, - origin: Origin.IDE, - context: session.context ?? [], - relevantTextDocuments: [], - additionalContents: [], - documentReferences: [], - useRelevantDocuments: false, - contextLengths: { - ...defaultContextLengths, - }, - }, - triggerID - ) - }) - .catch((e) => { - this.processException(e, tabID) - }) - } - private async processToolUseMessage(message: CustomFormActionMessage) { const tabID = message.tabID if (!tabID) { @@ -756,59 +693,69 @@ export class ChatController { }) this.messenger.sendAsyncEventProgress(tabID, true, '') const session = this.sessionStorage.getSession(tabID) - const toolUse = session.toolUse - if (!toolUse || !toolUse.input) { + const toolUseWithError = session.toolUseWithError + if (!toolUseWithError || !toolUseWithError.toolUse || !toolUseWithError.toolUse.input) { // Turn off AgentLoop flag if there's no tool use this.sessionStorage.setAgentLoopInProgress(tabID, false) return } - session.setToolUse(undefined) + session.setToolUseWithError(undefined) + const toolUse = toolUseWithError.toolUse + const toolUseError = toolUseWithError.error const toolResults: ToolResult[] = [] - const result = ToolUtils.tryFromToolUse(toolUse) - if ('type' in result) { - const tool: Tool = result - - try { - await ToolUtils.validate(tool) - - const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, { - requiresAcceptance: false, - }) - const output = await ToolUtils.invoke(tool, chatStream) - if (output.output.content.length > maxToolOutputCharacterLength) { - throw Error( - `Tool output exceeds maximum character limit of ${maxToolOutputCharacterLength}` - ) + if (toolUseError) { + toolResults.push({ + content: [{ text: toolUseError.message }], + toolUseId: toolUse.toolUseId, + status: ToolResultStatus.ERROR, + }) + } else { + const result = ToolUtils.tryFromToolUse(toolUse) + if ('type' in result) { + const tool: Tool = result + + try { + await ToolUtils.validate(tool) + + const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, { + requiresAcceptance: false, + }) + const output = await ToolUtils.invoke(tool, chatStream) + if (output.output.content.length > maxToolOutputCharacterLength) { + throw Error( + `Tool output exceeds maximum character limit of ${maxToolOutputCharacterLength}` + ) + } + + toolResults.push({ + content: [ + output.output.kind === OutputKind.Text + ? { text: output.output.content } + : { json: output.output.content }, + ], + toolUseId: toolUse.toolUseId, + status: ToolResultStatus.SUCCESS, + }) + } catch (e: any) { + toolResults.push({ + content: [{ text: e.message }], + toolUseId: toolUse.toolUseId, + status: ToolResultStatus.ERROR, + }) } - - toolResults.push({ - content: [ - output.output.kind === OutputKind.Text - ? { text: output.output.content } - : { json: output.output.content }, - ], - toolUseId: toolUse.toolUseId, - status: ToolResultStatus.SUCCESS, - }) - } catch (e: any) { - toolResults.push({ - content: [{ text: e.message }], - toolUseId: toolUse.toolUseId, - status: ToolResultStatus.ERROR, - }) + } else { + const toolResult: ToolResult = result + toolResults.push(toolResult) } - } else { - const toolResult: ToolResult = result - toolResults.push(toolResult) - } - if (toolUse.name === ToolType.FsWrite) { - await vscode.commands.executeCommand( - 'vscode.open', - vscode.Uri.file((toolUse.input as unknown as FsWriteParams).path) - ) + if (toolUse.name === ToolType.FsWrite) { + await vscode.commands.executeCommand( + 'vscode.open', + vscode.Uri.file((toolUse.input as unknown as FsWriteParams).path) + ) + } } await this.generateResponse( @@ -879,9 +826,6 @@ export class ChatController { case 'reject-shell-command': await this.rejectShellCommand(message) break - case 'tool-unavailable': - await this.processUnavailableToolUseMessage(message) - break default: getLogger().warn(`Unhandled action: ${message.action.id}`) } @@ -920,11 +864,11 @@ export class ChatController { await fs.mkdir(resultArtifactsDir) const tempFilePath = path.join( resultArtifactsDir, - `temp-${path.basename((session.toolUse?.input as unknown as FsWriteParams).path)}` + `temp-${path.basename((session.toolUseWithError?.toolUse.input as unknown as FsWriteParams).path)}` ) // If we have existing filePath copy file content from existing file to temporary file. - const filePath = (session.toolUse?.input as any).path ?? message.filePath + const filePath = (session.toolUseWithError?.toolUse.input as any).path ?? message.filePath const fileExists = await fs.existsFile(filePath) if (fileExists) { const fileContent = await fs.readFileText(filePath) @@ -932,7 +876,7 @@ export class ChatController { } // Create a deep clone of the toolUse object and pass this toolUse to FsWrite tool execution to get the modified temporary file. - const clonedToolUse = structuredClone(session.toolUse) + const clonedToolUse = structuredClone(session.toolUseWithError?.toolUse) if (!clonedToolUse) { return } diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index ae245180124..e3e097681a3 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -240,49 +240,56 @@ export class Messenger { toolUse.input = JSON.parse(toolUseInput) toolUse.toolUseId = cwChatEvent.toolUseEvent.toolUseId ?? '' toolUse.name = cwChatEvent.toolUseEvent.name ?? '' - session.setToolUse(toolUse) - const availableToolsNames = (session.pairProgrammingModeOn ? tools : noWriteTools).map( - (item) => item.toolSpecification?.name - ) - if (!availableToolsNames.includes(toolUse.name)) { - this.dispatcher.sendCustomFormActionMessage( - new CustomFormActionMessage(tabID, { - id: 'tool-unavailable', - }) + let toolError = undefined + try { + const availableToolsNames = (session.pairProgrammingModeOn ? tools : noWriteTools).map( + (item) => item.toolSpecification?.name ) - return - } - - const tool = ToolUtils.tryFromToolUse(toolUse) - if ('type' in tool) { - let changeList: Change[] | undefined = undefined - if (tool.type === ToolType.FsWrite) { - session.setShowDiffOnFileWrite(true) - changeList = await tool.tool.getDiffChanges() + if (!availableToolsNames.includes(toolUse.name)) { + throw new Error(`Tool ${toolUse.name} is not available in the current mode`) } - const validation = ToolUtils.requiresAcceptance(tool) - const chatStream = new ChatStream(this, tabID, triggerID, toolUse, validation, changeList) - await ToolUtils.queueDescription(tool, chatStream) - - if (!validation.requiresAcceptance) { - // Need separate id for read tool and safe bash command execution as 'run-shell-command' id is required to state in cwChatConnector.ts which will impact generic tool execution. - if (tool.type === ToolType.ExecuteBash) { - this.dispatcher.sendCustomFormActionMessage( - new CustomFormActionMessage(tabID, { - id: 'run-shell-command', - }) - ) - } else { - this.dispatcher.sendCustomFormActionMessage( - new CustomFormActionMessage(tabID, { - id: 'generic-tool-execution', - }) - ) + const tool = ToolUtils.tryFromToolUse(toolUse) + if ('type' in tool) { + let changeList: Change[] | undefined = undefined + if (tool.type === ToolType.FsWrite) { + session.setShowDiffOnFileWrite(true) + changeList = await tool.tool.getDiffChanges() + } + const validation = ToolUtils.requiresAcceptance(tool) + const chatStream = new ChatStream( + this, + tabID, + triggerID, + toolUse, + validation, + changeList + ) + await ToolUtils.queueDescription(tool, chatStream) + + if (!validation.requiresAcceptance) { + // Need separate id for read tool and safe bash command execution as 'run-shell-command' id is required to state in cwChatConnector.ts which will impact generic tool execution. + if (tool.type === ToolType.ExecuteBash) { + this.dispatcher.sendCustomFormActionMessage( + new CustomFormActionMessage(tabID, { + id: 'run-shell-command', + }) + ) + } else { + this.dispatcher.sendCustomFormActionMessage( + new CustomFormActionMessage(tabID, { + id: 'generic-tool-execution', + }) + ) + } } + } else { + toolError = new Error('Tool not found') } - } else { - // TODO: Handle the error + } catch (error: any) { + toolError = error + } finally { + session.setToolUseWithError({ toolUse, error: toolError }) } } else if (cwChatEvent.toolUseEvent?.stop === undefined && toolUseInput !== '') { // This is for the case when writing tool is executed. The toolUseEvent is non stop but in toolUseInput is not empty. In this case we need show user the current spinner UI. diff --git a/packages/core/src/shared/utilities/messageUtil.ts b/packages/core/src/shared/utilities/messageUtil.ts index f2cefe824de..6cd811c4516 100644 --- a/packages/core/src/shared/utilities/messageUtil.ts +++ b/packages/core/src/shared/utilities/messageUtil.ts @@ -13,7 +13,7 @@ export interface MessageErrorInfo { } export function extractErrorInfo(error: any): MessageErrorInfo { - let errorMessage = 'Error reading chat stream.' + let errorMessage = 'Error reading chat response stream: ' + error.message let statusCode = undefined let requestId = undefined