Skip to content

Commit 44ea42f

Browse files
committed
feat(file-reads): Implement file-read caching
1 parent b20bd7b commit 44ea42f

File tree

2 files changed

+85
-79
lines changed

2 files changed

+85
-79
lines changed
Lines changed: 83 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,126 @@
1-
import { stat } from "fs/promises"
1+
import fs from "fs"
22

3+
// Types
34
export interface LineRange {
45
start: number
56
end: number
67
}
78

89
export interface FileMetadata {
910
fileName: string
10-
mtime: number
11-
loadedRanges: LineRange[]
11+
mtime: string
12+
lineRanges: LineRange[]
1213
}
1314

1415
export interface ConversationMessage {
16+
role: "user" | "assistant"
17+
content: any
18+
ts: number
19+
tool?: {
20+
name: string
21+
options: any
22+
}
1523
files?: FileMetadata[]
1624
}
1725

18-
export interface FilteredReadRequest {
19-
status: "REJECT_ALL" | "ALLOW_PARTIAL" | "ALLOW_ALL"
20-
rangesToRead: LineRange[]
21-
}
26+
type CacheResult =
27+
| { status: "ALLOW_ALL" }
28+
| { status: "ALLOW_PARTIAL"; rangesToRead: LineRange[] }
29+
| { status: "REJECT_ALL" }
2230

2331
/**
24-
* Checks if two ranges overlap.
32+
* Checks if two line ranges overlap.
33+
* @param r1 - The first line range.
34+
* @param r2 - The second line range.
35+
* @returns True if the ranges overlap, false otherwise.
2536
*/
2637
function rangesOverlap(r1: LineRange, r2: LineRange): boolean {
2738
return r1.start <= r2.end && r1.end >= r2.start
2839
}
2940

