diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e2ae92303c..c2a9a56c93 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,9 +1,9 @@ { - "chat-client": "0.1.55", - "core/aws-lsp-core": "0.0.21", - "server/aws-lsp-antlr4": "0.1.25", - "server/aws-lsp-codewhisperer": "0.0.124", - "server/aws-lsp-json": "0.1.26", - "server/aws-lsp-partiql": "0.0.23", - "server/aws-lsp-yaml": "0.1.26" + "chat-client": "0.1.56", + "core/aws-lsp-core": "0.0.22", + "server/aws-lsp-antlr4": "0.1.26", + "server/aws-lsp-codewhisperer": "0.0.127", + "server/aws-lsp-json": "0.1.27", + "server/aws-lsp-partiql": "0.0.24", + "server/aws-lsp-yaml": "0.1.27" } diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 8f307b2695..e8fa26028c 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.75.0" + "agenticChat": "1.78.0" } diff --git a/chat-client/CHANGELOG.md b/chat-client/CHANGELOG.md index b6ec7991b8..ab8096741e 100644 --- a/chat-client/CHANGELOG.md +++ b/chat-client/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.1.56](https://github.com/Amazon-Q-Developer/language-servers/compare/chat-client/v0.1.55...chat-client/v0.1.56) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + ## [0.1.55](https://github.com/aws/language-servers/compare/chat-client/v0.1.54...chat-client/v0.1.55) (2026-06-30) diff --git a/chat-client/package.json b/chat-client/package.json index 1def952379..b3a1aa46b7 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -1,6 +1,6 @@ { "name": "@aws/chat-client", - "version": "0.1.55", + "version": "0.1.56", "description": "AWS Chat Client", "main": "out/index.js", "repository": { diff --git a/core/aws-lsp-core/CHANGELOG.md b/core/aws-lsp-core/CHANGELOG.md index 868606d0ac..9967096253 100644 --- a/core/aws-lsp-core/CHANGELOG.md +++ b/core/aws-lsp-core/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.22](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-core/v0.0.21...lsp-core/v0.0.22) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + ## [0.0.21](https://github.com/aws/language-servers/compare/lsp-core/v0.0.20...lsp-core/v0.0.21) (2026-02-17) diff --git a/core/aws-lsp-core/package.json b/core/aws-lsp-core/package.json index 0874682d04..9aec44109c 100644 --- a/core/aws-lsp-core/package.json +++ b/core/aws-lsp-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-core", - "version": "0.0.21", + "version": "0.0.22", "description": "Core library, contains common code and utilities", "main": "out/index.js", "repository": { diff --git a/package-lock.json b/package-lock.json index 3f193aa49e..81579468b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -251,7 +251,7 @@ }, "chat-client": { "name": "@aws/chat-client", - "version": "0.1.55", + "version": "0.1.56", "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "0.1.71", @@ -302,7 +302,7 @@ }, "core/aws-lsp-core": { "name": "@aws/lsp-core", - "version": "0.0.21", + "version": "0.0.22", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", @@ -30287,11 +30287,11 @@ }, "server/aws-lsp-antlr4": { "name": "@aws/lsp-antlr4", - "version": "0.1.25", + "version": "0.1.26", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21" + "@aws/lsp-core": "^0.0.22" }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.24.1", @@ -30351,7 +30351,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.124", + "version": "0.0.127", "bundleDependencies": [ "@amzn/codewhisperer", "@amzn/codewhisperer-runtime", @@ -30372,7 +30372,7 @@ "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "0.1.71", "@aws/language-server-runtimes": "^0.3.21", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "@modelcontextprotocol/sdk": "^1.23.0", "@mozilla/readability": "^0.6.0", "@smithy/node-http-handler": "^2.5.0", @@ -30642,11 +30642,11 @@ }, "server/aws-lsp-json": { "name": "@aws/lsp-json", - "version": "0.1.26", + "version": "0.1.27", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" }, @@ -30718,7 +30718,7 @@ }, "server/aws-lsp-partiql": { "name": "@aws/lsp-partiql", - "version": "0.0.23", + "version": "0.0.24", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", @@ -30768,12 +30768,12 @@ }, "server/aws-lsp-yaml": { "name": "@aws/lsp-yaml", - "version": "0.1.26", + "version": "0.1.27", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", "yaml-language-server": "1.13.0" diff --git a/server/aws-lsp-antlr4/CHANGELOG.md b/server/aws-lsp-antlr4/CHANGELOG.md index 85c4542684..ad6bffd9a3 100644 --- a/server/aws-lsp-antlr4/CHANGELOG.md +++ b/server/aws-lsp-antlr4/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.26](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-antlr4/v0.1.25...lsp-antlr4/v0.1.26) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.21 to ^0.0.22 + ## [0.1.25](https://github.com/aws/language-servers/compare/lsp-antlr4/v0.1.24...lsp-antlr4/v0.1.25) (2026-02-17) diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index 918f2ef5f7..ac12cf53b5 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-antlr4", - "version": "0.1.25", + "version": "0.1.26", "description": "ANTLR4 language server", "main": "out/index.js", "repository": { @@ -29,7 +29,7 @@ }, "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21" + "@aws/lsp-core": "^0.0.22" }, "peerDependencies": { "antlr4-c3": ">=3.4 < 4", diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index 22eb2e02df..19f7b5fd2e 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## [0.0.127](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.126...lsp-codewhisperer/v0.0.127) (2026-08-24) + + +### Bug Fixes + +* **amazonq:** cover merged env and headers in MCP consent fingerprint ([#2851](https://github.com/Amazon-Q-Developer/language-servers/issues/2851)) ([#2853](https://github.com/Amazon-Q-Developer/language-servers/issues/2853)) ([529aed4](https://github.com/Amazon-Q-Developer/language-servers/commit/529aed43259503cf71b475fb3496de7bfab25f17)) +* beam - flat-named transformed zips + lightweight discovery + IsLbvPending ([#2849](https://github.com/Amazon-Q-Developer/language-servers/issues/2849)) ([863c5bf](https://github.com/Amazon-Q-Developer/language-servers/commit/863c5bf00a6c30ec7658056a155f1ca1005127e6)) +* surface backend interactive mode in getTransformInfo ([836b756](https://github.com/Amazon-Q-Developer/language-servers/commit/836b756ee2d8a553d9b26cf095e12979141e2367)) + +## [0.0.126](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.125...lsp-codewhisperer/v0.0.126) (2026-08-20) + + +### Bug Fixes + +* **amazonq:** narrow tool-use truncation detection and classify retries in telemetry ([#2847](https://github.com/Amazon-Q-Developer/language-servers/issues/2847)) ([1eb2f9f](https://github.com/Amazon-Q-Developer/language-servers/commit/1eb2f9f270c479ca457658e22f2ef0d06f6517ca)) +* scope and forward stepId for the planning-branch LBV HITL ([#2835](https://github.com/Amazon-Q-Developer/language-servers/issues/2835)) ([9f8bb89](https://github.com/Amazon-Q-Developer/language-servers/commit/9f8bb8943c41edc105c460d4b2c60836183cdd2b)) + +## [0.0.125](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-codewhisperer/v0.0.124...lsp-codewhisperer/v0.0.125) (2026-08-18) + + +### Features + +* **amazonq:** initialize Q services when credentials arrive ([#2840](https://github.com/Amazon-Q-Developer/language-servers/issues/2840)) ([4e0d3b0](https://github.com/Amazon-Q-Developer/language-servers/commit/4e0d3b0d69e0aa7a230f64a1b168a09bcfd4b7cd)) + + +### Bug Fixes + +* **amazonq:** retry truncated tool-use streams instead of dropping them silently ([#2841](https://github.com/Amazon-Q-Developer/language-servers/issues/2841)) ([368a099](https://github.com/Amazon-Q-Developer/language-servers/commit/368a099cc9b5cc3cf7bb1f2298718824a1ab1118)) +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.21 to ^0.0.22 + ## [0.0.124](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.123...lsp-codewhisperer/v0.0.124) (2026-08-13) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 120744dc41..af7b4aaa17 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.124", + "version": "0.0.127", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { @@ -39,7 +39,7 @@ "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "0.1.71", "@aws/language-server-runtimes": "^0.3.21", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "@modelcontextprotocol/sdk": "^1.23.0", "@mozilla/readability": "^0.6.0", "@smithy/node-http-handler": "^2.5.0", diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts index be67753637..ac991235ed 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -180,6 +180,10 @@ import { OUTPUT_LIMIT_EXCEEDS_PARTIAL_MSG, RESPONSE_TIMEOUT_MS, RESPONSE_TIMEOUT_PARTIAL_MSG, + INCOMPLETE_TOOL_USE_RETRY_LIMIT_MSG, + MAX_INCOMPLETE_TOOL_USE_RETRIES, + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING, + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED, COMPACTION_BODY, COMPACTION_HEADER_BODY, DEFAULT_MACOS_RUN_SHORTCUT, @@ -1427,6 +1431,9 @@ export class AgenticChatController implements ChatHandlers { let iterationCount = 0 let shouldDisplayMessage = true let currentRequestCount = 0 + // Number of consecutive responses that produced an incomplete tool-use input. Bounded + // so a model that keeps truncating cannot keep the agent loop running indefinitely. + let consecutiveIncompleteToolUses = 0 const pinnedContext = additionalContext?.filter(item => item.pinned) metric.recordStart() @@ -1645,6 +1652,8 @@ export class AgenticChatController implements ChatHandlers { let toolResults: ToolResult[] session.setConversationType('AgenticChatWithToolUse') if (result.success) { + // A complete tool use was received, so the incomplete-input streak is over. + consecutiveIncompleteToolUses = 0 // Process tool uses and update the request input for the next iteration toolResults = await this.processToolUses( pendingToolUses, @@ -1689,6 +1698,21 @@ export class AgenticChatController implements ChatHandlers { status: ToolResultStatus.ERROR, content: [{ text: result.error }], })) + // Classify the failure *before* emitting telemetry. An incomplete tool-use input is + // retried inside this loop and usually recovers within the same user turn, so it is + // not a user-visible failure and should not depress a per-call success rate. Because + // the retry budget is bounded we already know here whether this iteration will be + // retried, which lets us distinguish the transient case from the terminal give-up. + const isIncompleteToolUse = result.error.startsWith('ToolUse input is invalid JSON:') + let willRetryIncompleteToolUse = false + let invokeLlmReason: string | undefined + if (isIncompleteToolUse) { + consecutiveIncompleteToolUses++ + willRetryIncompleteToolUse = consecutiveIncompleteToolUses <= MAX_INCOMPLETE_TOOL_USE_RETRIES + invokeLlmReason = willRetryIncompleteToolUse + ? INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING + : INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED + } this.#telemetryController.emitAgencticLoop_InvokeLLM( response.$metadata.requestId!, conversationId, @@ -1704,9 +1728,25 @@ export class AgenticChatController implements ChatHandlers { this.#timeBetweenChunks, session.pairProgrammingMode, this.#abTestingAllocation?.experimentName, - this.#abTestingAllocation?.userVariation + this.#abTestingAllocation?.userVariation, + invokeLlmReason ) - if (result.error.startsWith('ToolUse input is invalid JSON:')) { + if (isIncompleteToolUse) { + if (!willRetryIncompleteToolUse) { + // The model has failed to produce a complete tool request several times + // in a row. Retrying again is unlikely to help and would keep the agent + // loop running, so stop and surface a real error to the user instead. + this.#features.logging.error( + `Giving up after ${consecutiveIncompleteToolUses} consecutive incomplete tool uses: ${result.error}` + ) + await chatResultStream.updateOngoingProgressResult('Error') + finalResult = { + success: false, + error: INCOMPLETE_TOOL_USE_RETRY_LIMIT_MSG, + data: result.data, + } + break + } content = 'Your toolUse input is incomplete, try again. If the error happens consistently, break this task down into multiple tool uses with smaller input. Do not apologize.' shouldDisplayMessage = false @@ -4668,7 +4708,12 @@ export class AgenticChatController implements ChatHandlers { cwsprChatResponseLength: chatEventParser.body?.length ?? 0, }) - return chatEventParser.getResult() + // Use finalize() (not getResult()) so a tool use whose stream ended before its + // terminating `stop` event is surfaced as an incomplete-input error and retried, + // rather than being silently dropped and reported as a successful turn. The abort + // state is passed in so a timed-out/cancelled request is not misreported as a + // truncated tool input. + return chatEventParser.finalize({ aborted: abortSignal?.aborted === true }) } /** diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts index 917ae28179..0fb7640106 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.test.ts @@ -304,4 +304,94 @@ describe('AgenticChatEventParser', () => { assert.deepStrictEqual(chatEventParser.getResult(), { success: true, data: expectedData }) }) + + it('finalize flags an unterminated tool use (no stop event) as incomplete and marks it stopped', () => { + const chatEventParser = new AgenticChatEventParser(mockMessageId, new Metric(), logging) + + // A tool use whose input streams in fragments but never receives a `stop` event + // (e.g. the response hit the output-token limit mid tool-input). + chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '{"path":"out.py","fileText":"import os', + stop: false, + }, + }) + const midStream = chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '\\nprint(1)', + stop: false, + }, + }) + + // Mid-stream this looks like a clean success and would be filtered out (dropped) by the loop. + assert.strictEqual(midStream.success, true) + assert.strictEqual(midStream.data?.toolUses['tool-1'].stop, false) + + // After the stream ends, finalize() surfaces it as an error and marks it stopped so it + // survives the pending-tool-use filter and is routed into the existing retry path. + const result = chatEventParser.finalize() + assert.strictEqual(result.success, false) + assert.ok(result.error?.startsWith('ToolUse input is invalid JSON:')) + assert.strictEqual(result.data?.toolUses['tool-1'].stop, true) + }) + + it('finalize leaves a properly stopped tool use untouched', () => { + const chatEventParser = new AgenticChatEventParser(mockMessageId, new Metric(), logging) + + chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '{"path":"out.py","fileText":"print(1)"}', + stop: true, + }, + }) + + const result = chatEventParser.finalize() + assert.strictEqual(result.success, true) + assert.strictEqual(result.data?.toolUses['tool-1'].stop, true) + assert.deepStrictEqual(result.data?.toolUses['tool-1'].input, { path: 'out.py', fileText: 'print(1)' }) + }) + + it('finalize does not flag an unterminated tool use when no input was received', () => { + const chatEventParser = new AgenticChatEventParser(mockMessageId, new Metric(), logging) + + // The model announced a tool use but never streamed any input before the stream ended. + // There is nothing to truncate, so this must be dropped rather than retried. + chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '', + stop: false, + }, + }) + + const result = chatEventParser.finalize() + assert.strictEqual(result.success, true) + assert.strictEqual(result.data?.toolUses['tool-1'].stop, false) + }) + + it('finalize does not flag an unterminated tool use when the request was aborted', () => { + const chatEventParser = new AgenticChatEventParser(mockMessageId, new Metric(), logging) + + chatEventParser.processPartialEvent({ + toolUseEvent: { + toolUseId: 'tool-1', + name: 'fsWrite', + input: '{"path":"out.py","fileText":"import os', + stop: false, + }, + }) + + // An aborted request (response-processing timeout or cancellation) is expected to end + // without a stop event, so it must not be reported as a truncated tool input. + const result = chatEventParser.finalize({ aborted: true }) + assert.strictEqual(result.success, true) + assert.strictEqual(result.data?.toolUses['tool-1'].stop, false) + }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts index 6878e19caf..12da3654e7 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatEventParser.ts @@ -229,4 +229,62 @@ export class AgenticChatEventParser implements ChatResult { data: chatResultWithMetadata, } } + + /** + * Finalizes parsing after the response stream has been fully consumed. + * + * A tool use only becomes executable once its terminating `stop` event arrives — that + * is also the only point at which its streamed input is JSON-parsed. If the stream ends + * before that event (for example, the response reaches the output-token limit mid + * tool-input), the tool use is left with `stop === false` and no error is recorded, so + * `getResult()` reports success. The agentic loop then filters it out (only stopped tool + * uses are treated as pending) and completes the turn without ever running the tool — a + * silent no-op with no error and no retry. + * + * To avoid that, a genuinely *truncated tool input* is recorded as an error (reusing the + * malformed-JSON prefix so it is handled by the existing recovery branch) and marked + * `stop` so it survives the pending-tool-use filter and the model is re-prompted to split + * the work into smaller tool uses. + * + * Two other situations also reach this point and must keep the previous behaviour of being + * dropped, otherwise a benign end-of-stream is reported as a failure and re-prompted — + * which inflates failure metrics and can re-run the agent loop without making progress: + * + * - the request was aborted (response-processing timeout or cancellation): the missing + * `stop` event is expected, and the abort is already surfaced by its own path; + * - no tool input was received at all: the model never committed to a tool call, so + * there is nothing to truncate and nothing to retry. + */ + public finalize(options?: { aborted?: boolean }): Result { + for (const toolUseId of Object.keys(this.toolUses)) { + const toolUse = this.toolUses[toolUseId] + if (toolUse.stop) { + continue + } + + const partialInput = typeof toolUse.input === 'string' ? toolUse.input : '' + + if (options?.aborted || partialInput.length === 0) { + // Leave the tool use unstopped so it is filtered out downstream, as before. + this.#logging.debug( + `ToolUse ${toolUseId} (${toolUse.name}) ended without a stop event and is not a truncated input (aborted=${!!options?.aborted}, receivedCharacters=${partialInput.length}); dropping it` + ) + continue + } + + this.#logging.error( + `ToolUse ${toolUseId} (${toolUse.name}) stream ended before a stop event after ${partialInput.length} characters; input was truncated` + ) + // Reuse the malformed-JSON error prefix so this routes into the same recovery + // branch in the agentic loop. Keep the message short so the (potentially very + // large) partial input is not echoed back to the model in the tool result. + this.error = `ToolUse input is invalid JSON: incomplete tool input, stream ended after ${partialInput.length} characters before completion.` + this.toolUses[toolUseId] = { + ...toolUse, + input: {}, + stop: true, + } + } + return this.getResult() + } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts index 901e08964b..34e5a0aecb 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts @@ -2,6 +2,26 @@ export const GENERIC_ERROR_MS = 'An unexpected error occurred, check the logs for more information.' export const OUTPUT_LIMIT_EXCEEDS_PARTIAL_MSG = 'output exceeds maximum character limit of' export const RESPONSE_TIMEOUT_PARTIAL_MSG = 'Response processing timed out after' +export const INCOMPLETE_TOOL_USE_RETRY_LIMIT_MSG = + 'Amazon Q could not complete a tool request because its input was cut off repeatedly. Try asking for a smaller change, or generating the content in sections.' + +// Retry limits +// Maximum number of consecutive responses with an incomplete tool-use input that will be +// retried before giving up and surfacing an error to the user. +export const MAX_INCOMPLETE_TOOL_USE_RETRIES = 3 + +// Telemetry reason codes reported on the amazonq_invokeLLM metric. +// +// An incomplete tool-use input is retried inside the agent loop and usually recovers within the +// same user turn, so it is not a user-visible failure. These codes let downstream metrics exclude +// those transient iterations from per-call success rates while still counting the terminal +// give-up, which the user does see, as a genuine failure. `result` stays 'Failed' in both cases +// so raw failure counts remain intact for diagnostics. +// +// NOTE: these string values form part of the telemetry contract and are consumed by a downstream +// metrics pipeline. Do not rename either value without updating that consumer first. +export const INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING = 'INCOMPLETE_TOOL_USE_RETRYING' +export const INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED = 'INCOMPLETE_TOOL_USE_EXHAUSTED' // Time Constants export const LOADING_THRESHOLD_MS = 2000 diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts index 79696b5f9d..b638968139 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.test.ts @@ -8,6 +8,8 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' import { + effectiveEnv, + effectiveHeaders, fingerprintServerConfig, fingerprintWorkspace, hasApproval, @@ -74,6 +76,71 @@ describe('mcpConsentStore', () => { const b: MCPServerConfig = { url: 'https://b.example' } expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) }) + + // __additionalEnv__ / __additionalHeaders__ are merged into the spawn env/headers, + // so they must be covered by the fingerprint alongside the raw fields — otherwise a + // post-approval config edit to those fields would not re-prompt. + it('differs when __additionalEnv__ is added', () => { + const a: MCPServerConfig = { command: 'npx', args: ['-y', 's'], env: { LOG_LEVEL: 'info' } } + const b: MCPServerConfig = { + ...a, + __additionalEnv__: { EXTRA: '1' }, + } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when __additionalEnv__ overrides an existing env value', () => { + const a: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' } } + const b: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' }, __additionalEnv__: { FOO: '2' } } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when headers change', () => { + const a: MCPServerConfig = { url: 'https://a.example', headers: { Authorization: 'Bearer good' } } + const b: MCPServerConfig = { url: 'https://a.example', headers: { Authorization: 'Bearer attacker' } } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + it('differs when __additionalHeaders__ overrides an existing header', () => { + const a: MCPServerConfig = { url: 'https://a.example', headers: { Authorization: 'Bearer good' } } + const b: MCPServerConfig = { + url: 'https://a.example', + headers: { Authorization: 'Bearer good' }, + __additionalHeaders__: { Authorization: 'Bearer attacker' }, + } + expect(fingerprintServerConfig(a)).to.not.equal(fingerprintServerConfig(b)) + }) + + // Consent is about what will execute, not how the config is spelled: two configs + // that spawn the process with the identical effective env share a fingerprint. + it('hashes the merged spawn env, so the same effective env matches either field', () => { + const viaEnv: MCPServerConfig = { command: 'sh', args: [], env: { FOO: '1' } } + const viaAdditional: MCPServerConfig = { command: 'sh', args: [], __additionalEnv__: { FOO: '1' } } + expect(fingerprintServerConfig(viaEnv)).to.equal(fingerprintServerConfig(viaAdditional)) + }) + + it('is stable regardless of __additionalEnv__ key order', () => { + const a: MCPServerConfig = { command: 'sh', args: [], __additionalEnv__: { A: '1', B: '2' } } + const b: MCPServerConfig = { command: 'sh', args: [], __additionalEnv__: { B: '2', A: '1' } } + expect(fingerprintServerConfig(a)).to.equal(fingerprintServerConfig(b)) + }) + }) + + describe('effectiveEnv / effectiveHeaders', () => { + it('merges __additionalEnv__ over env, matching the spawn-time merge', () => { + const cfg: MCPServerConfig = { env: { A: '1', B: '2' }, __additionalEnv__: { B: 'override', C: '3' } } + expect(effectiveEnv(cfg)).to.deep.equal({ A: '1', B: 'override', C: '3' }) + }) + + it('merges __additionalHeaders__ over headers', () => { + const cfg: MCPServerConfig = { headers: { X: '1' }, __additionalHeaders__: { X: '2', Y: '3' } } + expect(effectiveHeaders(cfg)).to.deep.equal({ X: '2', Y: '3' }) + }) + + it('returns an empty object when nothing is set', () => { + expect(effectiveEnv({})).to.deep.equal({}) + expect(effectiveHeaders({})).to.deep.equal({}) + }) }) describe('fingerprintWorkspace', () => { @@ -183,11 +250,34 @@ describe('mcpConsentStore', () => { const storeDir = path.join(tmpHome, '.aws', 'amazonq') fs.mkdirSync(storeDir, { recursive: true }) fs.writeFileSync(path.join(storeDir, 'mcp-approvals.json'), JSON.stringify({ version: 999, approvals: [] })) - // record should still work (overwrites with v1) + // record should still work (overwrites with the current version) await recordApproval(workspace, logger, 'poc', cfg, configPath) expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.true }) + // STORE_VERSION 1 -> 2: v1 fingerprints were computed over a narrower field set and + // cannot be trusted to cover the merged env/headers, so they are discarded and the + // user is re-prompted once per workspace-scoped server after upgrade. + it('discards legacy v1 approvals so the user is re-prompted once after upgrade', async () => { + const storeDir = path.join(tmpHome, '.aws', 'amazonq') + fs.mkdirSync(storeDir, { recursive: true }) + fs.writeFileSync( + path.join(storeDir, 'mcp-approvals.json'), + JSON.stringify({ + version: 1, + approvals: [ + { + serverName: 'poc', + fingerprint: fingerprintServerConfig(cfg), + workspaceHash: fingerprintWorkspace(configPath), + approvedAt: new Date().toISOString(), + }, + ], + }) + ) + expect(await hasApproval(workspace, logger, 'poc', cfg, configPath)).to.be.false + }) + it('treats a malformed store as empty', async () => { const storeDir = path.join(tmpHome, '.aws', 'amazonq') fs.mkdirSync(storeDir, { recursive: true }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts index b9230a6655..45a4f252fc 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpConsentStore.ts @@ -9,7 +9,11 @@ import type { Workspace, Logging } from '@aws/language-server-runtimes/server-in import type { MCPServerConfig } from './mcpTypes' const APPROVALS_FILE = 'mcp-approvals.json' -const STORE_VERSION = 1 +// v2: the fingerprint now covers the fully-merged spawn environment and headers +// (see fingerprintServerConfig). Bumping the version discards v1 approvals, which +// were computed over a narrower field set, so every workspace-scoped server is +// re-consented once after upgrade. +const STORE_VERSION = 2 interface Approval { serverName: string @@ -23,17 +27,49 @@ interface ApprovalStore { approvals: Approval[] } +function sortedRecord(rec?: Record): Record { + return rec ? Object.fromEntries(Object.entries(rec).sort(([a], [b]) => a.localeCompare(b))) : {} +} + +/** + * The environment the stdio transport will actually spawn the server with, as far + * as the config controls it. Mirrors the merge in McpManager (`cfg.env` overlaid by + * `cfg.__additionalEnv__`). + * + * `__additionalEnv__` carries workspace/agent-level `env` for registry servers and is + * NOT folded into `cfg.env`, so it must be merged here or it escapes the fingerprint. + */ +export function effectiveEnv(cfg: MCPServerConfig): Record { + return sortedRecord({ ...(cfg.env ?? {}), ...(cfg.__additionalEnv__ ?? {}) }) +} + +/** + * The headers the HTTP/SSE transport will actually send, as far as the config + * controls it. Mirrors the merge in McpManager (`cfg.headers` overlaid by + * `cfg.__additionalHeaders__`). + */ +export function effectiveHeaders(cfg: MCPServerConfig): Record { + return sortedRecord({ ...(cfg.headers ?? {}), ...(cfg.__additionalHeaders__ ?? {}) }) +} + /** * SHA-256 of a canonical JSON form of the server's execution-relevant fields. - * Any change to command/args/env/url yields a new fingerprint, invalidating + * Any change to command/args/env/url/headers yields a new fingerprint, invalidating * prior approvals — so mutation of the config re-prompts. + * + * `env` and `headers` are hashed in their *merged* form (see effectiveEnv / + * effectiveHeaders) so every field that reaches the spawned process is covered by + * consent. Two configs that spawn an identical process share a fingerprint regardless + * of which field supplied a value: consent is about what will execute, not how the + * config is spelled. */ export function fingerprintServerConfig(cfg: MCPServerConfig): string { const canonical = { command: cfg.command ?? null, args: cfg.args ?? [], - env: cfg.env ? Object.fromEntries(Object.entries(cfg.env).sort(([a], [b]) => a.localeCompare(b))) : {}, + env: effectiveEnv(cfg), url: cfg.url ?? null, + headers: effectiveHeaders(cfg), } return 'sha256:' + createHash('sha256').update(JSON.stringify(canonical)).digest('hex') } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts index 61e7ea75fe..bde82719d7 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts @@ -42,7 +42,14 @@ import { Mutex } from 'async-mutex' import path = require('path') import { URI } from 'vscode-uri' import { MessageType } from '@aws/language-server-runtimes/protocol' -import { hasApproval, recordApproval, removeApproval, fingerprintServerConfig } from './mcpConsentStore' +import { + hasApproval, + recordApproval, + removeApproval, + fingerprintServerConfig, + effectiveEnv, + effectiveHeaders, +} from './mcpConsentStore' import { sanitizeInput } from '../../../../shared/utils' import { ProfileStatusMonitor } from './profileStatusMonitor' import { OAuthClient } from './mcpOauthClient' @@ -440,6 +447,17 @@ export class McpManager { ) if (!approved) { const cmdLine = [cfg.command ?? cfg.url ?? '(none)', ...(cfg.args ?? [])].join(' ').slice(0, 200) + // Surface the environment variables and headers the server will actually be + // launched with. Names only, never values: a config may legitimately hold + // tokens, and this string is shown in a dialog and written to logs. Values are + // covered by the fingerprint, so any value change re-prompts. + const envKeys = Object.keys(effectiveEnv(cfg)) + const headerNames = Object.keys(effectiveHeaders(cfg)) + const envLine = + envKeys.length > 0 + ? `Environment variables: ${envKeys.join(', ').slice(0, 200)}\n` + : `Environment variables: (none)\n` + const headerLine = headerNames.length > 0 ? `Headers: ${headerNames.join(', ').slice(0, 200)}\n` : '' const allowBtn = { title: 'Allow for this server' } const denyBtn = { title: 'Deny' } let choice: { title: string } | null | undefined @@ -451,8 +469,13 @@ export class McpManager { `A workspace configuration file wants to start an MCP server.\n` + `Server: ${serverName}\n` + `Command: ${cmdLine}\n` + + envLine + + headerLine + `Source: ${configPath}\n\n` + - `Running this server executes the above command on your machine. ` + + `Running this server executes the above command on your machine, ` + + `with the environment variables listed above. ` + + `Review them in the configuration file if you are unsure — variables such as ` + + `NODE_OPTIONS can cause additional code to run. ` + `Only allow if you trust the authors of this workspace.\n\n` + `Your choice will be remembered for this workspace. ` + `If you allow, you won't be asked again unless the server configuration changes.`, diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.test.ts index 07cac09984..2c99b4dcf1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.test.ts @@ -5,6 +5,10 @@ import { CONVERSATION_ID_METRIC_KEY, ChatTelemetryController } from './chatTelem import assert = require('assert') import { ChatUIEventName } from './clientTelemetry' import { TelemetryService } from '../../../shared/telemetry/telemetryService' +import { + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED, + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING, +} from '../../agenticChat/constants/constants' describe('TelemetryController', () => { const mockTabId = 'mockTabId' @@ -163,6 +167,60 @@ describe('TelemetryController', () => { }) }) + describe('emitAgencticLoop_InvokeLLM reason', () => { + // The `reason` field lets downstream metrics tell a transient, self-recovering agent-loop + // failure apart from a terminal one, so it must survive the emit unchanged. + const emitInvokeLLM = (reason?: string) => + telemetryController.emitAgencticLoop_InvokeLLM( + 'mockRequestId', + mockConversationId, + 'AgenticChatWithToolUse', + undefined, + undefined, + 'Failed', + '1.0.0', + 'mockModelId', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + reason + ) + + it('forwards the reason when one is supplied', () => { + emitInvokeLLM(INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING) + + sinon.assert.calledOnce(testFeatures.telemetry.emitMetric) + const emitted = testFeatures.telemetry.emitMetric.firstCall.firstArg + assert.strictEqual(emitted.name, ChatTelemetryEventName.AgencticLoop_InvokeLLM) + assert.strictEqual(emitted.data.reason, INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING) + // `result` must stay 'Failed' so raw failure counts are unchanged for diagnostics. + assert.strictEqual(emitted.data.result, 'Failed') + }) + + it('distinguishes the terminal give-up from the retrying case', () => { + emitInvokeLLM(INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED) + + const emitted = testFeatures.telemetry.emitMetric.firstCall.firstArg + assert.strictEqual(emitted.data.reason, INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED) + assert.notStrictEqual( + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_EXHAUSTED, + INVOKE_LLM_REASON_INCOMPLETE_TOOL_USE_RETRYING + ) + }) + + it('leaves reason undefined for failures with no specific classification', () => { + emitInvokeLLM(undefined) + + const emitted = testFeatures.telemetry.emitMetric.firstCall.firstArg + assert.strictEqual(emitted.data.reason, undefined) + assert.strictEqual(emitted.data.result, 'Failed') + }) + }) + describe('enqueueCodeDiffEntry', () => { const mockTextDocumentUri = 'file:///path/to/file.ts' const mockCode = 'const x = 42;' diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts index e2f336e0ae..ed4634c69e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts @@ -197,7 +197,8 @@ export class ChatTelemetryController { cwsprChatTimeBetweenChunks?: number[], agenticCodingMode?: boolean, experimentName?: string, - userVariation?: string + userVariation?: string, + reason?: string ) { this.#telemetry.emitMetric({ name: ChatTelemetryEventName.AgencticLoop_InvokeLLM, @@ -218,6 +219,7 @@ export class ChatTelemetryController { modelId, experimentName: experimentName, userVariation: userVariation, + reason, }, }) } diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts index 21622fea61..f010011550 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -146,6 +146,10 @@ export interface AtxGetTransformInfoResponse { MissingPackageJsonPath?: string | null DiffApplyFailed?: boolean DiffApplyFailedStepIds?: string[] + // Interactive mode as resolved from the backend job objective (interactive_mode). + // Surfaced so the IDE can restore the correct mode instead of relying on its local + // settings store, which may be missing/stale (e.g. cold restart on another machine). + InteractiveMode?: InteractiveMode } /** diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts index 65e29c2642..b74e6a8155 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts @@ -276,11 +276,17 @@ export const AtxNetTransformServerToken = // ---- Beam to IDE ---- case AtxListBeamedReposCommand: { // PascalCase params/return to match the C# ExecuteCommandParams convention. - const { WorkspaceId, ParentJobId } = params as any + // Lightweight (optional): the poll-tick refresh sets it to skip the beam-map + // download scan (throttle-safe). + const { WorkspaceId, ParentJobId, Lightweight } = params as any if (!WorkspaceId || !ParentJobId) { throw new Error('WorkspaceId and ParentJobId are required for listBeamedRepos') } - const repos = await atxTransformHandler.listBeamedRepos(WorkspaceId, ParentJobId) + const repos = await atxTransformHandler.listBeamedRepos( + WorkspaceId, + ParentJobId, + Lightweight === true + ) return { Repos: repos } } case AtxDownloadBeamArtifactCommand: { diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts index 70649ff0fc..04de8649da 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -107,6 +107,9 @@ interface BeamedRepoInfo { BeamTargetFramework: string BeamScenario: string IsLbvOpen: boolean + // Beamed but LBV HITL not created yet (no beamed node / no LBV child / no plan). IDE shows Load + // only when IsLbvOpen && !IsLbvPending, so a repo can't be Loaded before its HITL exists. + IsLbvPending: boolean } // Bounds for the beam-map candidate scan (serial download+parse per candidate). @@ -572,9 +575,9 @@ export class ATXTransformHandler { * (category HITL_FROM_AGENT) listing repos the user chose to beam. We find it * by listing the parent job's artifacts and picking the beam-map JSON. */ - async listBeamedRepos(workspaceId: string, parentJobId: string): Promise { + async listBeamedRepos(workspaceId: string, parentJobId: string, lightweight = false): Promise { try { - this.logging.log(`ATX: listBeamedRepos for parentJobId=${parentJobId}`) + this.logging.log(`ATX: listBeamedRepos for parentJobId=${parentJobId} lightweight=${lightweight}`) if (!this.atxClient && !(await this.initializeAtxClient())) { throw new Error('ATX client not initialized') } @@ -623,7 +626,15 @@ export class ATXTransformHandler { for (const a of transformedZips) { const path = a.fileMetadata?.path || '' const m = path.match(ZIP_RE) - const repoName = m ? m[2] : (path.split('/')[0] || '').replace(/_[0-9a-f]{6,}$/i, '') + // Repo name from the transformed-source zip, handling both producer shapes: subdir + // "_/.zip" (ZIP_RE) and flat "_[_suffix].zip". Flat must + // be handled or a flat-named repo never matches its beam-status entry and drops. + const repoName = m + ? m[2] + : (path.split('/').pop() || '') + .replace(/\.zip$/i, '') + .replace(/_(transformed[_-]?source|transformed|migrated|output|source)$/i, '') + .replace(/_[0-9a-f]{6,}$/i, '') if (!repoName || !a.artifactId) continue const key = repoName.toLowerCase() if (!zipByRepo.has(key)) { @@ -654,10 +665,15 @@ export class ATXTransformHandler { // source bundles, whose paths match "_/.zip" (ZIP_RE). Anything // else (including an unnamed/empty-path zip = the beam-map) is a candidate. let beamMapRepos: BeamMapRepo[] | null = null - const allCandidates = artifacts.filter(a => { - const p = (a.fileMetadata?.path || '').toLowerCase() - return !ZIP_RE.test(p) // keep non-transformed-bundle artifacts (incl. the beam-map zip) - }) + // Lightweight (poll refresh): skip the beam-map download scan (throttle-safe) — use only + // beam-status + one plan fetch. stepId isn't resolved here; the IDE keeps the one from full + // discovery / resolves it at Load. + const allCandidates = lightweight + ? [] + : artifacts.filter(a => { + const p = (a.fileMetadata?.path || '').toLowerCase() + return !ZIP_RE.test(p) // keep non-transformed-bundle artifacts (incl. the beam-map zip) + }) // Bound the scan: each candidate is a serial download+parse, so an artifact-heavy // job (logs, metadata) could otherwise stall the IDE's discovery UI. Cap the number // scanned and enforce an overall deadline; the beam-map is written early and is @@ -755,6 +771,7 @@ export class ATXTransformHandler { ) } const lbvOpen = planRoot ? this.isRepoLbvOpen(planRoot, nr.RepositoryName) : true + const lbvPending = planRoot ? this.isRepoLbvHitlPending(planRoot, nr.RepositoryName) : true beamed.push({ RepositoryName: nr.RepositoryName, BeamArtifactId: artifactId, @@ -762,9 +779,10 @@ export class ATXTransformHandler { BeamTargetFramework: nr.BeamTargetFramework || '', BeamScenario: nr.BeamScenario || 'transformed', IsLbvOpen: lbvOpen, + IsLbvPending: lbvPending, }) this.logging.log( - `[BEAM-PKG] beamed repo (from beam-map) | repo=${nr.RepositoryName} artifact=${artifactId} stepId=${nr.BeamStepId || ''} scenario=${nr.BeamScenario || 'transformed'} lbvOpen=${lbvOpen}` + `[BEAM-PKG] beamed repo (from beam-map) | repo=${nr.RepositoryName} artifact=${artifactId} stepId=${nr.BeamStepId || ''} scenario=${nr.BeamScenario || 'transformed'} lbvOpen=${lbvOpen} lbvPending=${lbvPending}` ) } } else { @@ -809,6 +827,7 @@ export class ATXTransformHandler { continue } const lbvOpen = planRoot ? this.isRepoLbvOpen(planRoot, repoName) : true + const lbvPending = planRoot ? this.isRepoLbvHitlPending(planRoot, repoName) : true beamed.push({ RepositoryName: repoName, BeamArtifactId: zip.artifactId, @@ -816,9 +835,10 @@ export class ATXTransformHandler { BeamTargetFramework: '', BeamScenario: 'transformed', IsLbvOpen: lbvOpen, + IsLbvPending: lbvPending, }) this.logging.log( - `[BEAM-PKG] beamed repo (from beam-status) | repo=${repoName} artifact=${zip.artifactId} path=${zip.path} lbvOpen=${lbvOpen}` + `[BEAM-PKG] beamed repo (from beam-status) | repo=${repoName} artifact=${zip.artifactId} path=${zip.path} lbvOpen=${lbvOpen} lbvPending=${lbvPending}` ) } this.logging.log( @@ -841,11 +861,6 @@ export class ATXTransformHandler { } } - /** - * Normalize a beam-map repo item into the PascalCase shape the C# IDE consumes, - * tolerant of key-name variants from the web writer (repoName/repo/name, - * artifactId/beamArtifactId, stepId/planStepId, targetFramework/tfm). - */ /** * The wire contract with the web orchestrator / FES is not yet pinned to a single * spelling: a plan-step / HITL id arrives as stepId, planStepId, or parentStepId @@ -855,7 +870,9 @@ export class ATXTransformHandler { */ private getStepId(obj: any): string | undefined { if (obj == null || typeof obj !== 'object') return undefined - return obj.stepId ?? obj.planStepId ?? obj.parentStepId ?? undefined + // Use || (not ??) so an empty-string id falls through to the next spelling; an empty + // stepId must not short-circuit coalescing and then fail every scope match. + return obj.stepId || obj.planStepId || obj.parentStepId || undefined } /** @@ -879,7 +896,12 @@ export class ATXTransformHandler { return { scopedLbv, allOutOfScopeLbv } } - private normalizeBeamRepo(r: BeamMapRepo | null | undefined): Omit { + /** + * Normalize a beam-map repo item into the PascalCase shape the C# IDE consumes, + * tolerant of key-name variants from the web writer (repoName/repo/name, + * artifactId/beamArtifactId, stepId/planStepId, targetFramework/tfm). + */ + private normalizeBeamRepo(r: BeamMapRepo | null | undefined): Omit { const o: BeamMapRepo = r || {} return { RepositoryName: o.repoName ?? o.repositoryName ?? o.repo ?? o.name ?? '', @@ -1748,6 +1770,12 @@ export class ATXTransformHandler { result.DiffApplyFailed = true result.DiffApplyFailedStepIds = diffContext.failedStepIds } + // Surface the backend-resolved interactive mode (from job.objective) on every + // response so the IDE can restore it. Single injection point covers all internal + // return paths. cachedInteractiveMode is populated in _getTransformInfoInternal. + if (result && this.cachedInteractiveMode) { + result.InteractiveMode = this.cachedInteractiveMode + } return result } finally { this._currentDiffContext = null @@ -2001,13 +2029,46 @@ export class ATXTransformHandler { // Plan not available yet } - // Check for pending HITL tasks (e.g. missing packages) - job stays in PLANNING while HITL is pending + // Check for pending HITL tasks (e.g. missing packages) - job stays in PLANNING while HITL is pending. + // Beam multi-repo: several repos can have a pending LBV HITL at once; prefer the loaded repo's + // in-scope one (parity with AWAITING_HUMAN_INPUT). No-op when scope is empty, so non-beam is unchanged. const hitls = await this.listHitls(request.WorkspaceId, request.TransformationJobId) if (hitls && hitls.length > 0) { + const planScopeStepIds = (request.beamScopeStepIds || '') + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0) + const { scopedLbv: scopedPlanLbv, allOutOfScopeLbv: planAllOutOfScopeLbv } = + this.selectScopedLbvHitl(hitls, planScopeStepIds) + // Beam multi-repo parity with EXECUTING / getHitlAgentArtifact: if scope is set and every + // pending HITL is a sibling repo's LBV (none in the loaded repo's subtree), do NOT surface a + // sibling's LBV — return plan-only. Prevents handing the IDE another repo's build HITL. + if (planScopeStepIds.length > 0 && planAllOutOfScopeLbv) { + this.logging.log( + 'ATX: PLANNING job — all pending HITLs are sibling-repo LBV (none in loaded scope); not surfacing' + ) + return { + TransformationJob: { + WorkspaceId: request.WorkspaceId, + JobId: request.TransformationJobId, + Status: jobStatus, + } as AtxTransformationJob, + TransformationPlan: plan, + } as AtxGetTransformInfoResponse + } + // When scope is set but no in-scope LBV matched, drop out-of-scope LBVs from the fallback + // pool so the plain find() below can't select a sibling's LBV (mixed pending-set case). + const planFallbackPool = + planScopeStepIds.length > 0 && !scopedPlanLbv + ? hitls.filter(h => h.tag !== 'local-build-verification') + : hitls const hitl = - hitls.find(h => h.tag === 'local-build-verification') || - hitls.find(h => h.tag === 'missing-packages' || h.tag === 'handle_missing_packages_hitl') || - hitls[0] + scopedPlanLbv || + planFallbackPool.find(h => h.tag === 'local-build-verification') || + planFallbackPool.find( + h => h.tag === 'missing-packages' || h.tag === 'handle_missing_packages_hitl' + ) || + planFallbackPool[0] this.logging.log(`ATX: Found HITL task - tag: ${hitl.tag}, hasArtifact: ${!!hitl.agentArtifact}`) // For missing packages HITL, try to download artifact if available if (hitl.tag === 'handle_missing_packages_hitl' || hitl.tag === 'missing-packages') { @@ -2036,8 +2097,11 @@ export class ATXTransformHandler { } if (hitl.tag === 'local-build-verification') { this.jobsPastLocalBuild.add(request.TransformationJobId) + // Forward the HITL's plan-step id so a multi-repo beam IDE can confirm this LBV + // belongs to the loaded repo (parity with AWAITING_HUMAN_INPUT). Undefined when absent. + const planLbvStepId = this.getStepId(hitl) this.logging.log( - `ATX: ${jobStatus} job has pending LBV HITL — taskId=${hitl.taskId}; surfacing AWAITING_HUMAN_INPUT to IDE` + `ATX: ${jobStatus} job has pending LBV HITL — taskId=${hitl.taskId} stepId=${planLbvStepId ?? ''}; surfacing AWAITING_HUMAN_INPUT to IDE` ) return { TransformationJob: { @@ -2047,6 +2111,7 @@ export class ATXTransformHandler { } as AtxTransformationJob, HitlTag: hitl.tag, HitlTaskId: hitl.taskId, + StepInformation: planLbvStepId ? { StepId: planLbvStepId } : undefined, TransformationPlan: plan, } as AtxGetTransformInfoResponse } @@ -2341,6 +2406,18 @@ export class ATXTransformHandler { return !terminal } + /** + * Is this repo's LBV HITL NOT created yet? TRUE when the beamed node isn't in the plan tree, or + * has no LBV child. FALSE once an LBV node exists (open or terminal — IsLbvOpen distinguishes + * those). Gates the Load button (isLbvOpen && !isLbvPending) so the IDE never Loads before the + * HITL exists. Same node resolution as isRepoLbvOpen. + */ + private isRepoLbvHitlPending(planRoot: AtxPlanStep, repoName: string): boolean { + const repoNode = this.findBeamedRepoNode(planRoot, repoName) + if (!repoNode) return true + return !this.findLbvNode(repoNode) + } + /** * Match the beamed repo's plan node. CRITICAL: the plan tree has MULTIPLE nodes named for a * repo — the ORIGINAL TRANSFORM node named plain "" (top-level, SUCCEEDED, NO LBV child) diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts index 7f9b4fddbd..b8a0894b5c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts @@ -497,6 +497,30 @@ describe('ATXTransformHandler - getTransformInfo', () => { expect((handler as any).jobsPastLocalBuild.has('job-123')).to.be.true }) + it('should NOT surface a sibling repo LBV when status is PLANNING and all pending LBVs are out of the loaded scope', async () => { + // Beam multi-repo: the IDE loaded one repo (beamScopeStepIds = its subtree). Every pending + // HITL is a local-build-verification for a DIFFERENT (sibling) repo's plan step. Surfacing + // one would make the IDE build the loaded solution against a sibling's HITL → false-green. + // Mirrors the EXECUTING/getHitlAgentArtifact guard: return the plan-only view instead. + getJobStub.resolves({ statusDetails: { status: 'PLANNING' } }) + getTransformationPlanStub.resolves({ Root: { Children: [] } }) + listHitlsStub.resolves([ + { tag: 'local-build-verification', taskId: 'task-sib1', stepId: 'sibling-step-1' }, + { tag: 'local-build-verification', taskId: 'task-sib2', stepId: 'sibling-step-2' }, + ]) + + const result = await handler.getTransformInfo({ + ...baseRequest, + beamScopeStepIds: 'loaded-step-a,loaded-step-b', + }) + + // Plan-only view: original job status preserved, no HITL surfaced to the IDE. + expect(result?.TransformationJob.Status).to.equal('PLANNING') + expect(result?.HitlTag).to.be.undefined + expect(result?.HitlTaskId).to.be.undefined + expect(result?.TransformationPlan).to.deep.equal({ Root: { Children: [] } }) + }) + it('should filter pre-job mode-selection -checkpoint HITL before LBV has run', async () => { getJobStub.resolves({ statusDetails: { status: 'PLANNING' } }) getTransformationPlanStub.resolves({ Root: { Children: [] } }) @@ -4167,6 +4191,15 @@ describe('ATXTransformHandler - Beam to IDE', () => { expect(g('str')).to.be.undefined expect(g({ other: 'x' })).to.be.undefined }) + + it('skips an empty-string stepId and falls through to planStepId / parentStepId (|| not ??)', () => { + const g = (o: any) => (handler as any).getStepId(o) + // Empty stepId must NOT short-circuit coalescing (the ??→|| hardening) — an empty + // string is falsy under ||, so the non-empty planStepId is returned. + expect(g({ stepId: '', planStepId: 'step-1' })).to.equal('step-1') + // Empty stepId + empty planStepId → fall all the way through to parentStepId. + expect(g({ stepId: '', planStepId: '', parentStepId: 'parent-1' })).to.equal('parent-1') + }) }) // --- normalizeBeamRepo: tolerant wire-shape mapping --- diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts index 8534f907f9..260fe677a8 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts @@ -253,6 +253,13 @@ export type AgencticLoop_InvokeLLMEvent = { enabled?: boolean languageServerVersion?: string latency?: string + /** + * Classifies *why* a call failed, for failures that are handled inside the agent loop and are + * therefore not user-visible. Used by downstream metrics to exclude transient, self-recovering + * iterations from per-call success rates. Absent when `result` is 'Succeeded', and absent for + * failures that have no specific classification. + */ + reason?: string } export type ToolUseSuggestedEvent = { diff --git a/server/aws-lsp-json/CHANGELOG.md b/server/aws-lsp-json/CHANGELOG.md index 15449049b7..977d7dda14 100644 --- a/server/aws-lsp-json/CHANGELOG.md +++ b/server/aws-lsp-json/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.27](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-json/v0.1.26...lsp-json/v0.1.27) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.21 to ^0.0.22 + ## [0.1.26](https://github.com/aws/language-servers/compare/lsp-json/v0.1.25...lsp-json/v0.1.26) (2026-02-17) diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index b1c22c3601..b9f6ea357c 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-json", - "version": "0.1.26", + "version": "0.1.27", "description": "JSON Language Server", "main": "out/index.js", "repository": { @@ -27,7 +27,7 @@ }, "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" }, diff --git a/server/aws-lsp-partiql/CHANGELOG.md b/server/aws-lsp-partiql/CHANGELOG.md index ddd53ffcdf..8577629e33 100644 --- a/server/aws-lsp-partiql/CHANGELOG.md +++ b/server/aws-lsp-partiql/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.24](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-partiql/v0.0.23...lsp-partiql/v0.0.24) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + ## [0.0.23](https://github.com/aws/language-servers/compare/lsp-partiql/v0.0.22...lsp-partiql/v0.0.23) (2026-02-17) diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index cf735c6184..12cf012af8 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -3,7 +3,7 @@ "author": "Amazon Web Services", "license": "Apache-2.0", "description": "PartiQL language server", - "version": "0.0.23", + "version": "0.0.24", "repository": { "type": "git", "url": "https://github.com/Amazon-Q-Developer/language-servers" diff --git a/server/aws-lsp-yaml/CHANGELOG.md b/server/aws-lsp-yaml/CHANGELOG.md index 4b20d35253..fe6850ee84 100644 --- a/server/aws-lsp-yaml/CHANGELOG.md +++ b/server/aws-lsp-yaml/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.27](https://github.com/Amazon-Q-Developer/language-servers/compare/lsp-yaml/v0.1.26...lsp-yaml/v0.1.27) (2026-08-18) + + +### Bug Fixes + +* repoint CODEOWNERS and package repository URLs to the new org ([#2838](https://github.com/Amazon-Q-Developer/language-servers/issues/2838)) ([43e44dc](https://github.com/Amazon-Q-Developer/language-servers/commit/43e44dc5af94703f51456cd8a913f0d61af7570e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.21 to ^0.0.22 + ## [0.1.26](https://github.com/aws/language-servers/compare/lsp-yaml/v0.1.25...lsp-yaml/v0.1.26) (2026-02-17) diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index 64119bdcf2..cd5348b36b 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-yaml", - "version": "0.1.26", + "version": "0.1.27", "description": "YAML Language Server", "main": "out/index.js", "repository": { @@ -27,7 +27,7 @@ }, "dependencies": { "@aws/language-server-runtimes": "^0.3.14", - "@aws/lsp-core": "^0.0.21", + "@aws/lsp-core": "^0.0.22", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", "yaml-language-server": "1.13.0"