Skip to content

Commit 8ca59a2

Browse files
authored
Merge branch 'aws:main' into feature/mcp
2 parents ace2bcf + e9e63b5 commit 8ca59a2

File tree

3 files changed

+68
-17
lines changed

3 files changed

+68
-17
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,30 @@ export class AgenticChatController implements ChatHandlers {
459459
#getPendingToolUses(toolUses: Record<string, ToolUse & { stop: boolean }>): Array<ToolUse & { stop: boolean }> {
460460
return Object.values(toolUses).filter(toolUse => toolUse.stop)
461461
}
462+
/**
463+
* Creates a promise that does not resolve until the user accepts or rejects the tool usage.
464+
* @param toolUseId
465+
* @param toolUseName
466+
* @param resultStream
467+
* @param promptBlockId id of approval block. This allows us to overwrite the buttons with 'accepted' or 'rejected' text.
468+
* @param session
469+
*/
470+
async waitForToolApproval(
471+
toolUseId: string,
472+
toolUseName: string,
473+
resultStream: AgenticChatResultStream,
474+
promptBlockId: number,
475+
session: ChatSessionService
476+
) {
477+
const deferred = this.#createDeferred()
478+
session.setDeferredToolExecution(toolUseId, deferred.resolve, deferred.reject)
479+
this.#log(`Prompting for tool approval for tool: ${toolUseName}`)
480+
await deferred.promise
481+
if (toolUseName === 'executeBash') {
482+
// Note: we want to overwrite the button block because it already exists in the stream.
483+
await resultStream.overwriteResultBlock(this.#getUpdateBashConfirmResult(toolUseId, true), promptBlockId)
484+
}
485+
}
462486

463487
/**
464488
* Processes tool uses by running the tools and collecting results
@@ -474,7 +498,6 @@ export class AgenticChatController implements ChatHandlers {
474498
for (const toolUse of toolUses) {
475499
if (!toolUse.name || !toolUse.toolUseId) continue
476500
this.#triggerContext.getToolUseLookup().set(toolUse.toolUseId, toolUse)
477-
let needsConfirmation
478501

479502
try {
480503
const { explanation } = toolUse.input as unknown as ExplanatoryParams
@@ -506,9 +529,15 @@ export class AgenticChatController implements ChatHandlers {
506529
toolUse.input as unknown as ExecuteBashParams
507530
)
508531
if (requiresAcceptance) {
509-
needsConfirmation = true
510532
const confirmationResult = this.#processExecuteBashConfirmation(toolUse, warning)
511-
await chatResultStream.writeResultBlock(confirmationResult)
533+
const buttonBlockId = await chatResultStream.writeResultBlock(confirmationResult)
534+
await this.waitForToolApproval(
535+
toolUse.toolUseId,
536+
toolUse.name,
537+
chatResultStream,
538+
buttonBlockId,
539+
session
540+
)
512541
}
513542
break
514543
default:
@@ -520,16 +549,6 @@ export class AgenticChatController implements ChatHandlers {
520549
break
521550
}
522551

523-
if (needsConfirmation) {
524-
const deferred = this.#createDeferred()
525-
session.setDeferredToolExecution(toolUse.toolUseId, deferred.resolve, deferred.reject)
526-
this.#log(`Prompting for tool approval for tool: ${toolUse.name}`)
527-
await deferred.promise
528-
if (toolUse.name === 'executeBash') {
529-
await chatResultStream.writeResultBlock(this.#getUpdateBashConfirmResult(toolUse, true))
530-
}
531-
}
532-
533552
const result = await this.#features.agent.runTool(toolUse.name, toolUse.input, token)
534553
let toolResultContent: ToolResultContentBlock
535554

@@ -572,7 +591,9 @@ export class AgenticChatController implements ChatHandlers {
572591
// If we did not approve a tool to be used or the user stopped the response, bubble this up to interrupt agentic loop
573592
if (CancellationError.isUserCancelled(err) || err instanceof ToolApprovalException) {
574593
if (err instanceof ToolApprovalException) {
575-
await chatResultStream.writeResultBlock(this.#getUpdateBashConfirmResult(toolUse, false))
594+
await chatResultStream.writeResultBlock(
595+
this.#getUpdateBashConfirmResult(toolUse.toolUseId, false)
596+
)
576597
}
577598
throw err
578599
}
@@ -592,9 +613,9 @@ export class AgenticChatController implements ChatHandlers {
592613
return results
593614
}
594615

595-
#getUpdateBashConfirmResult(toolUse: ToolUse, isAccept: boolean): ChatResult {
616+
#getUpdateBashConfirmResult(toolUseId: string, isAccept: boolean): ChatResult {
596617
return {
597-
messageId: toolUse.toolUseId,
618+
messageId: toolUseId,
598619
type: 'tool',
599620
body: '',
600621
header: {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ChatResult } from '@aws/language-server-runtimes/protocol'
33
import { AgenticChatResultStream } from './agenticChatResultStream'
44
import { TestFeatures } from '@aws/language-server-runtimes/testing'
55

6+
// TODO: renable this test suite and update the following tests.
67
xdescribe('agenticChatResponse', function () {
78
let output: (ChatResult | string)[] = []
89
const logging = new TestFeatures().logging
@@ -69,4 +70,17 @@ xdescribe('agenticChatResponse', function () {
6970

7071
assert.ok(chatResultStream.getResultStreamWriter())
7172
})
73+
74+
it('allows blocks to overwritten on id', async function () {
75+
const first = await chatResultStream.writeResultBlock({ body: 'first' })
76+
const second = await chatResultStream.writeResultBlock({ body: 'second' })
77+
await chatResultStream.writeResultBlock({ body: 'third' })
78+
79+
await chatResultStream.overwriteResultBlock({ body: 'fourth' }, first)
80+
await chatResultStream.overwriteResultBlock({ body: 'fifth' }, second)
81+
82+
assert.deepStrictEqual(chatResultStream.getResult(), {
83+
body: `fourth${AgenticChatResultStream.resultDelimiter}fifth${AgenticChatResultStream.resultDelimiter}third`,
84+
})
85+
})
7286
})

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatResultStream.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,25 @@ export class AgenticChatResultStream {
9797
}, result)
9898
}
9999

100-
async writeResultBlock(result: ChatMessage) {
100+
/**
101+
* Add a block to the message block store and send it to the client.
102+
* @param result the blockId associated with the block such that it can be overwritten later
103+
* @returns
104+
*/
105+
async writeResultBlock(result: ChatMessage): Promise<number> {
101106
this.#state.chatResultBlocks.push(result)
102107
await this.#sendProgress(this.getResult(result.messageId))
108+
return this.#state.chatResultBlocks.length - 1
109+
}
110+
111+
/**
112+
* Overwrites a specific blockId and re-sends the resulting blocks to the client.
113+
* @param result
114+
* @param blockId
115+
*/
116+
async overwriteResultBlock(result: ChatMessage, blockId: number) {
117+
this.#state.chatResultBlocks[blockId] = result
118+
await this.#sendProgress(this.getResult(result.messageId))
103119
}
104120

105121
getResultStreamWriter(): ResultStreamWriter {

0 commit comments

Comments
 (0)