Skip to content

Commit fd6ffcb

Browse files
authored
fix: abandon requests with invalid toolResults (#1274)
1 parent 16d6a9d commit fd6ffcb

File tree

2 files changed

+56
-9
lines changed

2 files changed

+56
-9
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,15 @@ export class AgenticChatController implements ChatHandlers {
547547

548548
// Fix the history to maintain invariants
549549
if (currentMessage) {
550-
this.#chatHistoryDb.fixHistory(tabId, currentMessage, conversationIdentifier ?? '')
550+
const isHistoryValid = this.#chatHistoryDb.fixHistory(
551+
tabId,
552+
currentMessage,
553+
conversationIdentifier ?? ''
554+
)
555+
if (!isHistoryValid) {
556+
this.#features.logging.warn('Skipping request due to invalid tool result/tool use relationship')
557+
break
558+
}
551559
}
552560

553561
// Retrieve the history from DB; Do not include chatHistory for requests going to Mynah Backend

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -368,21 +368,21 @@ export class ChatDatabase {
368368
* 5. If the last message is from the assistant and it contains tool uses, and a next user
369369
* message is set without tool results, then the user message will have cancelled tool results.
370370
*/
371-
fixHistory(tabId: string, newUserMessage: ChatMessage, conversationId: string): void {
371+
fixHistory(tabId: string, newUserMessage: ChatMessage, conversationId: string): boolean {
372372
if (!this.#initialized) {
373-
return
373+
return true
374374
}
375375
const historyId = this.#historyIdMapping.get(tabId)
376376
this.#features.logging.info(`Fixing history: tabId=${tabId}, historyId=${historyId || 'undefined'}`)
377377

378378
if (!historyId) {
379-
return
379+
return true
380380
}
381381

382382
const tabCollection = this.#db.getCollection<Tab>(TabCollection)
383383
const tabData = tabCollection.findOne({ historyId })
384384
if (!tabData) {
385-
return
385+
return true
386386
}
387387

388388
let allMessages = tabData.conversations.flatMap((conversation: Conversation) => conversation.messages)
@@ -401,7 +401,7 @@ export class ChatDatabase {
401401
this.ensureValidMessageSequence(allMessages)
402402

403403
// If the last message is from the assistant and it contains tool uses, and a next user message is set without tool results, then the user message will have cancelled tool results.
404-
this.handleToolUses(allMessages, newUserMessage)
404+
const isValid = this.validateToolUses(allMessages, newUserMessage)
405405
const clientType = this.#features.lsp.getClientInitializeParams()?.clientInfo?.name || 'unknown'
406406

407407
tabData.conversations = [
@@ -414,6 +414,7 @@ export class ChatDatabase {
414414
tabData.updatedAt = new Date()
415415
tabCollection.update(tabData)
416416
this.#features.logging.info(`Updated tab data in collection`)
417+
return isValid
417418
}
418419

419420
private trimHistoryToMaxLength(messages: Message[]): Message[] {
@@ -611,20 +612,57 @@ export class ChatDatabase {
611612
}
612613
}
613614

614-
private handleToolUses(messages: Message[], newUserMessage: ChatMessage): void {
615+
private validateToolUses(messages: Message[], newUserMessage: ChatMessage): boolean {
615616
if (messages.length === 0) {
616617
if (newUserMessage.userInputMessage?.userInputMessageContext?.toolResults) {
617618
this.#features.logging.debug('No history message found, but new user message has tool results.')
618619
newUserMessage.userInputMessage.userInputMessageContext.toolResults = undefined
619620
// tool results are empty, so content must not be empty
620621
newUserMessage.userInputMessage.content = 'Conversation history was too large, so it was cleared.'
621622
}
622-
return
623+
return true
623624
}
624625

625626
const lastMsg = messages[messages.length - 1]
627+
const toolResults = newUserMessage.userInputMessage?.userInputMessageContext?.toolResults
628+
629+
if (toolResults && toolResults.length > 0) {
630+
// If last message has no tool uses but new message has tool results, this is invalid
631+
if (!lastMsg.toolUses || lastMsg.toolUses.length === 0) {
632+
this.#features.logging.warn('New message has tool results but last message has no tool uses')
633+
return false
634+
}
635+
636+
const toolUseIds = new Set(lastMsg.toolUses.map(toolUse => toolUse.toolUseId))
637+
const validToolResults = toolResults.filter(toolResult => toolUseIds.has(toolResult.toolUseId))
638+
const invalidToolResults = toolResults.filter(toolResult => !toolUseIds.has(toolResult.toolUseId))
639+
640+
if (invalidToolResults.length > 0) {
641+
this.#features.logging.warn(
642+
`Found ${invalidToolResults.length} tool results without matching tool uses, marking them as cancelled`
643+
)
644+
645+
// Mark invalid tool results as cancelled
646+
for (const invalidResult of invalidToolResults) {
647+
invalidResult.status = ToolResultStatus.ERROR
648+
invalidResult.content = [
649+
{
650+
text: 'Tool use was cancelled by the user',
651+
},
652+
]
653+
}
654+
655+
// Update the tool results in the message
656+
if (newUserMessage.userInputMessage?.userInputMessageContext) {
657+
newUserMessage.userInputMessage.userInputMessageContext.toolResults = [
658+
...validToolResults,
659+
...invalidToolResults,
660+
]
661+
}
662+
}
663+
}
664+
626665
if (lastMsg.toolUses && lastMsg.toolUses.length > 0) {
627-
const toolResults = newUserMessage.userInputMessage?.userInputMessageContext?.toolResults
628666
if (!toolResults || toolResults.length === 0) {
629667
this.#features.logging.debug(
630668
`No tools results in last user message following a tool use message from assisstant, marking as canceled`
@@ -645,5 +683,6 @@ export class ChatDatabase {
645683
}
646684
}
647685
}
686+
return true
648687
}
649688
}

0 commit comments

Comments
 (0)