Skip to content

Commit e7aa2a6

Browse files
fix: improve history management (aws#2312) (aws#2357)
Co-authored-by: aws-toolkit-automation <[email protected]>
1 parent 5eb3768 commit e7aa2a6

File tree

3 files changed

+21
-93
lines changed

3 files changed

+21
-93
lines changed

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

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ import { AmazonQBaseServiceManager } from '../../shared/amazonQServiceManager/Ba
137137
import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/AmazonQTokenServiceManager'
138138
import { AmazonQWorkspaceConfig } from '../../shared/amazonQServiceManager/configurationUtils'
139139
import { TabBarController } from './tabBarController'
140-
import { ChatDatabase, MaxOverallCharacters, ToolResultValidationError } from './tools/chatDb/chatDb'
140+
import { ChatDatabase, ToolResultValidationError } from './tools/chatDb/chatDb'
141141
import {
142142
AgenticChatEventParser,
143143
ChatResultWithMetadata as AgenticChatResultWithMetadata,
@@ -187,6 +187,8 @@ import {
187187
DEFAULT_WINDOW_REJECT_SHORTCUT,
188188
DEFAULT_MACOS_STOP_SHORTCUT,
189189
DEFAULT_WINDOW_STOP_SHORTCUT,
190+
COMPACTION_CHARACTER_THRESHOLD,
191+
MAX_OVERALL_CHARACTERS,
190192
} from './constants/constants'
191193
import {
192194
AgenticChatError,
@@ -1179,8 +1181,7 @@ export class AgenticChatController implements ChatHandlers {
11791181
* Runs the compaction, making requests and processing tool uses until completion
11801182
*/
11811183
#shouldCompact(currentRequestCount: number): boolean {
1182-
// 80% of 570K limit
1183-
if (currentRequestCount > 456_000) {
1184+
if (currentRequestCount > COMPACTION_CHARACTER_THRESHOLD) {
11841185
this.#debug(`Current request total character count is: ${currentRequestCount}, prompting user to compact`)
11851186
return true
11861187
} else {
@@ -1225,7 +1226,7 @@ export class AgenticChatController implements ChatHandlers {
12251226
// Get and process the messages from history DB to maintain invariants for service requests
12261227
try {
12271228
const { history: historyMessages, historyCount: historyCharCount } =
1228-
this.#chatHistoryDb.fixAndGetHistory(tabId, conversationIdentifier ?? '', currentMessage, [])
1229+
this.#chatHistoryDb.fixAndGetHistory(tabId, currentMessage, [])
12291230
messages = historyMessages
12301231
characterCount = historyCharCount
12311232
} catch (err) {
@@ -1403,12 +1404,7 @@ export class AgenticChatController implements ChatHandlers {
14031404
history: historyMessages,
14041405
historyCount: historyCharacterCount,
14051406
currentCount: currentInputCount,
1406-
} = this.#chatHistoryDb.fixAndGetHistory(
1407-
tabId,
1408-
conversationId,
1409-
currentMessage,
1410-
pinnedContextMessages
1411-
)
1407+
} = this.#chatHistoryDb.fixAndGetHistory(tabId, currentMessage, pinnedContextMessages)
14121408
messages = historyMessages
14131409
currentRequestCount = currentInputCount + historyCharacterCount
14141410
this.#debug(`Request total character count: ${currentRequestCount}`)
@@ -2779,7 +2775,7 @@ export class AgenticChatController implements ChatHandlers {
27792775
body: COMPACTION_HEADER_BODY,
27802776
buttons,
27812777
} as any
2782-
const body = COMPACTION_BODY(Math.round((characterCount / MaxOverallCharacters) * 100))
2778+
const body = COMPACTION_BODY(Math.round((characterCount / MAX_OVERALL_CHARACTERS) * 100))
27832779
return {
27842780
type: 'tool',
27852781
messageId,

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { BedrockModel } from './modelSelection'
2-
31
// Error message constants
42
export const GENERIC_ERROR_MS = 'An unexpected error occurred, check the logs for more information.'
53
export const OUTPUT_LIMIT_EXCEEDS_PARTIAL_MSG = 'output exceeds maximum character limit of'
@@ -16,6 +14,10 @@ export const SERVICE_MANAGER_POLL_INTERVAL_MS = 100
1614
export const GENERATE_ASSISTANT_RESPONSE_INPUT_LIMIT = 500_000
1715

1816
// Compaction
17+
// Maximum number of characters per request used for compaction prompt
18+
// 200K tokens * 3.5 = 700K characters, intentionally overestimating with 3.5:1 ratio
19+
export const MAX_OVERALL_CHARACTERS = 700_000
20+
export const COMPACTION_CHARACTER_THRESHOLD = 0.7 * MAX_OVERALL_CHARACTERS
1921
export const COMPACTION_BODY = (threshold: number) =>
2022
`The context window is almost full (${threshold}%) and exceeding it will clear your history. Amazon Q can compact your history instead.`
2123
export const COMPACTION_HEADER_BODY = 'Compact chat history?'

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

Lines changed: 10 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ export class ToolResultValidationError extends Error {
4545
}
4646

4747
export const EMPTY_CONVERSATION_LIST_ID = 'empty'
48-
// Maximum number of characters to keep in request
49-
// (200K tokens - 8K output tokens - 2k system prompt) * 3 = 570K characters, intentionally overestimating with 3:1 ratio
50-
export const MaxOverallCharacters = 570_000
51-
// Maximum number of history messages to include in each request to the LLM
52-
const maxConversationHistoryMessages = 250
5348

5449
/**
5550
* A singleton database class that manages chat history persistence using LokiJS.
@@ -708,15 +703,12 @@ export class ChatDatabase {
708703

709704
/**
710705
* Prepare the history messages for service request and fix the persisted history in DB to maintain the following invariants:
711-
* 1. The history contains at most MaxConversationHistoryMessages messages. Oldest messages are dropped.
712-
* 2. The first message is from the user and without any tool usage results, and the last message is from the assistant.
706+
* 1. The first message is from the user and without any tool usage results, and the last message is from the assistant.
713707
* The history contains alternating sequene of userMessage followed by assistantMessages
714-
* 3. The toolUse and toolResult relationship is valid
715-
* 4. The history character length is <= MaxConversationHistoryCharacters - newUserMessageCharacterCount. Oldest messages are dropped.
708+
* 2. The toolUse and toolResult relationship is valid
716709
*/
717710
fixAndGetHistory(
718711
tabId: string,
719-
conversationId: string,
720712
newUserMessage: ChatMessage,
721713
pinnedContextMessages: ChatMessage[]
722714
): MessagesWithCharacterCount {
@@ -732,18 +724,19 @@ export class ChatDatabase {
732724

733725
this.#features.logging.info(`Fixing history: tabId=${tabId}`)
734726

735-
// 1. Make sure the length of the history messages don't exceed MaxConversationHistoryMessages
736-
let allMessages = this.getMessages(tabId, maxConversationHistoryMessages)
727+
let allMessages = this.getMessages(tabId)
737728
if (allMessages.length > 0) {
738-
// 2. Fix history: Ensure messages in history is valid for server side checks
729+
// 1. Fix history: Ensure messages in history is valid for server side checks
739730
this.ensureValidMessageSequence(tabId, allMessages)
740731

741-
// 3. Fix new user prompt: Ensure lastMessage in history toolUse and newMessage toolResult relationship is valid
732+
// 2. Fix new user prompt: Ensure lastMessage in history toolUse and newMessage toolResult relationship is valid
742733
this.validateAndFixNewMessageToolResults(allMessages, newUserMessage)
743734

744-
// 4. NOTE: Keep this trimming logic at the end of the preprocess.
745-
// Make sure max characters ≤ remaining Character Budget, must be put at the end of preprocessing
746-
messagesWithCount = this.trimMessagesToMaxLength(allMessages, newUserInputCount, tabId, conversationId)
735+
messagesWithCount = {
736+
history: allMessages,
737+
historyCount: this.calculateMessagesCharacterCount(allMessages),
738+
currentCount: newUserInputCount,
739+
}
747740

748741
// Edge case: If the history is empty and the next message contains tool results, then we have to just abandon them.
749742
if (
@@ -772,74 +765,11 @@ export class ChatDatabase {
772765
return messagesWithCount
773766
}
774767

775-
/**
776-
* Finds a suitable "break point" index in the message sequence.
777-
*
778-
* It ensures that the "break point" is at a clean conversation boundary where:
779-
* 1. The message is from a user (type === 'prompt')
780-
* 2. The message doesn't contain tool results that would break tool use/result pairs
781-
* 3. The message has a non-empty body
782-
*
783-
* @param allMessages The array of conversation messages to search through
784-
* @returns The index to trim from, or undefined if no suitable trimming point is found
785-
*/
786-
private findIndexToTrim(allMessages: Message[]): number | undefined {
787-
for (let i = 2; i < allMessages.length; i++) {
788-
const message = allMessages[i]
789-
if (message.type === ('prompt' as ChatItemType) && this.isValidUserMessageWithoutToolResults(message)) {
790-
return i
791-
}
792-
}
793-
return undefined
794-
}
795-
796768
private isValidUserMessageWithoutToolResults(message: Message): boolean {
797769
const ctx = message.userInputMessageContext
798770
return !!ctx && (!ctx.toolResults || ctx.toolResults.length === 0) && message.body !== ''
799771
}
800772

801-
private trimMessagesToMaxLength(
802-
messages: Message[],
803-
newUserInputCount: number,
804-
tabId: string,
805-
conversationId: string
806-
): MessagesWithCharacterCount {
807-
let historyCharacterCount = this.calculateMessagesCharacterCount(messages)
808-
const maxHistoryCharacterSize = Math.max(0, MaxOverallCharacters - newUserInputCount)
809-
let trimmedHistory = false
810-
this.#features.logging.debug(
811-
`Current history character count: ${historyCharacterCount}, remaining history character budget: ${maxHistoryCharacterSize}`
812-
)
813-
while (historyCharacterCount > maxHistoryCharacterSize && messages.length > 2) {
814-
trimmedHistory = true
815-
// Find the next valid user message to start from
816-
const indexToTrim = this.findIndexToTrim(messages)
817-
if (indexToTrim !== undefined && indexToTrim > 0) {
818-
this.#features.logging.debug(
819-
`Removing the first ${indexToTrim} elements in the history due to character count limit`
820-
)
821-
messages.splice(0, indexToTrim)
822-
} else {
823-
this.#features.logging.debug(
824-
'Could not find a valid point to trim, reset history to reduce character count'
825-
)
826-
this.replaceHistory(tabId, 'cwc', conversationId, [])
827-
return { history: [], historyCount: 0, currentCount: newUserInputCount }
828-
}
829-
historyCharacterCount = this.calculateMessagesCharacterCount(messages)
830-
this.#features.logging.debug(`History character count post trimming: ${historyCharacterCount}`)
831-
}
832-
833-
if (trimmedHistory) {
834-
this.replaceHistory(tabId, 'cwc', conversationId, messages)
835-
}
836-
return {
837-
history: messages,
838-
historyCount: historyCharacterCount,
839-
currentCount: newUserInputCount,
840-
}
841-
}
842-
843773
private calculateToolSpecCharacterCount(currentMessage: ChatMessage): number {
844774
let count = 0
845775
if (currentMessage.userInputMessage?.userInputMessageContext?.tools) {

0 commit comments

Comments
 (0)