Skip to content

Commit 8313fa0

Browse files
committed
reduce complexity, get working
1 parent 9f418b1 commit 8313fa0

File tree

6 files changed

+91
-53
lines changed

6 files changed

+91
-53
lines changed

src/core/Cline.ts

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,10 +1166,8 @@ export class Cline {
11661166

11671167
// Tools
11681168

1169-
async executeCommandTool(command: string): Promise<[boolean, ToolResponse]> {
1170-
const contextWindow = this.api.getModel().info.contextWindow || 128_000
1171-
const maxAllowedSize = getMaxAllowedSize(contextWindow)
1172-
const usedContext = this.apiConversationHistory.reduce((total, msg) => {
1169+
private calculateUsedContext(): number {
1170+
return this.apiConversationHistory.reduce((total, msg) => {
11731171
if (Array.isArray(msg.content)) {
11741172
return (
11751173
total +
@@ -1183,6 +1181,12 @@ export class Cline {
11831181
}
11841182
return total + (typeof msg.content === "string" ? msg.content.length / 4 : 0)
11851183
}, 0)
1184+
}
1185+
1186+
async executeCommandTool(command: string): Promise<[boolean, ToolResponse]> {
1187+
const contextWindow = this.api.getModel().info.contextWindow || 128_000
1188+
const maxAllowedSize = getMaxAllowedSize(contextWindow)
1189+
const usedContext = this.calculateUsedContext()
11861190

11871191
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
11881192
terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
@@ -1359,20 +1363,7 @@ export class Cline {
13591363
if (this.api instanceof OpenAiHandler && this.api.getModel().id.toLowerCase().includes("deepseek")) {
13601364
contextWindow = 64_000
13611365
}
1362-
let maxAllowedSize: number
1363-
switch (contextWindow) {
1364-
case 64_000: // deepseek models
1365-
maxAllowedSize = contextWindow - 27_000
1366-
break
1367-
case 128_000: // most models
1368-
maxAllowedSize = contextWindow - 30_000
1369-
break
1370-
case 200_000: // claude models
1371-
maxAllowedSize = contextWindow - 40_000
1372-
break
1373-
default:
1374-
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
1375-
}
1366+
const maxAllowedSize = getMaxAllowedSize(contextWindow)
13761367

13771368
// This is the most reliable way to know when we're close to hitting the context window.
13781369
if (totalTokens >= maxAllowedSize) {
@@ -2017,20 +2008,7 @@ export class Cline {
20172008
const maxAllowedSize = getMaxAllowedSize(contextWindow)
20182009

20192010
// Calculate used context from current conversation
2020-
const usedContext = this.apiConversationHistory.reduce((total, msg) => {
2021-
if (Array.isArray(msg.content)) {
2022-
return (
2023-
total +
2024-
msg.content.reduce((acc, block) => {
2025-
if (block.type === "text") {
2026-
return acc + block.text.length / 4 // Rough estimate of tokens
2027-
}
2028-
return acc
2029-
}, 0)
2030-
)
2031-
}
2032-
return total + (typeof msg.content === "string" ? msg.content.length / 4 : 0)
2033-
}, 0)
2011+
const usedContext = this.calculateUsedContext()
20342012

20352013
// now execute the tool like normal
20362014
const content = await extractTextFromFile(absolutePath, maxAllowedSize, usedContext)

src/integrations/misc/extract-text.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from "fs/promises"
44
import path from "path"
55
import os from "os"
66
import { ContentTooLargeError } from "../../shared/errors"
7+
import { calculateMaxAllowedSize } from "../../utils/content-size"
78

89
const CONTEXT_LIMIT = 1000
910
const USED_CONTEXT = 200
@@ -29,8 +30,9 @@ describe("extract-text", () => {
2930
}
3031
})
3132

32-
it("throws ContentTooLargeError when file would exceed limit", async () => {
33-
const largeContent = "x".repeat(3000) // 3000 bytes = ~750 tokens
33+
it("throws ContentTooLargeError when file would exceed half of context limit", async () => {
34+
const halfContextLimit = calculateMaxAllowedSize(CONTEXT_LIMIT) // 500 tokens
35+
const largeContent = "x".repeat(halfContextLimit * 4 + 4) // Just over half context limit in tokens
3436
await fs.writeFile(tempFilePath, largeContent)
3537

3638
try {

src/integrations/misc/extract-text.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import pdf from "pdf-parse/lib/pdf-parse"
44
import mammoth from "mammoth"
55
import fs from "fs/promises"
66
import { isBinaryFile } from "isbinaryfile"
7-
import { estimateFileSize } from "../../utils/content-size"
7+
import { estimateFileSize, wouldExceedSizeLimit } from "../../utils/content-size"
88
import { ContentTooLargeError } from "../../shared/errors"
99

1010
export async function extractTextFromFile(filePath: string, contextLimit: number, usedContext: number = 0): Promise<string> {
@@ -14,9 +14,14 @@ export async function extractTextFromFile(filePath: string, contextLimit: number
1414
throw new Error(`File not found: ${filePath}`)
1515
}
1616

17-
// Check file size before attempting to read
18-
const sizeEstimate = await estimateFileSize(filePath, contextLimit, usedContext)
19-
if (sizeEstimate.wouldExceedLimit) {
17+
// Get file stats to check size
18+
const stats = await fs.stat(filePath)
19+
20+
// Check if file size would exceed limit before attempting to read
21+
// This is more efficient than creating a full SizeEstimate object when we just need a boolean check
22+
if (wouldExceedSizeLimit(stats.size, contextLimit)) {
23+
// Only create the full size estimate when we need it for the error
24+
const sizeEstimate = await estimateFileSize(filePath, contextLimit, usedContext)
2025
throw new ContentTooLargeError({
2126
type: "file",
2227
path: filePath,

src/integrations/terminal/TerminalProcess.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { EventEmitter } from "events"
2-
import * as stripAnsi from "strip-ansi"
2+
import stripAnsi from "strip-ansi"
33
import * as vscode from "vscode"
44
import { ContentTooLargeError } from "../../shared/errors"
5-
import { estimateContentSize } from "../../utils/content-size"
5+
import { estimateContentSize, wouldExceedSizeLimit } from "../../utils/content-size"
66

77
export interface TerminalProcessEvents {
88
line: [line: string]
@@ -52,9 +52,15 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
5252
const dataBytes = Buffer.from(data).length
5353
this.totalBytes += dataBytes
5454

55-
// Check total accumulated size
56-
const sizeEstimate = estimateContentSize(Buffer.alloc(this.totalBytes), this.contextLimit, this.usedContext)
57-
if (sizeEstimate.wouldExceedLimit) {
55+
// Check total accumulated size against half of context limit
56+
// Use wouldExceedSizeLimit to avoid creating unnecessary buffer
57+
if (wouldExceedSizeLimit(this.totalBytes, this.contextLimit)) {
58+
// Create size estimate only when needed for error details
59+
const sizeEstimate = estimateContentSize(
60+
Buffer.alloc(0, this.totalBytes),
61+
this.contextLimit,
62+
this.usedContext,
63+
)
5864
this.emit(
5965
"error",
6066
new ContentTooLargeError({
@@ -215,10 +221,11 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
215221

216222
// Inspired by https://github.com/sindresorhus/execa/blob/main/lib/transform/split.js
217223
private emitIfEol(chunk: string) {
218-
// Check size before adding to buffer
224+
// Check size before adding to buffer against half of context limit
219225
const newBufferSize = this.buffer.length + chunk.length
220-
const sizeEstimate = estimateContentSize(Buffer.alloc(newBufferSize), this.contextLimit, this.usedContext)
221-
if (sizeEstimate.wouldExceedLimit) {
226+
if (wouldExceedSizeLimit(newBufferSize, this.contextLimit)) {
227+
// Create size estimate only when needed for error details
228+
const sizeEstimate = estimateContentSize(Buffer.alloc(0, newBufferSize), this.contextLimit, this.usedContext)
222229
this.emit(
223230
"error",
224231
new ContentTooLargeError({

src/utils/content-size.test.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { expect } from "chai"
2-
import { estimateContentSize, estimateFileSize, estimateTokens } from "./content-size"
2+
import {
3+
estimateContentSize,
4+
estimateFileSize,
5+
estimateTokens,
6+
calculateMaxAllowedSize,
7+
wouldExceedSizeLimit,
8+
} from "./content-size"
39
import fs from "fs/promises"
410
import path from "path"
511
import os from "os"
@@ -8,13 +14,28 @@ const CONTEXT_LIMIT = 1000
814
const USED_CONTEXT = 200
915

1016
describe("content-size", () => {
17+
describe("calculateMaxAllowedSize", () => {
18+
it("calculates half of the context limit", () => {
19+
expect(calculateMaxAllowedSize(1000)).to.equal(500)
20+
expect(calculateMaxAllowedSize(128000)).to.equal(64000)
21+
})
22+
})
23+
1124
describe("estimateTokens", () => {
1225
it("estimates tokens based on byte count", () => {
1326
expect(estimateTokens(100)).to.equal(25) // 100 bytes / 4 chars per token = 25 tokens
1427
expect(estimateTokens(7)).to.equal(2) // Should round up for partial tokens
1528
})
1629
})
1730

31+
describe("wouldExceedSizeLimit", () => {
32+
it("checks if byte count would exceed half of context limit", () => {
33+
expect(wouldExceedSizeLimit(100, 1000)).to.equal(false) // 25 tokens < 500 tokens
34+
expect(wouldExceedSizeLimit(2000, 1000)).to.equal(true) // 500 tokens = 500 tokens (equal is considered exceeding)
35+
expect(wouldExceedSizeLimit(2004, 1000)).to.equal(true) // 501 tokens > 500 tokens
36+
})
37+
})
38+
1839
describe("estimateContentSize", () => {
1940
it("estimates size for string content", () => {
2041
const content = "Hello world" // 11 bytes
@@ -36,12 +57,13 @@ describe("content-size", () => {
3657
expect(result.wouldExceedLimit).to.equal(false)
3758
})
3859

39-
it("detects when content would exceed limit", () => {
40-
const largeContent = "x".repeat(3000) // 3000 bytes = ~750 tokens
60+
it("detects when content would exceed half of context limit", () => {
61+
const halfContextLimit = calculateMaxAllowedSize(CONTEXT_LIMIT) // 500 tokens
62+
const largeContent = "x".repeat(halfContextLimit * 4 + 4) // Just over half context limit in tokens
4163
const result = estimateContentSize(largeContent, CONTEXT_LIMIT, USED_CONTEXT)
4264

4365
expect(result.wouldExceedLimit).to.equal(true)
44-
expect(result.remainingContextSize).to.equal(800)
66+
expect(result.remainingContextSize).to.equal(800) // This is still contextLimit - usedContext
4567
})
4668
})
4769

src/utils/content-size.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import fs from "fs/promises"
21
import { stat } from "fs/promises"
32

43
// Rough approximation: 1 token ≈ 4 characters for English text
@@ -11,6 +10,14 @@ export interface SizeEstimate {
1110
remainingContextSize: number
1211
}
1312

13+
/**
14+
* Calculates the maximum allowed size for a single content item (file or terminal output)
15+
* We limit to half the context window to ensure no single item can consume too much context
16+
*/
17+
export function calculateMaxAllowedSize(contextLimit: number): number {
18+
return Math.floor(contextLimit / 2)
19+
}
20+
1421
/**
1522
* Estimates tokens from byte count using a simple character ratio
1623
* This is a rough approximation - actual token count may vary
@@ -19,18 +26,29 @@ export function estimateTokens(bytes: number): number {
1926
return Math.ceil(bytes / CHARS_PER_TOKEN)
2027
}
2128

29+
/**
30+
* Checks if the given byte count would exceed the size limit
31+
* More efficient than creating a buffer just to check size
32+
*/
33+
export function wouldExceedSizeLimit(byteCount: number, contextLimit: number): boolean {
34+
const estimatedTokenCount = estimateTokens(byteCount)
35+
const maxAllowedSize = calculateMaxAllowedSize(contextLimit)
36+
return estimatedTokenCount >= maxAllowedSize
37+
}
38+
2239
/**
2340
* Estimates size metrics for a string or buffer without loading entire content
2441
*/
2542
export function estimateContentSize(content: string | Buffer, contextLimit: number, usedContext: number = 0): SizeEstimate {
2643
const bytes = Buffer.isBuffer(content) ? content.length : Buffer.from(content).length
2744
const estimatedTokenCount = estimateTokens(bytes)
2845
const remainingContext = contextLimit - usedContext
46+
const maxAllowedSize = calculateMaxAllowedSize(contextLimit)
2947

3048
return {
3149
bytes,
3250
estimatedTokens: estimatedTokenCount,
33-
wouldExceedLimit: estimatedTokenCount > remainingContext,
51+
wouldExceedLimit: estimatedTokenCount >= maxAllowedSize,
3452
remainingContextSize: remainingContext,
3553
}
3654
}
@@ -43,15 +61,21 @@ export async function estimateFileSize(filePath: string, contextLimit: number, u
4361
const bytes = stats.size
4462
const estimatedTokenCount = estimateTokens(bytes)
4563
const remainingContext = contextLimit - usedContext
64+
const maxAllowedSize = calculateMaxAllowedSize(contextLimit)
4665

4766
return {
4867
bytes,
4968
estimatedTokens: estimatedTokenCount,
50-
wouldExceedLimit: estimatedTokenCount > remainingContext,
69+
wouldExceedLimit: estimatedTokenCount >= maxAllowedSize,
5170
remainingContextSize: remainingContext,
5271
}
5372
}
5473

74+
/**
75+
* Gets the maximum allowed size for the API context window
76+
* This is different from calculateMaxAllowedSize as it's for the entire context window
77+
* rather than a single content item
78+
*/
5579
export function getMaxAllowedSize(contextWindow: number): number {
5680
// Get context window and used context from API model
5781
let maxAllowedSize: number

0 commit comments

Comments
 (0)