Skip to content

Commit 101007f

Browse files
authored
chore: sync fsRead with VSC branch. (#936)
* chore: sync fsRead tool with recent changes * docs: add todo for likely next steps * test: add a test for stream usage * chore: update spec formatting to line up
1 parent 148062f commit 101007f

File tree

4 files changed

+77
-47
lines changed

4 files changed

+77
-47
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.test.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,24 @@ describe('FsRead Tool', () => {
4343
tempFolder.clear()
4444
})
4545

46-
it('throws if path is empty', async () => {
46+
it('invalidates empty path', async () => {
4747
const fsRead = new FsRead(features)
48-
await assert.rejects(() => fsRead.invoke({ path: '' }))
48+
await assert.rejects(
49+
fsRead.validate({ path: '' }),
50+
/Path cannot be empty/i,
51+
'Expected an error about empty path'
52+
)
53+
})
54+
55+
it('invalidates non-existent paths', async () => {
56+
const filePath = path.join(tempFolder.path, 'no_such_file.txt')
57+
const fsRead = new FsRead(features)
58+
59+
await assert.rejects(
60+
fsRead.validate({ path: filePath }),
61+
/does not exist or cannot be accessed/i,
62+
'Expected an error indicating the path does not exist'
63+
)
4964
})
5065

5166
it('reads entire file', async () => {
@@ -70,27 +85,6 @@ describe('FsRead Tool', () => {
7085
assert.strictEqual(result.output.content, 'B\nC\nD')
7186
})
7287

73-
it('throws error if path does not exist', async () => {
74-
const filePath = path.join(tempFolder.path, 'no_such_file.txt')
75-
const fsRead = new FsRead(features)
76-
77-
await assert.rejects(fsRead.invoke({ path: filePath }))
78-
})
79-
80-
it('throws error if content exceeds 30KB', async function () {
81-
const bigContent = 'x'.repeat(35_000)
82-
83-
const filePath = await tempFolder.write('bigFile.txt', bigContent)
84-
85-
const fsRead = new FsRead(features)
86-
87-
await assert.rejects(
88-
fsRead.invoke({ path: filePath }),
89-
/This tool only supports reading \d+ bytes at a time/i,
90-
'Expected a size-limit error'
91-
)
92-
})
93-
9488
it('invalid line range', async () => {
9589
const filePath = await tempFolder.write('rangeTest.txt', '1\n2\n3')
9690
const fsRead = new FsRead(features)
@@ -100,4 +94,17 @@ describe('FsRead Tool', () => {
10094
assert.strictEqual(result.output.kind, 'text')
10195
assert.strictEqual(result.output.content, '')
10296
})
97+
98+
it('updates the stream', async () => {
99+
const fsRead = new FsRead(features)
100+
const chunks = []
101+
const stream = new WritableStream({
102+
write: c => {
103+
chunks.push(c)
104+
},
105+
})
106+
await fsRead.queueDescription({ path: 'this/is/my/path' }, stream)
107+
assert.ok(chunks.length > 0)
108+
assert.ok(!stream.locked)
109+
})
103110
})

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
/*!
2-
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3-
* SPDX-License-Identifier: Apache-2.0
4-
*/
51
import { sanitize } from '@aws/lsp-core/out/util/path'
6-
import { InvokeOutput, maxToolResponseSize } from './toolShared'
2+
import { InvokeOutput } from './toolShared'
73
import { Features } from '@aws/language-server-runtimes/server-interface/server'
84

9-
// Port of https://github.com/aws/aws-toolkit-vscode/blob/10bb1c7dc45f128df14d749d95905c0e9b808096/packages/core/src/codewhispererChat/tools/fsRead.ts#L17
5+
// Port of https://github.com/aws/aws-toolkit-vscode/blob/8e00eefa33f4eee99eed162582c32c270e9e798e/packages/core/src/codewhispererChat/tools/fsRead.ts#L17
106

117
export interface FsReadParams {
128
path: string
@@ -22,6 +18,41 @@ export class FsRead {
2218
this.workspace = features.workspace
2319
}
2420

21+
public async validate(params: FsReadParams): Promise<void> {
22+
this.logging.debug(`Validating path: ${params.path}`)
23+
if (!params.path || params.path.trim().length === 0) {
24+
throw new Error('Path cannot be empty.')
25+
}
26+
27+
const fileExists = await this.workspace.fs.exists(params.path)
28+
if (!fileExists) {
29+
throw new Error(`Path: "${params.path}" does not exist or cannot be accessed.`)
30+
}
31+
32+
this.logging.debug(`Validation succeeded for path: ${params.path}`)
33+
}
34+
35+
public async queueDescription(params: FsReadParams, updates: WritableStream) {
36+
const updateWriter = updates.getWriter()
37+
await updateWriter.write(`Reading file: ${params.path}]`)
38+
39+
const [start, end] = params.readRange ?? []
40+
41+
if (start && end) {
42+
await updateWriter.write(`from line ${start} to ${end}`)
43+
} else if (start) {
44+
if (start > 0) {
45+
await updateWriter.write(`from line ${start} to end of file`)
46+
} else {
47+
await updateWriter.write(`${start} line from the end of file to end of file`)
48+
}
49+
} else {
50+
await updateWriter.write('all lines')
51+
}
52+
await updateWriter.close()
53+
updateWriter.releaseLock()
54+
}
55+
2556
public async invoke(params: FsReadParams): Promise<InvokeOutput> {
2657
const path = sanitize(params.path)
2758
const fileContents = await this.readFile(path)
@@ -37,7 +68,7 @@ export class FsRead {
3768
private handleFileRange(params: FsReadParams, fullText: string): InvokeOutput {
3869
if (!params.readRange || params.readRange.length === 0) {
3970
this.logging.log('No range provided. returning entire file.')
40-
return this.createOutput(this.enforceMaxSize(fullText))
71+
return this.createOutput(fullText)
4172
}
4273

4374
const lines = fullText.split('\n')
@@ -49,7 +80,7 @@ export class FsRead {
4980

5081
this.logging.log(`Reading file: ${params.path}, lines ${start + 1}-${end + 1}`)
5182
const slice = lines.slice(start, end + 1).join('\n')
52-
return this.createOutput(this.enforceMaxSize(slice))
83+
return this.createOutput(slice)
5384
}
5485

5586
private parseLineRange(lineCount: number, range: number[]): [number, number] {
@@ -69,17 +100,6 @@ export class FsRead {
69100
return [finalStart, finalEnd]
70101
}
71102

72-
private enforceMaxSize(content: string): string {
73-
const byteCount = Buffer.byteLength(content, 'utf8')
74-
if (byteCount > maxToolResponseSize) {
75-
throw new Error(
76-
`This tool only supports reading ${maxToolResponseSize} bytes at a time.
77-
You tried to read ${byteCount} bytes. Try executing with fewer lines specified.`
78-
)
79-
}
80-
return content
81-
}
82-
83103
private createOutput(content: string): InvokeOutput {
84104
return {
85105
output: {
@@ -93,7 +113,7 @@ export class FsRead {
93113
return {
94114
name: 'fsRead',
95115
description:
96-
'A tool for reading a file. \n* This tool returns the contents of a file, and the optional `readRange` determines what range of lines will be read from the specified file.',
116+
'A tool for reading a file.\n * This tool returns the contents of a file, and the optional `readRange` determines what range of lines will be read from the specified file',
97117
inputSchema: {
98118
type: 'object',
99119
properties: {
@@ -103,7 +123,7 @@ export class FsRead {
103123
},
104124
readRange: {
105125
description:
106-
'Optional parameter when reading files.\n* If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.',
126+
'Optional parameter when reading files.\n * If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.',
107127
type: 'array',
108128
items: {
109129
type: 'number',

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ export const FsToolsServer: Server = ({ workspace, logging, agent }) => {
1010

1111
const listDirectoryTool = new ListDirectory({ workspace, logging })
1212

13-
agent.addTool(fsReadTool.getSpec(), (input: FsReadParams) => fsReadTool.invoke(input))
13+
agent.addTool(fsReadTool.getSpec(), async (input: FsReadParams) => {
14+
// TODO: fill in logic for handling invalid tool invocations
15+
// TODO: implement chat streaming via queueDescription.
16+
await fsReadTool.validate(input)
17+
await fsReadTool.invoke(input)
18+
})
1419

1520
agent.addTool(fsWriteTool.getSpec(), (input: FsWriteParams) => fsWriteTool.invoke(input))
1621

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolShared.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
export const maxToolResponseSize = 30720 // 30KB
2-
31
export interface InvokeOutput {
42
output:
53
| {

0 commit comments

Comments
 (0)