3041
/**
31-
* Subtracts one range from another.
32-
* Returns an array of ranges that are in `original` but not in `toRemove`.
42+
* Subtracts one line range from another.
43+
* @param from - The range to subtract from.
44+
* @param toSubtract - The range to subtract.
45+
* @returns An array of ranges remaining after subtraction.
3346
*/
34-
export function subtractRange(original: LineRange, toRemove: LineRange): LineRange[] {
35-
if (!rangesOverlap(original, toRemove)) {
36-
return [original]
47+
function subtractRange(from: LineRange, toSubtract: LineRange): LineRange[] {
48+
// No overlap
49+
if (from.end < toSubtract.start || from.start > toSubtract.end) {
50+
return [from]
3751
}
38-
39-
const result: LineRange[] = []
40-
41-
// Part of original before toRemove
42-
if (original.start < toRemove.start) {
43-
result.push({ start: original.start, end: toRemove.start - 1 })
52+
const remainingRanges: LineRange[] = []
53+
// Part of 'from' is before 'toSubtract'
54+
if (from.start < toSubtract.start) {
55+
remainingRanges.push({ start: from.start, end: toSubtract.start - 1 })
4456
}
45-
46-
// Part of original after toRemove
47-
if (original.end > toRemove.end) {
48-
result.push({ start: toRemove.end + 1, end: original.end })
57+
// Part of 'from' is after 'toSubtract'
58+
if (from.end > toSubtract.end) {
59+
remainingRanges.push({ start: toSubtract.end + 1, end: from.end })
4960
}
50-
51-
return result
61+
return remainingRanges
5262
}
5363

5464
/**
55-
* Subtracts a set of ranges from another set of ranges.
65+
* Processes a read request against cached file data in conversation history.
66+
* @param requestedFilePath - The full path of the file being requested.
67+
* @param requestedRanges - The line ranges being requested.
68+
* @param conversationHistory - The history of conversation messages.
69+
* @returns A CacheResult indicating whether to allow, partially allow, or reject the read.
5670
*/
57-
export function subtractRanges(originals: LineRange[], toRemoves: LineRange[]): LineRange[] {
58-
let remaining = [...originals]
59-
60-
for (const toRemove of toRemoves) {
61-
remaining = remaining.flatMap((original) => subtractRange(original, toRemove))
62-
}
63-
64-
return remaining
65-
}
66-
6771
export async function processAndFilterReadRequest(
68-
requestedFile: string,
72+
requestedFilePath: string,
6973
requestedRanges: LineRange[],
7074
conversationHistory: ConversationMessage[],
71-
): Promise<FilteredReadRequest> {
72-
let currentMtime: number
75+
): Promise<CacheResult> {
7376
try {
74-
currentMtime = (await stat(requestedFile)).mtime.getTime()
75-
} catch (error) {
76-
// File doesn't exist or other error, so we must read.
77-
return {
78-
status: "ALLOW_ALL",
79-
rangesToRead: requestedRanges,
77+
const stats = await fs.promises.stat(requestedFilePath)
78+
const currentMtime = stats.mtime.toISOString()
79+
80+
let rangesToRead = [...requestedRanges]
81+
82+
// If no specific ranges are requested, treat it as a request for the whole file.
83+
if (rangesToRead.length === 0) {
84+
// We need to know the number of lines to create a full range.
85+
// This logic is simplified; in a real scenario, you'd get the line count.
86+
// For this example, we'll assume we can't determine the full range without reading the file,
87+
// so we proceed with ALLOW_ALL if no ranges are specified.
88+
return { status: "ALLOW_ALL" }
8089
}
81-
}
82-
83-
let rangesThatStillNeedToBeRead = [...requestedRanges]
84-
85-
for (let i = conversationHistory.length - 1; i >= 0; i--) {
86-
const message = conversationHistory[i]
87-
if (!message.files) {
88-
continue
89-
}
90-
91-
const relevantFileHistory = message.files.find((f) => f.fileName === requestedFile)
92-
93-
if (relevantFileHistory && relevantFileHistory.mtime >= currentMtime) {
94-
rangesThatStillNeedToBeRead = subtractRanges(rangesThatStillNeedToBeRead, relevantFileHistory.loadedRanges)
9590

96-
if (rangesThatStillNeedToBeRead.length === 0) {
97-
return {
98-
status: "REJECT_ALL",
99-
rangesToRead: [],
91+
for (const message of conversationHistory) {
92+
if (message.files) {
93+
for (const file of message.files) {
94+
if (file.fileName === requestedFilePath && new Date(file.mtime) >= new Date(currentMtime)) {
95+
// File in history is up-to-date. Check ranges.
96+
for (const cachedRange of file.lineRanges) {
97+
rangesToRead = rangesToRead.flatMap((reqRange) => {
98+
if (rangesOverlap(reqRange, cachedRange)) {
99+
return subtractRange(reqRange, cachedRange)
100+
}
101+
return [reqRange]
102+
})
103+
}
104+
}
100105
}
101106
}
102107
}
103-
}
104-
105-
const originalRangesString = JSON.stringify(requestedRanges.sort((a, b) => a.start - b.start))
106-
const finalRangesString = JSON.stringify(rangesThatStillNeedToBeRead.sort((a, b) => a.start - b.start))
107108

108-
if (originalRangesString === finalRangesString) {
109-
return {
110-
status: "ALLOW_ALL",
111-
rangesToRead: requestedRanges,
109+
if (rangesToRead.length === 0) {
110+
return { status: "REJECT_ALL" }
111+
} else if (rangesToRead.length < requestedRanges.length) {
112+
return { status: "ALLOW_PARTIAL", rangesToRead }
113+
} else {
114+
return { status: "ALLOW_ALL" }
112115
}
113-
}
114-
115-
return {
116-
status: "ALLOW_PARTIAL",
117-
rangesToRead: rangesThatStillNeedToBeRead,
116+
} catch (error) {
117+
// If we can't get file stats, it's safer to allow the read.
118+
if (error.code === "ENOENT") {
119+
// File doesn't exist, let the regular tool handle it.
120+
return { status: "ALLOW_ALL" }
121+
}
122+
console.error(`Error processing file read request for ${requestedFilePath}:`, error)
123+
// On other errors, allow the read to proceed to handle it.
124+
return { status: "ALLOW_ALL" }
118125
}
119126
}

src/core/tools/readFileTool.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from "path"
2-
import fs from "fs" // Added fs import
2+
import fs from "fs"
33
import { isBinaryFile } from "isbinaryfile"
44

55
import { Task } from "../task/Task"
@@ -15,8 +15,7 @@ import { readLines } from "../../integrations/misc/read-lines"
1515
import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text"
1616
import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
1717
import { parseXml } from "../../utils/xml"
18-
import { processAndFilterReadRequest } from "../services/fileReadCacheService"
19-
import { ConversationMessage } from "../services/fileReadCacheService"
18+
import { processAndFilterReadRequest, ConversationMessage } from "../services/fileReadCacheService"
2019

2120
export function getReadFileToolDescription(blockName: string, blockParams: any): string {
2221
// Handle both single path and multiple files via args

0 commit comments

Comments
 (0)