feat: gate smart context tracking behind experimental setting

Added new 'smartContextTracking' experiment to gate the file context
tracking optimization. When disabled (default), the read_file tools
behave as before. When enabled, files that haven't changed and still
have content in the conversation context return a short 'unchanged'
response instead of re-reading the full content.

Changes:
- Added smartContextTracking to experimentIds in packages/types
- Added SMART_CONTEXT_TRACKING to experiments.ts
- Updated simpleReadFileTool.ts to check experiment before optimization
- Updated ReadFileTool.ts to check experiment before optimization
This commit is contained in:
Hannes Rudolph 2025-12-15 12:27:47 -07:00
parent 7b82c91090
commit 9f29251b8a
5 changed files with 52 additions and 10 deletions

View file

@ -13,6 +13,7 @@ export const experimentIds = [
"imageGeneration",
"runSlashCommand",
"multipleNativeToolCalls",
"smartContextTracking",
] as const
export const experimentIdsSchema = z.enum(experimentIds)
@ -30,6 +31,7 @@ export const experimentsSchema = z.object({
imageGeneration: z.boolean().optional(),
runSlashCommand: z.boolean().optional(),
multipleNativeToolCalls: z.boolean().optional(),
smartContextTracking: z.boolean().optional(),
})
export type Experiments = z.infer<typeof experimentsSchema>

View file

@ -9,7 +9,12 @@ import { formatResponse } from "../prompts/responses"
import { getModelMaxOutputTokens } from "../../shared/api"
import { t } from "../../i18n"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { checkFileContextStatus, getReReadNotice } from "../context-tracking/FileContextStatusChecker"
import {
checkFileContextStatus,
getReReadNotice,
FileContextStatus,
} from "../context-tracking/FileContextStatusChecker"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { getReadablePath } from "../../utils/path"
import { countFileLines } from "../../integrations/misc/line-counter"
@ -142,6 +147,13 @@ export class ReadFileTool extends BaseTool<"read_file"> {
try {
const filesToApprove: FileResult[] = []
// Check if smart context tracking experiment is enabled
const experimentState = await task.providerRef.deref()?.getState()
const isSmartContextTrackingEnabled = experiments.isEnabled(
experimentState?.experiments ?? {},
EXPERIMENT_IDS.SMART_CONTEXT_TRACKING,
)
for (const fileResult of fileResults) {
const relPath = fileResult.path
const fullPath = path.resolve(task.cwd, relPath)
@ -191,9 +203,12 @@ export class ReadFileTool extends BaseTool<"read_file"> {
continue
}
// Check if file needs to be re-read based on context status
// Check if file needs to be re-read based on context status (only if experiment enabled)
// Skip this check for line range requests as those always need fresh content
if (!fileResult.lineRanges || fileResult.lineRanges.length === 0) {
if (
isSmartContextTrackingEnabled &&
(!fileResult.lineRanges || fileResult.lineRanges.length === 0)
) {
const metadata = await task.fileContextTracker.getTaskMetadata(task.taskId)
const contextStatus = await checkFileContextStatus(
relPath,

View file

@ -7,7 +7,12 @@ import { formatResponse } from "../prompts/responses"
import { t } from "../../i18n"
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { checkFileContextStatus, getReReadNotice } from "../context-tracking/FileContextStatusChecker"
import {
checkFileContextStatus,
getReReadNotice,
FileContextStatus,
} from "../context-tracking/FileContextStatusChecker"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { getReadablePath } from "../../utils/path"
import { countFileLines } from "../../integrations/misc/line-counter"
@ -86,15 +91,30 @@ export async function simpleReadFileTool(
return
}
// Check if file needs to be re-read based on context status
const metadata = await cline.fileContextTracker.getTaskMetadata(cline.taskId)
const contextStatus = await checkFileContextStatus(relPath, fullPath, metadata, cline.apiConversationHistory)
// Check if smart context tracking experiment is enabled
const state = await cline.providerRef.deref()?.getState()
const isSmartContextTrackingEnabled = experiments.isEnabled(
state?.experiments ?? {},
EXPERIMENT_IDS.SMART_CONTEXT_TRACKING,
)
// If we need to re-read, get the notice explaining why (for later use)
const reReadNotice = getReReadNotice(contextStatus.reason)
// Check if file needs to be re-read based on context status (only if experiment enabled)
let contextStatus: FileContextStatus = {
shouldReRead: true,
reason: "never_read",
}
let reReadNotice: string | undefined = undefined
if (isSmartContextTrackingEnabled) {
const metadata = await cline.fileContextTracker.getTaskMetadata(cline.taskId)
contextStatus = await checkFileContextStatus(relPath, fullPath, metadata, cline.apiConversationHistory)
// If we need to re-read, get the notice explaining why (for later use)
reReadNotice = getReReadNotice(contextStatus.reason)
}
// Get max read file line setting
const { maxReadFileLine = -1 } = (await cline.providerRef.deref()?.getState()) ?? {}
const { maxReadFileLine = -1 } = state ?? {}
// Create approval message
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)

View file

@ -32,6 +32,7 @@ describe("experiments", () => {
imageGeneration: false,
runSlashCommand: false,
multipleNativeToolCalls: false,
smartContextTracking: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
})
@ -44,6 +45,7 @@ describe("experiments", () => {
imageGeneration: false,
runSlashCommand: false,
multipleNativeToolCalls: false,
smartContextTracking: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
})
@ -56,6 +58,7 @@ describe("experiments", () => {
imageGeneration: false,
runSlashCommand: false,
multipleNativeToolCalls: false,
smartContextTracking: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
})

View file

@ -7,6 +7,7 @@ export const EXPERIMENT_IDS = {
IMAGE_GENERATION: "imageGeneration",
RUN_SLASH_COMMAND: "runSlashCommand",
MULTIPLE_NATIVE_TOOL_CALLS: "multipleNativeToolCalls",
SMART_CONTEXT_TRACKING: "smartContextTracking",
} as const satisfies Record<string, ExperimentId>
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
@ -24,6 +25,7 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
IMAGE_GENERATION: { enabled: false },
RUN_SLASH_COMMAND: { enabled: false },
MULTIPLE_NATIVE_TOOL_CALLS: { enabled: false },
SMART_CONTEXT_TRACKING: { enabled: false },
}
export const experimentDefault = Object.fromEntries(