Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/core/sliding-window/__tests__/sliding-window.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,154 @@ describe("Sliding Window", () => {
// Clean up
summarizeSpy.mockRestore()
})
describe("Token-based thresholds", () => {
// Helper function to create messages with specific token counts
const createMessages = (count: number, tokensPerMessage: number): ApiMessage[] => {
const messages: ApiMessage[] = []
for (let i = 0; i < count; i++) {
const role = i % 2 === 0 ? "user" : "assistant"
// Create content that roughly corresponds to the desired token count
// This is a simplification - actual token count depends on the tokenizer
const content = "x".repeat(tokensPerMessage * 4) // Rough approximation
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth adding a comment explaining this approximation? The 4 characters per token ratio is a testing simplification that future maintainers might find helpful to understand.

messages.push({ role: role as "user" | "assistant", content })
}
return messages
}

it("should trigger condensing when token threshold is reached", async () => {
vi.clearAllMocks()
const mockCost = 0.05
const mockSummarizeResponse: condenseModule.SummarizeResponse = {
messages: [
{ role: "assistant", content: "Summary", ts: Date.now(), isSummary: true },
{ role: "user", content: "Message 8", ts: Date.now() },
{ role: "assistant", content: "Response 9", ts: Date.now() },
{ role: "user", content: "Message 10", ts: Date.now() },
],
summary: "Summary of conversation",
cost: mockCost,
newContextTokens: 400,
}

const summarizeSpy = vi
.spyOn(condenseModule, "summarizeConversation")
.mockResolvedValue(mockSummarizeResponse)

const messages = createMessages(10, 100) // 10 messages, 100 tokens each = 1000 tokens
const totalTokens = 900 // Excluding last message
const contextWindow = 4000
const maxTokens = 1000

const result = await truncateConversationIfNeeded({
messages,
totalTokens,
contextWindow,
maxTokens,
apiHandler: mockApiHandler,
autoCondenseContext: true,
autoCondenseContextPercent: 50, // 50% threshold (not reached)
systemPrompt: "System prompt",
taskId: "test-task",
profileThresholds: {
"test-profile": 800, // 800 tokens threshold
},
currentProfileId: "test-profile",
})

// Context should be above 800 token threshold
expect(summarizeSpy).toHaveBeenCalled()
const callArgs = summarizeSpy.mock.calls[0]
expect(callArgs[0]).toEqual(messages) // messages
expect(callArgs[1]).toBe(mockApiHandler) // apiHandler
expect(callArgs[2]).toBe("System prompt") // systemPrompt
expect(callArgs[3]).toBe("test-task") // taskId
expect(callArgs[4]).toBeGreaterThan(800) // prevContextTokens should be above threshold
expect(callArgs[5]).toBe(true) // automatic trigger
expect(callArgs[6]).toBeUndefined() // customCondensingPrompt
expect(callArgs[7]).toBeUndefined() // condensingApiHandler

expect(result.messages).toEqual(mockSummarizeResponse.messages)
expect(result.summary).toBe("Summary of conversation")
expect(result.cost).toBe(mockCost)
expect(result.prevContextTokens).toBeGreaterThan(800) // Should be above threshold
})

it("should not trigger condensing when token threshold is not reached", async () => {
vi.clearAllMocks()
const summarizeSpy = vi.spyOn(condenseModule, "summarizeConversation")

const messages = createMessages(10, 50) // 10 messages, 50 tokens each = 500 tokens
const totalTokens = 450 // Excluding last message
const contextWindow = 4000
const maxTokens = 1000

const result = await truncateConversationIfNeeded({
messages,
totalTokens,
contextWindow,
maxTokens,
apiHandler: mockApiHandler,
autoCondenseContext: true,
autoCondenseContextPercent: 50, // 50% threshold (not reached)
systemPrompt: "System prompt",
taskId: "test-task",
profileThresholds: {
"test-profile": 1000, // 1000 tokens threshold
},
currentProfileId: "test-profile",
})

// Context is at 500 tokens (450 + 50 for last message), below 1000 token threshold
expect(summarizeSpy).not.toHaveBeenCalled()
expect(result.messages).toEqual(messages)
})

it("should prefer token threshold over percentage when both are configured", async () => {
vi.clearAllMocks()
const mockCost = 0.05
const mockSummarizeResponse: condenseModule.SummarizeResponse = {
messages: [
{ role: "assistant", content: "Summary", ts: Date.now(), isSummary: true },
{ role: "user", content: "Message 8", ts: Date.now() },
{ role: "assistant", content: "Response 9", ts: Date.now() },
{ role: "user", content: "Message 10", ts: Date.now() },
],
summary: "Summary of conversation",
cost: mockCost,
newContextTokens: 400,
}

const summarizeSpy = vi
.spyOn(condenseModule, "summarizeConversation")
.mockResolvedValue(mockSummarizeResponse)

const messages = createMessages(10, 100) // 10 messages, 100 tokens each = 1000 tokens
const totalTokens = 900 // Excluding last message
const contextWindow = 4000
const maxTokens = 1000

// Test with token threshold that triggers before percentage
const result = await truncateConversationIfNeeded({
messages,
totalTokens,
contextWindow,
maxTokens,
apiHandler: mockApiHandler,
autoCondenseContext: true,
autoCondenseContextPercent: 50, // 50% = 2000 tokens (not reached)
systemPrompt: "System prompt",
taskId: "test-task",
profileThresholds: {
"test-profile": 800, // 800 tokens threshold (reached)
},
currentProfileId: "test-profile",
})

// Context is at 1000 tokens, above 800 token threshold but below 50% (2000 tokens)
expect(summarizeSpy).toHaveBeenCalled()
expect(result.messages).toEqual(mockSummarizeResponse.messages)
})
})
})

