Skip to content

Commit 04f3bf9

Browse files
authored
feat(chat): agentic chat loop stop with trigger ID for tracking conversation aws#6990
## Problem Currently stop button and new prompt only stop with UI changes but agentic loop continues on the background and new messages keep streaming in. ## Solution - Correcting the setup for the agentic loop. - This stops the agentic loop completely with multiple checks. - Trigger Id for each new user prompt linked to a token to track an agentic loop Conversation. - Added multiple cancellation checks to stop the loop once we hit the stop condition. - Stops the Execute Bash executions once stop is clicked
1 parent 1ae9789 commit 04f3bf9

File tree

14 files changed

+781
-95
lines changed

14 files changed

+781
-95
lines changed

packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,19 +285,18 @@ export class Connector extends BaseConnector {
285285
}
286286

287287
if (messageData.type === 'customFormActionMessage') {
288-
this.onCustomFormAction(messageData.tabID, messageData.messageId, messageData.action)
288+
this.onCustomFormAction(messageData.tabID, messageData.messageId, messageData.action, messageData.triggerId)
289289
return
290290
}
291291

292292
if (messageData.type === 'asyncEventProgressMessage') {
293-
const enableStopAction = false
294293
const isPromptInputDisabled = true
295294
this.onAsyncEventProgress(
296295
messageData.tabID,
297296
messageData.inProgress,
298297
messageData.message ?? undefined,
299298
messageData.messageId ?? undefined,
300-
enableStopAction,
299+
messageData.inProgress,
301300
isPromptInputDisabled
302301
)
303302
return
@@ -335,7 +334,8 @@ export class Connector extends BaseConnector {
335334
id: string
336335
text?: string | undefined
337336
formItemValues?: Record<string, string> | undefined
338-
}
337+
},
338+
triggerId: string
339339
) {
340340
if (action === undefined) {
341341
return
@@ -351,6 +351,7 @@ export class Connector extends BaseConnector {
351351
formSelectedValues: action.formItemValues,
352352
tabType: this.getTabType(),
353353
tabID: tabId,
354+
triggerId: triggerId,
354355
})
355356

356357
if (

packages/core/src/amazonq/webview/ui/connector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ export class Connector {
744744
tabType: 'cwc',
745745
})
746746
} else {
747-
this.cwChatConnector.onCustomFormAction(tabId, messageId ?? '', action)
747+
this.cwChatConnector.onCustomFormAction(tabId, messageId ?? '', action, messageId ?? '')
748748
}
749749
break
750750
case 'agentWalkthrough': {

packages/core/src/amazonq/webview/ui/main.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,13 @@ export const createMynahUI = (
719719
return
720720
}
721721

722+
// For new user prompt stopping chat with UI changes
723+
mynahUI.updateStore(tabID, {
724+
loadingChat: false,
725+
promptInputDisabledState: false,
726+
})
727+
connector.onStopChatResponse(tabID)
728+
722729
const tabType = tabsStorage.getTab(tabID)?.type
723730
if (tabType === 'featuredev') {
724731
mynahUI.addChatItem(tabID, {

packages/core/src/codewhispererChat/clients/chat/v0/chat.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWr
1717
import { DocumentReference, PromptMessage } from '../../../controllers/chat/model'
1818
import { FsWriteBackup } from '../../../../codewhispererChat/tools/fsWrite'
1919
import { randomUUID } from '../../../../shared/crypto'
20+
import { getLogger } from '../../../../shared/logger/logger'
2021

2122
export type ToolUseWithError = {
2223
toolUse: ToolUse
@@ -38,6 +39,8 @@ export class ChatSession {
3839
private _context: PromptMessage['context']
3940
private _pairProgrammingModeOn: boolean = true
4041
private _fsWriteBackups: Map<string, FsWriteBackup> = new Map()
42+
private _agenticLoopInProgress: boolean = false
43+
4144
/**
4245
* True if messages from local history have been sent to session.
4346
*/
@@ -68,6 +71,33 @@ export class ChatSession {
6871
this._messageIdToUpdateListDirectory = messageId
6972
}
7073

74+
public get agenticLoopInProgress(): boolean {
75+
return this._agenticLoopInProgress
76+
}
77+
78+
public setAgenticLoopInProgress(value: boolean) {
79+
// When setting agenticLoop to false (ending the loop), dispose the current token source
80+
if (this._agenticLoopInProgress === true && value === false) {
81+
this.disposeTokenSource()
82+
// Create a new token source for future operations
83+
this.createNewTokenSource()
84+
}
85+
this._agenticLoopInProgress = value
86+
}
87+
88+
/**
89+
* Safely disposes the current token source if it exists
90+
*/
91+
disposeTokenSource() {
92+
if (this.tokenSource) {
93+
try {
94+
this.tokenSource.dispose()
95+
} catch (error) {
96+
getLogger().debug(`Error disposing token source: ${error}`)
97+
}
98+
}
99+
}
100+
71101
public get pairProgrammingModeOn(): boolean {
72102
return this._pairProgrammingModeOn
73103
}

packages/core/src/codewhispererChat/controllers/chat/controller.ts

Lines changed: 94 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as path from 'path'
66
import * as vscode from 'vscode'
77
import { Event as VSCodeEvent, Uri, workspace, window, ViewColumn, Position, Selection } from 'vscode'
88
import { EditorContextExtractor } from '../../editor/context/extractor'
9+
import { ConversationTracker } from '../../storages/conversationTracker'
910
import { ChatSessionStorage } from '../../storages/chatSession'
1011
import { Messenger, MessengerResponseType, StaticTextResponseType } from './messenger/messenger'
1112
import {
@@ -414,8 +415,19 @@ export class ChatController {
414415
private async processStopResponseMessage(message: StopResponseMessage) {
415416
const session = this.sessionStorage.getSession(message.tabID)
416417
session.tokenSource.cancel()
418+
session.setAgenticLoopInProgress(false)
419+
session.setToolUseWithError(undefined)
420+
421+
// Mark any active triggers as cancelled when stopping the response
422+
const triggerEvents = this.triggerEventsStorage.getTriggerEventsByTabID(message.tabID)
423+
if (triggerEvents && triggerEvents.length > 0) {
424+
const conversationTracker = ConversationTracker.getInstance()
425+
for (const event of triggerEvents) {
426+
conversationTracker.cancelTrigger(event.id)
427+
}
428+
}
429+
417430
this.messenger.sendEmptyMessage(message.tabID, '', undefined)
418-
this.chatHistoryDb.clearRecentHistory(message.tabID)
419431
this.telemetryHelper.recordInteractionWithAgenticChat(AgenticChatInteractionType.StopChat, message)
420432
}
421433

@@ -472,6 +484,13 @@ export class ChatController {
472484
}
473485

474486
private async processTabCloseMessage(message: TabClosedMessage) {
487+
// First cancel any active triggers to stop ongoing operations
488+
const conversationTracker = ConversationTracker.getInstance()
489+
conversationTracker.cancelTabTriggers(message.tabID)
490+
491+
// Then clear all triggers to release resources
492+
conversationTracker.clearTabTriggers(message.tabID)
493+
475494
this.sessionStorage.deleteSession(message.tabID)
476495
this.triggerEventsStorage.removeTabEvents(message.tabID)
477496
// this.telemetryHelper.recordCloseChat(message.tabID)
@@ -686,7 +705,13 @@ export class ChatController {
686705
this.editorContextExtractor
687706
.extractContextForTrigger('ChatMessage')
688707
.then(async (context) => {
689-
const triggerID = randomUUID()
708+
const triggerID = message.triggerId
709+
710+
// Check if this trigger has already been cancelled
711+
if (this.isTriggerCancelled(triggerID)) {
712+
return
713+
}
714+
690715
this.triggerEventsStorage.addTriggerEvent({
691716
id: triggerID,
692717
tabID: message.tabID,
@@ -696,13 +721,16 @@ export class ChatController {
696721
})
697722
this.messenger.sendAsyncEventProgress(tabID, true, '')
698723
const session = this.sessionStorage.getSession(tabID)
724+
725+
// Check if the session has been cancelled before proceeding
726+
if (this.isTriggerCancelled(triggerID)) {
727+
getLogger().debug(`Tool execution cancelled for tabID: ${tabID}`)
728+
return
729+
}
730+
699731
const toolUseWithError = session.toolUseWithError
700-
getLogger().debug(
701-
`processToolUseMessage: ${toolUseWithError?.toolUse.name}:${toolUseWithError?.toolUse.toolUseId} with error: ${toolUseWithError?.error}`
702-
)
703732
if (!toolUseWithError || !toolUseWithError.toolUse) {
704-
// Turn off AgentLoop flag if there's no tool use
705-
this.sessionStorage.setAgentLoopInProgress(tabID, false)
733+
session.setAgenticLoopInProgress(false)
706734
return
707735
}
708736
session.setToolUseWithError(undefined)
@@ -742,7 +770,18 @@ export class ChatController {
742770
const backup = await tool.tool.getBackup()
743771
session.setFsWriteBackup(toolUse.toolUseId, backup)
744772
}
745-
const output = await ToolUtils.invoke(tool, chatStream)
773+
774+
// Check again if cancelled before invoking the tool
775+
if (this.isTriggerCancelled(triggerID)) {
776+
getLogger().debug(`Tool execution cancelled before invoke for tabID: ${tabID}`)
777+
return
778+
}
779+
780+
const output = await ToolUtils.invoke(
781+
tool,
782+
chatStream,
783+
ConversationTracker.getInstance().getTokenForTrigger(triggerID)
784+
)
746785
ToolUtils.validateOutput(output)
747786

748787
toolResults.push({
@@ -1039,7 +1078,17 @@ export class ChatController {
10391078

10401079
// Turn off AgentLoop flag in case of exception
10411080
if (tabID) {
1042-
this.sessionStorage.setAgentLoopInProgress(tabID, false)
1081+
const session = this.sessionStorage.getSession(tabID)
1082+
session.setAgenticLoopInProgress(false)
1083+
1084+
// Mark any active triggers as completed when there's an exception
1085+
const triggerEvents = this.triggerEventsStorage.getTriggerEventsByTabID(tabID)
1086+
if (triggerEvents && triggerEvents.length > 0) {
1087+
const conversationTracker = ConversationTracker.getInstance()
1088+
for (const event of triggerEvents) {
1089+
conversationTracker.cancelTrigger(event.id)
1090+
}
1091+
}
10431092
}
10441093

10451094
this.messenger.sendErrorMessage(errorMessage, tabID, requestID, statusCode)
@@ -1226,23 +1275,33 @@ export class ChatController {
12261275

12271276
private async processPromptMessageAsNewThread(message: PromptMessage) {
12281277
const session = this.sessionStorage.getSession(message.tabID)
1278+
// If there's an existing conversation, ensure we dispose the previous token
1279+
if (session.agenticLoopInProgress) {
1280+
session.disposeTokenSource()
1281+
}
1282+
1283+
// Create a fresh token for this new conversation
1284+
session.createNewTokenSource()
1285+
session.setAgenticLoopInProgress(true)
12291286
session.clearListOfReadFiles()
12301287
session.clearListOfReadFolders()
12311288
session.setShowDiffOnFileWrite(false)
12321289
this.editorContextExtractor
12331290
.extractContextForTrigger('ChatMessage')
12341291
.then(async (context) => {
12351292
const triggerID = randomUUID()
1293+
1294+
// Register the trigger ID with the token for cancellation tracking
1295+
const conversationTracker = ConversationTracker.getInstance()
1296+
conversationTracker.registerTrigger(triggerID, session.tokenSource, message.tabID)
1297+
12361298
this.triggerEventsStorage.addTriggerEvent({
12371299
id: triggerID,
12381300
tabID: message.tabID,
12391301
message: message.message,
12401302
type: 'chat_message',
12411303
context,
12421304
})
1243-
1244-
this.messenger.sendAsyncEventProgress(message.tabID, true, '')
1245-
12461305
await this.generateResponse(
12471306
{
12481307
message: message.message ?? '',
@@ -1438,16 +1497,6 @@ export class ChatController {
14381497
}
14391498

14401499
const tabID = triggerEvent.tabID
1441-
if (this.sessionStorage.isAgentLoopInProgress(tabID)) {
1442-
// If a response is already in progress, stop it first
1443-
const stopResponseMessage: StopResponseMessage = {
1444-
tabID: tabID,
1445-
}
1446-
await this.processStopResponseMessage(stopResponseMessage)
1447-
}
1448-
1449-
// Ensure AgentLoop flag is set to true during response generation
1450-
this.sessionStorage.setAgentLoopInProgress(tabID, true)
14511500

14521501
const credentialsState = await AuthUtil.instance.getChatAuthState()
14531502

@@ -1545,7 +1594,11 @@ export class ChatController {
15451594
session.setContext(triggerPayload.context)
15461595
}
15471596
this.messenger.sendInitalStream(tabID, triggerID)
1597+
this.messenger.sendAsyncEventProgress(tabID, true, '')
15481598
this.telemetryHelper.setConversationStreamStartTime(tabID)
1599+
if (this.isTriggerCancelled(triggerID)) {
1600+
return
1601+
}
15491602
if (isSsoConnection(AuthUtil.instance.conn)) {
15501603
const { $metadata, generateAssistantResponseResponse } = await session.chatSso(request)
15511604
response = {
@@ -1562,7 +1615,7 @@ export class ChatController {
15621615
this.telemetryHelper.recordEnterFocusConversation(triggerEvent.tabID)
15631616
this.telemetryHelper.recordStartConversation(triggerEvent, triggerPayload)
15641617

1565-
if (currentMessage && session.sessionIdentifier) {
1618+
if (currentMessage && session.sessionIdentifier && !this.isTriggerCancelled(triggerID)) {
15661619
this.chatHistoryDb.addMessage(tabID, 'cwc', session.sessionIdentifier, {
15671620
body: triggerPayload.message,
15681621
type: 'prompt' as any,
@@ -1577,22 +1630,14 @@ export class ChatController {
15771630
response.$metadata.requestId
15781631
} metadata: ${inspect(response.$metadata, { depth: 12 })}`
15791632
)
1580-
this.cancelTokenSource = new vscode.CancellationTokenSource()
1581-
await this.messenger.sendAIResponse(
1582-
response,
1583-
session,
1584-
tabID,
1585-
triggerID,
1586-
triggerPayload,
1587-
this.cancelTokenSource.token
1588-
)
15891633

1590-
// Turn off AgentLoop flag after sending the AI response
1591-
this.sessionStorage.setAgentLoopInProgress(tabID, false)
1634+
if (this.isTriggerCancelled(triggerID)) {
1635+
return
1636+
}
1637+
1638+
await this.messenger.sendAIResponse(response, session, tabID, triggerID, triggerPayload)
15921639
} catch (e: any) {
15931640
this.telemetryHelper.recordMessageResponseError(triggerPayload, tabID, getHttpStatusCode(e) ?? 0)
1594-
// Turn off AgentLoop flag in case of exception
1595-
this.sessionStorage.setAgentLoopInProgress(tabID, false)
15961641
// clears session, record telemetry before this call
15971642
this.processException(e, tabID)
15981643
}
@@ -1635,4 +1680,17 @@ export class ChatController {
16351680
return { relativeFilePath: filePath, lineRanges: mergedRanges }
16361681
})
16371682
}
1683+
1684+
/**
1685+
* Check if a trigger has been cancelled and should not proceed
1686+
* @param triggerId The trigger ID to check
1687+
* @returns true if the trigger is cancelled and should not proceed
1688+
*/
1689+
private isTriggerCancelled(triggerId: string): boolean {
1690+
if (!triggerId) {
1691+
return false
1692+
}
1693+
const conversationTracker = ConversationTracker.getInstance()
1694+
return conversationTracker.isTriggerCancelled(triggerId)
1695+
}
16381696
}

0 commit comments

Comments
 (0)