@@ -101,6 +101,7 @@ import { ListDirectory, ListDirectoryParams } from './tools/listDirectory'
101
101
import { FsWrite , FsWriteParams , getDiffChanges } from './tools/fsWrite'
102
102
import { ExecuteBash , ExecuteBashOutput , ExecuteBashParams } from './tools/executeBash'
103
103
import { ExplanatoryParams , InvokeOutput , ToolApprovalException } from './tools/toolShared'
104
+ import { ModelServiceException } from './errors'
104
105
import { FileSearch , FileSearchParams } from './tools/fileSearch'
105
106
import { GrepSearch , SanitizedRipgrepOutput } from './tools/grepSearch'
106
107
@@ -337,8 +338,20 @@ export class AgenticChatController implements ChatHandlers {
337
338
chatResultStream
338
339
)
339
340
} 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 ) ) {
342
355
/**
343
356
* when the session is aborted it generates an error.
344
357
* we need to resolve this error with an answer so the
@@ -347,9 +360,11 @@ export class AgenticChatController implements ChatHandlers {
347
360
return {
348
361
type : 'answer' ,
349
362
body : '' ,
363
+ messageId : errorMessageId ,
364
+ buttons : [ ] ,
350
365
}
351
366
}
352
- return this . #handleRequestError( err , params . tabId , metric )
367
+ return this . #handleRequestError( err , errorMessageId , params . tabId , metric )
353
368
}
354
369
}
355
370
@@ -445,10 +460,9 @@ export class AgenticChatController implements ChatHandlers {
445
460
}
446
461
447
462
// 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
+ )
452
466
453
467
// remove the temp loading message when we have response
454
468
if ( loadingMessageId ) {
@@ -699,8 +713,8 @@ export class AgenticChatController implements ChatHandlers {
699
713
this . #features. chat . sendChatUpdate ( { tabId, state : { inProgress : false } } )
700
714
loadingMessageId = undefined
701
715
}
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 ) ) {
704
718
if ( err instanceof ToolApprovalException && toolUse . name === 'executeBash' ) {
705
719
if ( buttonBlockId ) {
706
720
await chatResultStream . overwriteResultBlock (
@@ -714,9 +728,6 @@ export class AgenticChatController implements ChatHandlers {
714
728
throw err
715
729
}
716
730
const errMsg = err instanceof Error ? err . message : 'unknown error'
717
- await chatResultStream . writeResultBlock ( {
718
- body : toolErrorMessage ( toolUse , errMsg ) ,
719
- } )
720
731
this . #log( `Error running tool ${ toolUse . name } :` , errMsg )
721
732
results . push ( {
722
733
toolUseId : toolUse . toolUseId ,
@@ -729,6 +740,34 @@ export class AgenticChatController implements ChatHandlers {
729
740
return results
730
741
}
731
742
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
+
732
771
#validateToolResult( toolUse : ToolUse , result : ToolResultContentBlock ) {
733
772
let maxToolResponseSize
734
773
switch ( toolUse . name ) {
@@ -1111,6 +1150,7 @@ export class AgenticChatController implements ChatHandlers {
1111
1150
*/
1112
1151
#handleRequestError(
1113
1152
err : any ,
1153
+ errorMessageId : string ,
1114
1154
tabId : string ,
1115
1155
metric : Metric < CombinedConversationEvent >
1116
1156
) : ChatResult | ResponseError < ChatResult > {
@@ -1119,12 +1159,23 @@ export class AgenticChatController implements ChatHandlers {
1119
1159
this . #telemetryController. emitMessageResponseError ( tabId , metric . metric , err . requestId , err . message )
1120
1160
}
1121
1161
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 ) {
1123
1174
this . #log( `Q Chat SSO Connection error: ${ getErrorMessage ( err ) } ` )
1124
1175
return createAuthFollowUpResult ( 'full-auth' )
1125
1176
}
1126
1177
1127
- if ( err instanceof AmazonQServicePendingProfileError ) {
1178
+ if ( err . cause instanceof AmazonQServicePendingProfileError ) {
1128
1179
this . #log( `Q Chat SSO Connection error: ${ getErrorMessage ( err ) } ` )
1129
1180
const followUpResult = createAuthFollowUpResult ( 'use-supported-auth' )
1130
1181
// Access first element in array
@@ -1140,13 +1191,14 @@ export class AgenticChatController implements ChatHandlers {
1140
1191
return createAuthFollowUpResult ( authFollowType )
1141
1192
}
1142
1193
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
+ }
1150
1202
}
1151
1203
1152
1204
async onInlineChatPrompt (
0 commit comments