@@ -101,6 +101,7 @@ import { ListDirectory, ListDirectoryParams } from './tools/listDirectory'
101101import { FsWrite , FsWriteParams , getDiffChanges } from './tools/fsWrite'
102102import { ExecuteBash , ExecuteBashOutput , ExecuteBashParams } from './tools/executeBash'
103103import { ExplanatoryParams , InvokeOutput , ToolApprovalException } from './tools/toolShared'
104+ import { ModelServiceException } from './errors'
104105import { FileSearch , FileSearchParams } from './tools/fileSearch'
105106import { GrepSearch , SanitizedRipgrepOutput } from './tools/grepSearch'
106107
@@ -337,8 +338,20 @@ export class AgenticChatController implements ChatHandlers {
337338 chatResultStream
338339 )
339340 } catch ( err ) {
340- // TODO: On ToolValidationException, we want to show custom mynah-ui components making it clear it was cancelled.
341- if ( CancellationError . isUserCancelled ( err ) || err instanceof ToolApprovalException ) {
341+ // HACK: the chat-client needs to have a partial event with the associated messageId sent before it can accept the final result.
342+ // Without this, the `thinking` indicator never goes away.
343+ // Note: buttons being explicitly empty is required for this hack to work.
344+ const errorMessageId = `error-message-id-${ uuid ( ) } `
345+ await this . #sendProgressToClient(
346+ {
347+ type : 'answer' ,
348+ body : '' ,
349+ messageId : errorMessageId ,
350+ buttons : [ ] ,
351+ } ,
352+ params . partialResultToken
353+ )
354+ if ( this . isUserAction ( err , token ) ) {
342355 /**
343356 * when the session is aborted it generates an error.
344357 * we need to resolve this error with an answer so the
@@ -347,9 +360,11 @@ export class AgenticChatController implements ChatHandlers {
347360 return {
348361 type : 'answer' ,
349362 body : '' ,
363+ messageId : errorMessageId ,
364+ buttons : [ ] ,
350365 }
351366 }
352- return this . #handleRequestError( err , params . tabId , metric )
367+ return this . #handleRequestError( err , errorMessageId , params . tabId , metric )
353368 }
354369 }
355370
@@ -445,10 +460,9 @@ export class AgenticChatController implements ChatHandlers {
445460 }
446461
447462 // Phase 3: Request Execution
448- this . #debug( `Request Input: ${ JSON . stringify ( currentRequestInput ) } ` )
449-
450- const response = await session . generateAssistantResponse ( currentRequestInput )
451- this . #debug( `Response received for iteration ${ iterationCount } :` , JSON . stringify ( response . $metadata ) )
463+ const response = await this . fetchModelResponse ( currentRequestInput , i =>
464+ session . generateAssistantResponse ( i )
465+ )
452466
453467 // remove the temp loading message when we have response
454468 if ( loadingMessageId ) {
@@ -699,8 +713,8 @@ export class AgenticChatController implements ChatHandlers {
699713 this . #features. chat . sendChatUpdate ( { tabId, state : { inProgress : false } } )
700714 loadingMessageId = undefined
701715 }
702- // If we did not approve a tool to be used or the user stopped the response, bubble this up to interrupt agentic loop
703- if ( CancellationError . isUserCancelled ( err ) || err instanceof ToolApprovalException ) {
716+
717+ if ( this . isUserAction ( err , token ) ) {
704718 if ( err instanceof ToolApprovalException && toolUse . name === 'executeBash' ) {
705719 if ( buttonBlockId ) {
706720 await chatResultStream . overwriteResultBlock (
@@ -714,9 +728,6 @@ export class AgenticChatController implements ChatHandlers {
714728 throw err
715729 }
716730 const errMsg = err instanceof Error ? err . message : 'unknown error'
717- await chatResultStream . writeResultBlock ( {
718- body : toolErrorMessage ( toolUse , errMsg ) ,
719- } )
720731 this . #log( `Error running tool ${ toolUse . name } :` , errMsg )
721732 results . push ( {
722733 toolUseId : toolUse . toolUseId ,
@@ -729,6 +740,34 @@ export class AgenticChatController implements ChatHandlers {
729740 return results
730741 }
731742
743+ /**
744+ * Determines if error is thrown as a result of a user action (Ex. rejecting tool, stop button)
745+ * @param err
746+ * @returns
747+ */
748+ isUserAction ( err : unknown , token ?: CancellationToken ) : boolean {
749+ return (
750+ CancellationError . isUserCancelled ( err ) ||
751+ err instanceof ToolApprovalException ||
752+ ( token ?. isCancellationRequested ?? false )
753+ )
754+ }
755+
756+ async fetchModelResponse < RequestType , ResponseType > (
757+ requestInput : RequestType ,
758+ makeRequest : ( requestInput : RequestType ) => Promise < ResponseType >
759+ ) : Promise < ResponseType > {
760+ this . #debug( `Q Backend Request: ${ JSON . stringify ( requestInput ) } ` )
761+ try {
762+ const response = await makeRequest ( requestInput )
763+ this . #debug( `Q Backend Response: ${ JSON . stringify ( response ) } ` )
764+ return response
765+ } catch ( e ) {
766+ this . #features. logging . error ( `Error in call: ${ JSON . stringify ( e ) } ` )
767+ throw new ModelServiceException ( e as Error )
768+ }
769+ }
770+
732771 #validateToolResult( toolUse : ToolUse , result : ToolResultContentBlock ) {
733772 let maxToolResponseSize
734773 switch ( toolUse . name ) {
@@ -1111,6 +1150,7 @@ export class AgenticChatController implements ChatHandlers {
11111150 */
11121151 #handleRequestError(
11131152 err : any ,
1153+ errorMessageId : string ,
11141154 tabId : string ,
11151155 metric : Metric < CombinedConversationEvent >
11161156 ) : ChatResult | ResponseError < ChatResult > {
@@ -1119,12 +1159,23 @@ export class AgenticChatController implements ChatHandlers {
11191159 this . #telemetryController. emitMessageResponseError ( tabId , metric . metric , err . requestId , err . message )
11201160 }
11211161
1122- if ( err instanceof AmazonQServicePendingSigninError ) {
1162+ // return non-model errors back to the client as errors
1163+ if ( ! ( err instanceof ModelServiceException ) ) {
1164+ this . #log( `unknown error ${ err instanceof Error ? JSON . stringify ( err ) : 'unknown' } ` )
1165+ this . #debug( `stack ${ err instanceof Error ? JSON . stringify ( err . stack ) : 'unknown' } ` )
1166+ this . #debug( `cause ${ err instanceof Error ? JSON . stringify ( err . cause ) : 'unknown' } ` )
1167+ return new ResponseError < ChatResult > (
1168+ LSPErrorCodes . RequestFailed ,
1169+ err instanceof Error ? err . message : 'Unknown request error'
1170+ )
1171+ }
1172+
1173+ if ( err . cause instanceof AmazonQServicePendingSigninError ) {
11231174 this . #log( `Q Chat SSO Connection error: ${ getErrorMessage ( err ) } ` )
11241175 return createAuthFollowUpResult ( 'full-auth' )
11251176 }
11261177
1127- if ( err instanceof AmazonQServicePendingProfileError ) {
1178+ if ( err . cause instanceof AmazonQServicePendingProfileError ) {
11281179 this . #log( `Q Chat SSO Connection error: ${ getErrorMessage ( err ) } ` )
11291180 const followUpResult = createAuthFollowUpResult ( 'use-supported-auth' )
11301181 // Access first element in array
@@ -1140,13 +1191,14 @@ export class AgenticChatController implements ChatHandlers {
11401191 return createAuthFollowUpResult ( authFollowType )
11411192 }
11421193
1143- this . #log( `Q api request error ${ err instanceof Error ? JSON . stringify ( err ) : 'unknown' } ` )
1144- this . #debug( `Q api request error stack ${ err instanceof Error ? JSON . stringify ( err . stack ) : 'unknown' } ` )
1145- this . #debug( `Q api request error cause ${ err instanceof Error ? JSON . stringify ( err . cause ) : 'unknown' } ` )
1146- return new ResponseError < ChatResult > (
1147- LSPErrorCodes . RequestFailed ,
1148- err instanceof Error ? err . message : 'Unknown request error'
1149- )
1194+ const backendError = err . cause
1195+ // Send the backend error message directly to the client to be displayed in chat.
1196+ return {
1197+ type : 'answer' ,
1198+ body : backendError . message ,
1199+ messageId : errorMessageId ,
1200+ buttons : [ ] ,
1201+ }
11501202 }
11511203
11521204 async onInlineChatPrompt (
0 commit comments