From 9dcf6889c2263cf5b459ed59d06a71e22b795438 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 9 Apr 2025 15:37:42 -0400 Subject: [PATCH 1/4] chore: sync fsRead tool with recent changes --- .../agenticChat/tools/fsRead.test.ts | 40 ++++++------- .../agenticChat/tools/fsRead.ts | 56 ++++++++++++------- .../agenticChat/tools/toolShared.ts | 2 - 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.test.ts index 1842f1dde8..c35f0c00d5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.test.ts @@ -43,9 +43,24 @@ describe('FsRead Tool', () => { tempFolder.clear() }) - it('throws if path is empty', async () => { + it('invalidates empty path', async () => { const fsRead = new FsRead(features) - await assert.rejects(() => fsRead.invoke({ path: '' })) + await assert.rejects( + fsRead.validate({ path: '' }), + /Path cannot be empty/i, + 'Expected an error about empty path' + ) + }) + + it('invalidates non-existent paths', async () => { + const filePath = path.join(tempFolder.path, 'no_such_file.txt') + const fsRead = new FsRead(features) + + await assert.rejects( + fsRead.validate({ path: filePath }), + /does not exist or cannot be accessed/i, + 'Expected an error indicating the path does not exist' + ) }) it('reads entire file', async () => { @@ -70,27 +85,6 @@ describe('FsRead Tool', () => { assert.strictEqual(result.output.content, 'B\nC\nD') }) - it('throws error if path does not exist', async () => { - const filePath = path.join(tempFolder.path, 'no_such_file.txt') - const fsRead = new FsRead(features) - - await assert.rejects(fsRead.invoke({ path: filePath })) - }) - - it('throws error if content exceeds 30KB', async function () { - const bigContent = 'x'.repeat(35_000) - - const filePath = await tempFolder.write('bigFile.txt', bigContent) - - const fsRead = new FsRead(features) - - await assert.rejects( - fsRead.invoke({ path: filePath }), - /This tool only supports reading \d+ bytes at a time/i, - 'Expected a size-limit error' - ) - }) - it('invalid line range', async () => { const filePath = await tempFolder.write('rangeTest.txt', '1\n2\n3') const fsRead = new FsRead(features) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts index c5ecc36ecf..ee947caa50 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts @@ -1,12 +1,8 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ import { sanitize } from '@aws/lsp-core/out/util/path' -import { InvokeOutput, maxToolResponseSize } from './toolShared' +import { InvokeOutput } from './toolShared' import { Features } from '@aws/language-server-runtimes/server-interface/server' -// Port of https://github.com/aws/aws-toolkit-vscode/blob/10bb1c7dc45f128df14d749d95905c0e9b808096/packages/core/src/codewhispererChat/tools/fsRead.ts#L17 +// Port of https://github.com/aws/aws-toolkit-vscode/blob/8e00eefa33f4eee99eed162582c32c270e9e798e/packages/core/src/codewhispererChat/tools/fsRead.ts#L17 export interface FsReadParams { path: string @@ -22,6 +18,39 @@ export class FsRead { this.workspace = features.workspace } + public async validate(params: FsReadParams): Promise { + this.logging.debug(`Validating path: ${params.path}`) + if (!params.path || params.path.trim().length === 0) { + throw new Error('Path cannot be empty.') + } + + const fileExists = await this.workspace.fs.exists(params.path) + if (!fileExists) { + throw new Error(`Path: "${params.path}" does not exist or cannot be accessed.`) + } + + this.logging.debug(`Validation succeeded for path: ${params.path}`) + } + + public async queueDescription(params: FsReadParams, updates: WritableStream) { + const updateWriter = updates.getWriter() + await updateWriter.write(`Reading file: ${params.path}]`) + + const [start, end] = params.readRange ?? [] + + if (start && end) { + await updateWriter.write(`from line ${start} to ${end}`) + } else if (start) { + if (start > 0) { + await updateWriter.write(`from line ${start} to end of file`) + } else { + await updateWriter.write(`${start} line from the end of file to end of file`) + } + } else { + await updateWriter.write('all lines') + } + } + public async invoke(params: FsReadParams): Promise { const path = sanitize(params.path) const fileContents = await this.readFile(path) @@ -37,7 +66,7 @@ export class FsRead { private handleFileRange(params: FsReadParams, fullText: string): InvokeOutput { if (!params.readRange || params.readRange.length === 0) { this.logging.log('No range provided. returning entire file.') - return this.createOutput(this.enforceMaxSize(fullText)) + return this.createOutput(fullText) } const lines = fullText.split('\n') @@ -49,7 +78,7 @@ export class FsRead { this.logging.log(`Reading file: ${params.path}, lines ${start + 1}-${end + 1}`) const slice = lines.slice(start, end + 1).join('\n') - return this.createOutput(this.enforceMaxSize(slice)) + return this.createOutput(slice) } private parseLineRange(lineCount: number, range: number[]): [number, number] { @@ -69,17 +98,6 @@ export class FsRead { return [finalStart, finalEnd] } - private enforceMaxSize(content: string): string { - const byteCount = Buffer.byteLength(content, 'utf8') - if (byteCount > maxToolResponseSize) { - throw new Error( - `This tool only supports reading ${maxToolResponseSize} bytes at a time. - You tried to read ${byteCount} bytes. Try executing with fewer lines specified.` - ) - } - return content - } - private createOutput(content: string): InvokeOutput { return { output: { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolShared.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolShared.ts index 5942504312..e294abd624 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolShared.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolShared.ts @@ -1,5 +1,3 @@ -export const maxToolResponseSize = 30720 // 30KB - export interface InvokeOutput { output: | { From 6791fecc71907e058d3efd3290885eb7f701f9c4 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 9 Apr 2025 15:40:57 -0400 Subject: [PATCH 2/4] docs: add todo for likely next steps --- .../src/language-server/agenticChat/tools/toolServer.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts index f8043013f5..218a68a7c1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts @@ -10,7 +10,12 @@ export const FsToolsServer: Server = ({ workspace, logging, agent }) => { const listDirectoryTool = new ListDirectory({ workspace, logging }) - agent.addTool(fsReadTool.getSpec(), (input: FsReadParams) => fsReadTool.invoke(input)) + agent.addTool(fsReadTool.getSpec(), async (input: FsReadParams) => { + // TODO: fill in logic for handling invalid tool invocations + // TODO: implement chat streaming via queueDescription. + await fsReadTool.validate(input) + await fsReadTool.invoke(input) + }) agent.addTool(fsWriteTool.getSpec(), (input: FsWriteParams) => fsWriteTool.invoke(input)) From 769566ba55e3dd35727a11e3cae6793e0c71e3b9 Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 9 Apr 2025 15:47:54 -0400 Subject: [PATCH 3/4] test: add a test for stream usage --- .../agenticChat/tools/fsRead.test.ts | 13 +++++++++++++ .../src/language-server/agenticChat/tools/fsRead.ts | 2 ++ 2 files changed, 15 insertions(+) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.test.ts index c35f0c00d5..d2ac19f46b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.test.ts @@ -94,4 +94,17 @@ describe('FsRead Tool', () => { assert.strictEqual(result.output.kind, 'text') assert.strictEqual(result.output.content, '') }) + + it('updates the stream', async () => { + const fsRead = new FsRead(features) + const chunks = [] + const stream = new WritableStream({ + write: c => { + chunks.push(c) + }, + }) + await fsRead.queueDescription({ path: 'this/is/my/path' }, stream) + assert.ok(chunks.length > 0) + assert.ok(!stream.locked) + }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts index ee947caa50..eae18bb126 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts @@ -49,6 +49,8 @@ export class FsRead { } else { await updateWriter.write('all lines') } + await updateWriter.close() + updateWriter.releaseLock() } public async invoke(params: FsReadParams): Promise { From 7f5dca26cb1f99ec550b2b705f28d9c88b27cc6b Mon Sep 17 00:00:00 2001 From: hkobew Date: Wed, 9 Apr 2025 15:54:14 -0400 Subject: [PATCH 4/4] chore: update spec formatting to line up --- .../src/language-server/agenticChat/tools/fsRead.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts index eae18bb126..fd83a055b9 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsRead.ts @@ -113,7 +113,7 @@ export class FsRead { return { name: 'fsRead', description: - '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.', + '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', inputSchema: { type: 'object', properties: { @@ -123,7 +123,7 @@ export class FsRead { }, readRange: { description: - '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.', + '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.', type: 'array', items: { type: 'number',