/**
Expand Down
15 changes: 13 additions & 2 deletions src/core/sliding-window/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,19 @@ export async function truncateConversationIfNeeded({

// Determine the effective threshold to use
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be helpful to add a comment here explaining the threshold logic? Something like:

Suggested change
// Determine the effective threshold to use
// Determine the effective threshold to use
// Values interpretation:
// -1: inherit from global setting
// 5-100: percentage of context window
// >100: absolute token count
let effectiveThreshold = autoCondenseContextPercent

This would make it immediately clear to future maintainers how the different threshold values are interpreted.

let effectiveThreshold = autoCondenseContextPercent
let effectiveTokenThreshold: number | undefined = undefined
const profileThreshold = profileThresholds[currentProfileId]

if (profileThreshold !== undefined) {
if (profileThreshold === -1) {
// Special case: -1 means inherit from global setting
effectiveThreshold = autoCondenseContextPercent
} else if (profileThreshold >= MIN_CONDENSE_THRESHOLD && profileThreshold <= MAX_CONDENSE_THRESHOLD) {
// Valid custom threshold
// Valid percentage threshold
effectiveThreshold = profileThreshold
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting the magic number 100 as a constant like PERCENTAGE_TOKEN_BOUNDARY. This would make the distinction between percentage and token thresholds clearer throughout the code.

} else if (profileThreshold > MAX_CONDENSE_THRESHOLD) {
// Values above 100 are treated as token counts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider adding an upper bound check for token thresholds? For example, values larger than typical context windows (e.g., > 1,000,000) might indicate a configuration error.

effectiveTokenThreshold = profileThreshold
} else {
// Invalid threshold value, fall back to global setting
console.warn(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message could be more informative about the new token-based thresholds:

Suggested change
console.warn(
console.warn(
`Invalid profile threshold ${profileThreshold} for profile "${currentProfileId}". Valid values are: -1 (inherit), 5-100 (percentage), or >100 (token count). Using global default of ${autoCondenseContextPercent}%`,
)

Expand All @@ -144,7 +149,13 @@ export async function truncateConversationIfNeeded({

if (autoCondenseContext) {
const contextPercent = (100 * prevContextTokens) / contextWindow
if (contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens) {
// Check both percentage and token thresholds
const shouldCondenseByPercent = contextPercent >= effectiveThreshold
const shouldCondenseByTokens =
effectiveTokenThreshold !== undefined && prevContextTokens >= effectiveTokenThreshold
const shouldCondenseByLimit = prevContextTokens > allowedTokens

if (shouldCondenseByPercent || shouldCondenseByTokens || shouldCondenseByLimit) {
// Attempt to intelligently condense the context
const result = await summarizeConversation(
messages,
Expand Down
Loading