Skip to content

Commit 08c7828

Browse files
authored
Merge pull request aws#7019 from jguoamz/fsReadFix
fix(chat): Update FsRead toolspec, return truncated output is file is too large
2 parents 04f3bf9 + 8320c90 commit 08c7828

File tree

9 files changed

+129
-19
lines changed

9 files changed

+129
-19
lines changed

packages/core/src/codewhispererChat/controllers/chat/controller.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -739,12 +739,17 @@ export class ChatController {
739739
const toolUseError = toolUseWithError.error
740740
const toolResults: ToolResult[] = []
741741

742+
let response = ''
742743
if (toolUseError) {
743744
toolResults.push({
744745
content: [{ text: toolUseError.message }],
745746
toolUseId: toolUse.toolUseId,
746747
status: ToolResultStatus.ERROR,
747748
})
749+
if (toolUseError instanceof SyntaxError) {
750+
response =
751+
"Your toolUse input isn't valid. Please check the syntax and make sure the input is complete. If the input is large, break it down into multiple tool uses with smaller input."
752+
}
748753
} else {
749754
const result = ToolUtils.tryFromToolUse(toolUse)
750755
if ('type' in result) {
@@ -784,14 +789,19 @@ export class ChatController {
784789
)
785790
ToolUtils.validateOutput(output)
786791

792+
let status: ToolResultStatus = ToolResultStatus.SUCCESS
793+
if (output.output.success === false) {
794+
status = ToolResultStatus.ERROR
795+
}
796+
787797
toolResults.push({
788798
content: [
789799
output.output.kind === OutputKind.Text
790800
? { text: output.output.content }
791801
: { json: output.output.content },
792802
],
793803
toolUseId: toolUse.toolUseId,
794-
status: ToolResultStatus.SUCCESS,
804+
status,
795805
})
796806
} catch (e: any) {
797807
toolResults.push({
@@ -808,7 +818,7 @@ export class ChatController {
808818

809819
await this.generateResponse(
810820
{
811-
message: '',
821+
message: response,
812822
trigger: ChatTriggerType.ChatMessage,
813823
query: undefined,
814824
codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock,

packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,12 @@ export class Messenger {
289289
try {
290290
toolUse.input = JSON.parse(toolUseInput)
291291
} catch (error: any) {
292-
getLogger().error(`JSON parse error for toolUseInput: ${toolUseInput}`)
292+
getLogger().error(
293+
`JSON parse error: ${error.message} for toolUseInput: ${toolUseInput}`
294+
)
293295
// set toolUse.input to be empty valid json object
294296
toolUse.input = {}
295-
error.message = `Tool input has invalid JSON format: ${error.message}`
297+
error.message = `tooluse input is invalid: ${toolUseInput}`
296298
// throw it out to allow the error to be handled in the catch block
297299
throw error
298300
}

packages/core/src/codewhispererChat/tools/executeBash.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export class ExecuteBash {
343343
const exitStatus = result.exitCode ?? 0
344344
const stdout = stdoutBuffer.join('\n')
345345
const stderr = stderrBuffer.join('\n')
346+
const success = exitStatus === 0 && !stderr
346347
const [stdoutTrunc, stdoutSuffix] = ExecuteBash.truncateSafelyWithSuffix(
347348
stdout,
348349
maxBashToolResponseSize / 3
@@ -362,6 +363,7 @@ export class ExecuteBash {
362363
output: {
363364
kind: OutputKind.Json,
364365
content: outputJson,
366+
success,
365367
},
366368
})
367369
} catch (err: any) {

packages/core/src/codewhispererChat/tools/fsRead.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as vscode from 'vscode'
66
import { getLogger } from '../../shared/logger/logger'
77
import fs from '../../shared/fs/fs'
88
import { Writable } from 'stream'
9-
import { InvokeOutput, OutputKind, sanitizePath, CommandValidation } from './toolShared'
9+
import { InvokeOutput, OutputKind, sanitizePath, CommandValidation, fsReadToolResponseSize } from './toolShared'
1010
import { isInDirectory } from '../../shared/filesystemUtilities'
1111

1212
export interface FsReadParams {
@@ -118,11 +118,24 @@ export class FsRead {
118118
}
119119

120120
private createOutput(content: string): InvokeOutput {
121+
if (content.length > fsReadToolResponseSize) {
122+
this.logger.info(
123+
`The file is too large, truncating output to the first ${fsReadToolResponseSize} characters.`
124+
)
125+
content = this.truncateContent(content)
126+
}
121127
return {
122128
output: {
123129
kind: OutputKind.Text,
124130
content: content,
125131
},
126132
}
127133
}
134+
135+
private truncateContent(content: string): string {
136+
if (content.length > fsReadToolResponseSize) {
137+
return content.substring(0, fsReadToolResponseSize)
138+
}
139+
return content
140+
}
128141
}

packages/core/src/codewhispererChat/tools/toolShared.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import path from 'path'
77
import fs from '../../shared/fs/fs'
88

9-
export const maxToolResponseSize = 200_000
9+
export const defaultMaxToolResponseSize = 100_000
10+
export const listDirectoryToolResponseSize = 30_000
11+
export const fsReadToolResponseSize = 200_000
1012

1113
export enum OutputKind {
1214
Text = 'text',
@@ -17,6 +19,7 @@ export interface InvokeOutput {
1719
output: {
1820
kind: OutputKind
1921
content: string | any
22+
success?: boolean
2023
}
2124
}
2225

packages/core/src/codewhispererChat/tools/toolUtils.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import { FsRead, FsReadParams } from './fsRead'
77
import { FsWrite, FsWriteParams } from './fsWrite'
88
import { CommandValidation, ExecuteBash, ExecuteBashParams } from './executeBash'
99
import { ToolResult, ToolResultContentBlock, ToolResultStatus, ToolUse } from '@amzn/codewhisperer-streaming'
10-
import { InvokeOutput, maxToolResponseSize } from './toolShared'
10+
import {
11+
InvokeOutput,
12+
defaultMaxToolResponseSize,
13+
listDirectoryToolResponseSize,
14+
fsReadToolResponseSize,
15+
} from './toolShared'
1116
import { ListDirectory, ListDirectoryParams } from './listDirectory'
1217
import * as vscode from 'vscode'
1318

@@ -73,9 +78,20 @@ export class ToolUtils {
7378
}
7479
}
7580

76-
static validateOutput(output: InvokeOutput): void {
81+
static validateOutput(output: InvokeOutput, toolType: ToolType): void {
82+
let maxToolResponseSize = defaultMaxToolResponseSize
83+
switch (toolType) {
84+
case ToolType.FsRead:
85+
maxToolResponseSize = fsReadToolResponseSize
86+
break
87+
case ToolType.ListDirectory:
88+
maxToolResponseSize = listDirectoryToolResponseSize
89+
break
90+
default:
91+
break
92+
}
7793
if (output.output.content.length > maxToolResponseSize) {
78-
throw Error(`Tool output exceeds maximum character limit of ${maxToolResponseSize}`)
94+
throw Error(`${toolType} output exceeds maximum character limit of ${maxToolResponseSize}`)
7995
}
8096
}
8197

packages/core/src/codewhispererChat/tools/tool_index.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"fsRead": {
33
"name": "fsRead",
4-
"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.",
4+
"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.\n * If the file exceeds 200K characters, this tool will only read the first 200K characters of the file.",
55
"inputSchema": {
66
"type": "object",
77
"properties": {
@@ -10,7 +10,7 @@
1010
"type": "string"
1111
},
1212
"readRange": {
13-
"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. If the whole file is too large, try reading 4000 lines at once, for example: after reading [1, 4000], read [4000, 8000] next and repeat. You should read atleast 250 lines per invocation of the tool. In some cases, if reading a range of lines results in too many invocations instead attempt to read 4000 lines.",
13+
"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.\n * Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.",
1414
"items": {
1515
"type": "integer"
1616
},

packages/core/src/test/codewhispererChat/tools/fsRead.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { TestFolder } from '../../testUtil'
88
import path from 'path'
99
import * as vscode from 'vscode'
1010
import sinon from 'sinon'
11+
import { fsReadToolResponseSize } from '../../../codewhispererChat/tools/toolShared'
1112

1213
describe('FsRead Tool', () => {
1314
let testFolder: TestFolder
@@ -37,6 +38,20 @@ describe('FsRead Tool', () => {
3738
assert.strictEqual(result.output.content, fileContent, 'File content should match exactly')
3839
})
3940

41+
it('truncate output if too large', async () => {
42+
const fileContent = 'A'.repeat(fsReadToolResponseSize + 10)
43+
const filePath = await testFolder.write('largeFile.txt', fileContent)
44+
const fsRead = new FsRead({ path: filePath })
45+
await fsRead.validate()
46+
const result = await fsRead.invoke(process.stdout)
47+
assert.strictEqual(result.output.kind, 'text', 'Output kind should be "text"')
48+
assert.strictEqual(
49+
result.output.content.length,
50+
fsReadToolResponseSize,
51+
'Output should be truncated to the max size'
52+
)
53+
})
54+
4055
it('reads partial lines of a file', async () => {
4156
const fileContent = 'A\nB\nC\nD\nE\nF'
4257
const filePath = await testFolder.write('partialFile.txt', fileContent)

packages/core/src/test/codewhispererChat/tools/toolShared.test.ts

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
import assert from 'assert'
77
import * as sinon from 'sinon'
88
import { Writable } from 'stream'
9-
import { sanitizePath, OutputKind, InvokeOutput } from '../../../codewhispererChat/tools/toolShared'
9+
import {
10+
sanitizePath,
11+
OutputKind,
12+
InvokeOutput,
13+
listDirectoryToolResponseSize,
14+
fsReadToolResponseSize,
15+
defaultMaxToolResponseSize,
16+
} from '../../../codewhispererChat/tools/toolShared'
1017
import { ToolUtils, Tool, ToolType } from '../../../codewhispererChat/tools/toolUtils'
1118
import { FsRead } from '../../../codewhispererChat/tools/fsRead'
1219
import { FsWrite } from '../../../codewhispererChat/tools/fsWrite'
@@ -158,25 +165,67 @@ describe('ToolUtils', function () {
158165
})
159166

160167
describe('validateOutput', function () {
161-
it('does not throw error if output is within size limit', function () {
168+
it('does not throw error if output is within size limit for fsRead', function () {
162169
const output: InvokeOutput = {
163170
output: {
164171
kind: OutputKind.Text,
165-
content: 'a'.repeat(150_000),
172+
content: 'a'.repeat(fsReadToolResponseSize - 1),
166173
},
167174
}
168-
assert.doesNotThrow(() => ToolUtils.validateOutput(output))
175+
assert.doesNotThrow(() => ToolUtils.validateOutput(output, ToolType.FsRead))
169176
})
170-
it('throws error if output exceeds max size', function () {
177+
it('throws error if output exceeds max size for fsRead', function () {
171178
const output: InvokeOutput = {
172179
output: {
173180
kind: OutputKind.Text,
174-
content: 'a'.repeat(200_001), // 200,001 characters
181+
content: 'a'.repeat(fsReadToolResponseSize + 1),
175182
},
176183
}
177-
assert.throws(() => ToolUtils.validateOutput(output), {
184+
assert.throws(() => ToolUtils.validateOutput(output, ToolType.FsRead), {
178185
name: 'Error',
179-
message: 'Tool output exceeds maximum character limit of 200000',
186+
message: `fsRead output exceeds maximum character limit of ${fsReadToolResponseSize}`,
187+
})
188+
})
189+
it('does not throw error if output is within size limit for listDirectory', function () {
190+
const output: InvokeOutput = {
191+
output: {
192+
kind: OutputKind.Text,
193+
content: 'a'.repeat(listDirectoryToolResponseSize - 1),
194+
},
195+
}
196+
assert.doesNotThrow(() => ToolUtils.validateOutput(output, ToolType.ListDirectory))
197+
})
198+
it('throws error if output exceeds max size for listDirectory', function () {
199+
const output: InvokeOutput = {
200+
output: {
201+
kind: OutputKind.Text,
202+
content: 'a'.repeat(listDirectoryToolResponseSize + 1),
203+
},
204+
}
205+
assert.throws(() => ToolUtils.validateOutput(output, ToolType.ListDirectory), {
206+
name: 'Error',
207+
message: `listDirectory output exceeds maximum character limit of ${listDirectoryToolResponseSize}`,
208+
})
209+
})
210+
it('does not throw error if output is within size limit for fsWrite', function () {
211+
const output: InvokeOutput = {
212+
output: {
213+
kind: OutputKind.Text,
214+
content: 'a'.repeat(defaultMaxToolResponseSize - 1),
215+
},
216+
}
217+
assert.doesNotThrow(() => ToolUtils.validateOutput(output, ToolType.FsWrite))
218+
})
219+
it('does not throw error if output is within size limit for fsWrite', function () {
220+
const output: InvokeOutput = {
221+
output: {
222+
kind: OutputKind.Text,
223+
content: 'a'.repeat(defaultMaxToolResponseSize + 1),
224+
},
225+
}
226+
assert.throws(() => ToolUtils.validateOutput(output, ToolType.FsWrite), {
227+
name: 'Error',
228+
message: `fsWrite output exceeds maximum character limit of ${defaultMaxToolResponseSize}`,
180229
})
181230
})
182231
})

0 commit comments

Comments
 (0)