Skip to content

Commit b734c71

Browse files
authored
Merge branch 'aws:feature/agentic-chat' into feature/agentic-chat
2 parents 4dac86b + 49687a7 commit b734c71

File tree

7 files changed

+367
-42
lines changed

7 files changed

+367
-42
lines changed

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

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
aditionalContentNameLimit,
7979
additionalContentInnerContextLimit,
8080
contextMaxLength,
81+
tools,
8182
} from '../../constants'
8283
import { ChatSession } from '../../clients/chat/v0/chat'
8384
import { ChatHistoryManager } from '../../storages/chatHistory'
@@ -769,13 +770,6 @@ export class ChatController {
769770
command,
770771
})
771772

772-
this.chatHistoryManager.appendUserMessage({
773-
userInputMessage: {
774-
content: prompt,
775-
userIntent: this.userIntentRecognizer.getFromContextMenuCommand(command),
776-
},
777-
})
778-
779773
return this.generateResponse(
780774
{
781775
message: prompt,
@@ -855,13 +849,6 @@ export class ChatController {
855849
context: lastTriggerEvent.context,
856850
})
857851

858-
this.chatHistoryManager.appendUserMessage({
859-
userInputMessage: {
860-
content: message.message,
861-
userIntent: message.userIntent,
862-
},
863-
})
864-
865852
return this.generateResponse(
866853
{
867854
message: message.message,
@@ -917,7 +904,7 @@ export class ChatController {
917904
// result = await executeBash.invoke(process.stdout)
918905
// break
919906
// }
920-
case 'fs_read': {
907+
case 'fsRead': {
921908
const fsRead = new FsRead(toolUse.input as unknown as FsReadParams)
922909
await fsRead.validate()
923910
result = await fsRead.invoke()
@@ -949,17 +936,9 @@ export class ChatController {
949936
toolResults.push({ content: [{ text: e.message }], toolUseId: toolUse.toolUseId, status: 'error' })
950937
}
951938

952-
this.chatHistoryManager.appendUserMessage({
953-
userInputMessage: {
954-
content: 'Tool Results',
955-
userIntent: undefined,
956-
origin: Origin.IDE,
957-
},
958-
})
959-
960939
await this.generateResponse(
961940
{
962-
message: 'Tool Results',
941+
message: '',
963942
trigger: ChatTriggerType.ChatMessage,
964943
query: undefined,
965944
codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock,
@@ -973,6 +952,7 @@ export class ChatController {
973952
context: undefined,
974953
toolResults: toolResults,
975954
origin: Origin.IDE,
955+
chatHistory: this.chatHistoryManager.getHistory(),
976956
},
977957
triggerID
978958
)
@@ -997,13 +977,6 @@ export class ChatController {
997977
type: 'chat_message',
998978
context,
999979
})
1000-
this.chatHistoryManager.appendUserMessage({
1001-
userInputMessage: {
1002-
content: message.message,
1003-
userIntent: message.userIntent,
1004-
origin: Origin.IDE,
1005-
},
1006-
})
1007980
await this.generateResponse(
1008981
{
1009982
message: message.message,
@@ -1343,6 +1316,18 @@ export class ChatController {
13431316
this.telemetryHelper.recordEnterFocusConversation(triggerEvent.tabID)
13441317
this.telemetryHelper.recordStartConversation(triggerEvent, triggerPayload)
13451318

1319+
this.chatHistoryManager.appendUserMessage({
1320+
userInputMessage: {
1321+
content: triggerPayload.message,
1322+
userIntent: triggerPayload.userIntent,
1323+
...(triggerPayload.origin && { origin: triggerPayload.origin }),
1324+
userInputMessageContext: {
1325+
tools: tools,
1326+
...(triggerPayload.toolResults && { toolResults: triggerPayload.toolResults }),
1327+
},
1328+
},
1329+
})
1330+
13461331
getLogger().info(
13471332
`response to tab: ${tabID} conversationID: ${session.sessionIdentifier} requestID: ${
13481333
response.$metadata.requestId

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
AuthNeededException,
1010
CodeReference,
1111
ContextCommandData,
12+
CustomFormActionMessage,
1213
EditorContextCommandMessage,
1314
OpenSettingsMessage,
1415
QuickActionMessage,
@@ -234,13 +235,14 @@ export class Messenger {
234235
tabID
235236
)
236237
)
238+
239+
this.dispatcher.sendCustomFormActionMessage(
240+
new CustomFormActionMessage(tabID, {
241+
id: 'confirm-tool-use',
242+
})
243+
)
237244
// TODO: setup permission action
238245
// if (!isConfirmationRequired) {
239-
// this.dispatcher.sendCustomFormActionMessage(
240-
// new CustomFormActionMessage(tabID, {
241-
// id: 'confirm-tool-use',
242-
// })
243-
// )
244246
// }
245247
}
246248

@@ -394,7 +396,9 @@ export class Messenger {
394396
messageId: messageID,
395397
content: message,
396398
references: codeReference,
397-
toolUses: [{ ...toolUse }],
399+
...(toolUse &&
400+
toolUse.input !== undefined &&
401+
toolUse.input !== '' && { toolUses: [{ ...toolUse }] }),
398402
},
399403
})
400404

packages/core/src/codewhispererChat/storages/chatHistory.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class ChatHistoryManager {
6969
if (!newMessage.userInputMessage?.content || newMessage.userInputMessage?.content.trim() === '') {
7070
this.logger.warn('input must not be empty when adding new messages')
7171
}
72-
this.history.push(this.lastUserMessage)
72+
this.history.push(this.formatChatHistoryMessage(this.lastUserMessage))
7373
}
7474

7575
/**
@@ -196,4 +196,19 @@ export class ChatHistoryManager {
196196
this.lastUserMessage.userInputMessage = msg
197197
}
198198
}
199+
200+
private formatChatHistoryMessage(message: ChatMessage): ChatMessage {
201+
if (message.userInputMessage !== undefined) {
202+
return {
203+
userInputMessage: {
204+
...message.userInputMessage,
205+
userInputMessageContext: {
206+
...message.userInputMessage.userInputMessageContext,
207+
tools: undefined,
208+
},
209+
},
210+
}
211+
}
212+
return message
213+
}
199214
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Writable } from 'stream'
7+
import { getLogger } from '../../shared/logger/logger'
8+
import { fs } from '../../shared/fs/fs' // e.g. for getUserHomeDir()
9+
import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils'
10+
import { InvokeOutput, OutputKind, sanitizePath } from './toolShared'
11+
12+
export const readOnlyCommands: string[] = ['ls', 'cat', 'echo', 'pwd', 'which', 'head', 'tail']
13+
export const maxBashToolResponseSize: number = 1024 * 1024 // 1MB
14+
export const lineCount: number = 1024
15+
export const dangerousPatterns: string[] = ['|', '<(', '$(', '`', '>', '&&', '||']
16+
17+
export interface ExecuteBashParams {
18+
command: string
19+
cwd?: string
20+
}
21+
22+
export class ExecuteBash {
23+
private readonly command: string
24+
private readonly workingDirectory?: string
25+
private readonly logger = getLogger('executeBash')
26+
27+
constructor(params: ExecuteBashParams) {
28+
this.command = params.command
29+
this.workingDirectory = params.cwd ? sanitizePath(params.cwd) : fs.getUserHomeDir()
30+
}
31+
32+
public async validate(): Promise<void> {
33+
if (!this.command.trim()) {
34+
throw new Error('Bash command cannot be empty.')
35+
}
36+
37+
const args = ExecuteBash.parseCommand(this.command)
38+
if (!args || args.length === 0) {
39+
throw new Error('No command found.')
40+
}
41+
42+
try {
43+
await ExecuteBash.whichCommand(args[0])
44+
} catch {
45+
throw new Error(`Command "${args[0]}" not found on PATH.`)
46+
}
47+
}
48+
49+
public requiresAcceptance(): boolean {
50+
try {
51+
const args = ExecuteBash.parseCommand(this.command)
52+
if (!args || args.length === 0) {
53+
return true
54+
}
55+
56+
if (args.some((arg) => dangerousPatterns.some((pattern) => arg.includes(pattern)))) {
57+
return true
58+
}
59+
60+
const command = args[0]
61+
return !readOnlyCommands.includes(command)
62+
} catch (error) {
63+
this.logger.warn(`Error while checking acceptance: ${(error as Error).message}`)
64+
return true
65+
}
66+
}
67+
68+
public async invoke(updates: Writable): Promise<InvokeOutput> {
69+
this.logger.info(`Invoking bash command: "${this.command}" in cwd: "${this.workingDirectory}"`)
70+
71+
return new Promise(async (resolve, reject) => {
72+
this.logger.debug(`Spawning process with command: bash -c "${this.command}" (cwd=${this.workingDirectory})`)
73+
74+
const stdoutBuffer: string[] = []
75+
const stderrBuffer: string[] = []
76+
77+
const childProcessOptions: ChildProcessOptions = {
78+
spawnOptions: {
79+
cwd: this.workingDirectory,
80+
stdio: ['pipe', 'pipe', 'pipe'],
81+
},
82+
collect: false,
83+
waitForStreams: true,
84+
onStdout: (chunk: string) => {
85+
ExecuteBash.handleChunk(chunk, stdoutBuffer, updates)
86+
},
87+
onStderr: (chunk: string) => {
88+
ExecuteBash.handleChunk(chunk, stderrBuffer, updates)
89+
},
90+
}
91+
92+
const childProcess = new ChildProcess('bash', ['-c', this.command], childProcessOptions)
93+
94+
try {
95+
const result = await childProcess.run()
96+
const exitStatus = result.exitCode ?? 0
97+
const stdout = stdoutBuffer.join('\n')
98+
const stderr = stderrBuffer.join('\n')
99+
const [stdoutTrunc, stdoutSuffix] = ExecuteBash.truncateSafelyWithSuffix(
100+
stdout,
101+
maxBashToolResponseSize / 3
102+
)
103+
const [stderrTrunc, stderrSuffix] = ExecuteBash.truncateSafelyWithSuffix(
104+
stderr,
105+
maxBashToolResponseSize / 3
106+
)
107+
108+
const outputJson = {
109+
exitStatus: exitStatus.toString(),
110+
stdout: stdoutTrunc + (stdoutSuffix ? ' ... truncated' : ''),
111+
stderr: stderrTrunc + (stderrSuffix ? ' ... truncated' : ''),
112+
}
113+
114+
return {
115+
output: {
116+
kind: OutputKind.Json,
117+
content: outputJson,
118+
},
119+
}
120+
} catch (err: any) {
121+
this.logger.error(`Failed to execute bash command '${this.command}': ${err.message}`)
122+
throw new Error(`Failed to execute command: ${err.message}`)
123+
}
124+
})
125+
}
126+
127+
private static handleChunk(chunk: string, buffer: string[], updates: Writable) {
128+
const lines = chunk.split(/\r?\n/)
129+
for (const line of lines) {
130+
updates.write(`${line}\n`)
131+
buffer.push(line)
132+
if (buffer.length > lineCount) {
133+
buffer.shift()
134+
}
135+
}
136+
}
137+
138+
private static truncateSafelyWithSuffix(str: string, maxLength: number): [string, boolean] {
139+
if (str.length > maxLength) {
140+
return [str.substring(0, maxLength), true]
141+
}
142+
return [str, false]
143+
}
144+
145+
private static async whichCommand(cmd: string): Promise<string> {
146+
const cp = new ChildProcess('which', [cmd], {
147+
collect: true,
148+
waitForStreams: true,
149+
})
150+
const result = await cp.run()
151+
152+
if (result.exitCode !== 0) {
153+
throw new Error(`Command "${cmd}" not found on PATH.`)
154+
}
155+
156+
const output = result.stdout.trim()
157+
if (!output) {
158+
throw new Error(`Command "${cmd}" found but 'which' returned empty output.`)
159+
}
160+
return output
161+
}
162+
163+
private static parseCommand(command: string): string[] | undefined {
164+
const result: string[] = []
165+
let current = ''
166+
let inQuote: string | undefined
167+
let escaped = false
168+
169+
for (const char of command) {
170+
if (escaped) {
171+
current += char
172+
escaped = false
173+
} else if (char === '\\') {
174+
escaped = true
175+
} else if (inQuote) {
176+
if (char === inQuote) {
177+
inQuote = undefined
178+
} else {
179+
current += char
180+
}
181+
} else if (char === '"' || char === "'") {
182+
inQuote = char
183+
} else if (char === ' ' || char === '\t') {
184+
if (current) {
185+
result.push(current)
186+
current = ''
187+
}
188+
} else {
189+
current += char
190+
}
191+
}
192+
193+
if (current) {
194+
result.push(current)
195+
}
196+
197+
return result
198+
}
199+
200+
public queueDescription(updates: Writable): void {
201+
updates.write(`I will run the following shell command: `)
202+
203+
if (this.command.length > 20) {
204+
updates.write('\n')
205+
}
206+
updates.write(`\x1b[32m${this.command}\x1b[0m\n`)
207+
}
208+
}

0 commit comments

Comments
 (0)