From 9f29251b8aa4cb8fe360b652bd53510f27bb1558 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 15 Dec 2025 12:27:47 -0700 Subject: [PATCH] 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 --- packages/types/src/experiment.ts | 2 ++ src/core/tools/ReadFileTool.ts | 21 ++++++++++++--- src/core/tools/simpleReadFileTool.ts | 34 +++++++++++++++++++----- src/shared/__tests__/experiments.spec.ts | 3 +++ src/shared/experiments.ts | 2 ++ 5 files changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index cc0aabd6f6..f1c16eb058 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -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 diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts index ca0b73f12d..fe8ad65189 100644 --- a/src/core/tools/ReadFileTool.ts +++ b/src/core/tools/ReadFileTool.ts @@ -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, diff --git a/src/core/tools/simpleReadFileTool.ts b/src/core/tools/simpleReadFileTool.ts index 9999d6cd08..12e8e83cc1 100644 --- a/src/core/tools/simpleReadFileTool.ts +++ b/src/core/tools/simpleReadFileTool.ts @@ -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) diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 23116b19b2..2f51469c5c 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -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) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index 0b11edfcdf..a42ab71c40 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -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 type _AssertExperimentIds = AssertEqual>> @@ -24,6 +25,7 @@ export const experimentConfigsMap: Record = { 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(