feat: rename smartContextTracking to smartRead and handle @ mentioned files

- Rename experiment ID from smartContextTracking to smartRead
- Apply smart read logic to @ file mentions (skip files already in context)
- Add parseMentions parameters for experiments and apiConversationHistory
- Add English translation keys for SMART_READ experiment
- Fix TypeScript errors and update tests
This commit is contained in:
Hannes Rudolph 2025-12-15 13:53:34 -07:00
parent 59a8b38203
commit dbd059f548
10 changed files with 75 additions and 28 deletions

View file

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

View file

@ -15,11 +15,14 @@ import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
import { checkFileContextStatus, getReReadNotice } from "../context-tracking/FileContextStatusChecker"
import type { ApiMessage } from "../task-persistence/apiMessages"
import { RooIgnoreController } from "../ignore/RooIgnoreController"
import { getCommand, type Command } from "../../services/command/commands"
import { t } from "../../i18n"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
function getUrlErrorMessage(error: unknown): string {
const errorMessage = error instanceof Error ? error.message : String(error)
@ -81,6 +84,8 @@ export async function parseMentions(
includeDiagnosticMessages: boolean = true,
maxDiagnosticMessages: number = 50,
maxReadFileLine?: number,
experimentsConfig?: Record<string, boolean>,
apiConversationHistory?: ApiMessage[],
): Promise<string> {
const mentions: Set<string> = new Set()
const validCommands: Map<string, Command> = new Map()
@ -182,6 +187,37 @@ export async function parseMentions(
} else if (mention.startsWith("/")) {
const mentionPath = mention.slice(1)
try {
// Check if smart read is enabled and file is already in effective context
const isSmartReadEnabled = experiments.isEnabled(experimentsConfig ?? {}, EXPERIMENT_IDS.SMART_READ)
if (
isSmartReadEnabled &&
fileContextTracker &&
apiConversationHistory &&
!mention.endsWith("/") // Only for files, not folders
) {
const absolutePath = path.resolve(cwd, unescapeSpaces(mentionPath))
const taskMetadata = await fileContextTracker.getTaskMetadata(fileContextTracker.taskId)
const status = await checkFileContextStatus(
mentionPath,
absolutePath,
taskMetadata,
apiConversationHistory,
)
if (!status.shouldReRead) {
// File is already in effective context, return notice instead of content
const notice = getReReadNotice(status.reason)
const noticeText = notice
? `(This file's content is already in context. ${notice})`
: "(This file's content is already in context and hasn't changed.)"
parsedText += `\n\n<file_content path="${mentionPath}">\n${noticeText}\n</file_content>`
// Still track the context with the same source
await fileContextTracker.trackFileContext(mentionPath, "file_mentioned")
continue
}
}
const content = await getFileOrFolderContent(
mentionPath,
cwd,

View file

@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { parseMentions } from "./index"
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
import type { ApiMessage } from "../task-persistence/apiMessages"
/**
* Process mentions in user content, specifically within task and feedback tags
@ -16,6 +17,8 @@ export async function processUserContentMentions({
includeDiagnosticMessages = true,
maxDiagnosticMessages = 50,
maxReadFileLine,
experiments,
apiConversationHistory,
}: {
userContent: Anthropic.Messages.ContentBlockParam[]
cwd: string
@ -26,6 +29,8 @@ export async function processUserContentMentions({
includeDiagnosticMessages?: boolean
maxDiagnosticMessages?: number
maxReadFileLine?: number
experiments?: Record<string, boolean>
apiConversationHistory?: ApiMessage[]
}) {
// Process userContent array, which contains various block types:
// TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam.
@ -59,6 +64,8 @@ export async function processUserContentMentions({
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine,
experiments,
apiConversationHistory,
),
}
}
@ -79,6 +86,8 @@ export async function processUserContentMentions({
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine,
experiments,
apiConversationHistory,
),
}
}
@ -100,6 +109,8 @@ export async function processUserContentMentions({
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine,
experiments,
apiConversationHistory,
),
}
}

View file

@ -2257,12 +2257,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}),
)
const {
showRooIgnoredFiles = false,
includeDiagnosticMessages = true,
maxDiagnosticMessages = 50,
maxReadFileLine = -1,
} = (await this.providerRef.deref()?.getState()) ?? {}
const providerState = await this.providerRef.deref()?.getState()
const showRooIgnoredFiles = providerState?.showRooIgnoredFiles ?? false
const includeDiagnosticMessages = providerState?.includeDiagnosticMessages ?? true
const maxDiagnosticMessages = providerState?.maxDiagnosticMessages ?? 50
const maxReadFileLine = providerState?.maxReadFileLine ?? -1
const experimentsConfig = providerState?.experiments
const parsedUserContent = await processUserContentMentions({
userContent: currentUserContent,
@ -2274,6 +2274,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine,
experiments: experimentsConfig,
apiConversationHistory: this.apiConversationHistory,
})
const environmentDetails = await getEnvironmentDetails(this, currentIncludeFileDetails)

View file

@ -147,11 +147,11 @@ export class ReadFileTool extends BaseTool<"read_file"> {
try {
const filesToApprove: FileResult[] = []
// Check if smart context tracking experiment is enabled
// Check if smart read experiment is enabled
const experimentState = await task.providerRef.deref()?.getState()
const isSmartContextTrackingEnabled = experiments.isEnabled(
const isSmartReadEnabled = experiments.isEnabled(
experimentState?.experiments ?? {},
EXPERIMENT_IDS.SMART_CONTEXT_TRACKING,
EXPERIMENT_IDS.SMART_READ,
)
for (const fileResult of fileResults) {
@ -205,10 +205,7 @@ export class ReadFileTool extends BaseTool<"read_file"> {
// 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 (
isSmartContextTrackingEnabled &&
(!fileResult.lineRanges || fileResult.lineRanges.length === 0)
) {
if (isSmartReadEnabled && (!fileResult.lineRanges || fileResult.lineRanges.length === 0)) {
const metadata = await task.fileContextTracker.getTaskMetadata(task.taskId)
const contextStatus = await checkFileContextStatus(
relPath,

View file

@ -91,12 +91,9 @@ export async function simpleReadFileTool(
return
}
// Check if smart context tracking experiment is enabled
// Check if smart read experiment is enabled
const state = await cline.providerRef.deref()?.getState()
const isSmartContextTrackingEnabled = experiments.isEnabled(
state?.experiments ?? {},
EXPERIMENT_IDS.SMART_CONTEXT_TRACKING,
)
const isSmartReadEnabled = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.SMART_READ)
// Check if file needs to be re-read based on context status (only if experiment enabled)
let contextStatus: FileContextStatus = {
@ -105,7 +102,7 @@ export async function simpleReadFileTool(
}
let reReadNotice: string | undefined = undefined
if (isSmartContextTrackingEnabled) {
if (isSmartReadEnabled) {
const metadata = await cline.fileContextTracker.getTaskMetadata(cline.taskId)
contextStatus = await checkFileContextStatus(relPath, fullPath, metadata, cline.apiConversationHistory)

View file

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

View file

@ -7,7 +7,7 @@ export const EXPERIMENT_IDS = {
IMAGE_GENERATION: "imageGeneration",
RUN_SLASH_COMMAND: "runSlashCommand",
MULTIPLE_NATIVE_TOOL_CALLS: "multipleNativeToolCalls",
SMART_CONTEXT_TRACKING: "smartContextTracking",
SMART_READ: "smartRead",
} as const satisfies Record<string, ExperimentId>
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
@ -25,7 +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 },
SMART_READ: { enabled: false },
}
export const experimentDefault = Object.fromEntries(

View file

@ -240,7 +240,7 @@ describe("mergeExtensionState", () => {
runSlashCommand: false,
nativeToolCalling: false,
multipleNativeToolCalls: false,
smartContextTracking: false,
smartRead: false,
} as Record<ExperimentId, boolean>,
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS + 5,
}
@ -264,7 +264,7 @@ describe("mergeExtensionState", () => {
runSlashCommand: false,
nativeToolCalling: false,
multipleNativeToolCalls: false,
smartContextTracking: false,
smartRead: false,
})
})
})

View file

@ -831,6 +831,10 @@
"MULTIPLE_NATIVE_TOOL_CALLS": {
"name": "Parallel tool calls",
"description": "When enabled, the native protocol can execute multiple tools in a single assistant message turn."
},
"SMART_READ": {
"name": "Smart file context tracking",
"description": "When enabled, Roo will skip re-reading files that are already in the conversation context. This reduces token usage and speeds up responses when working with files you've recently viewed."
}
},
"promptCaching": {