From feadb9663a63a224b7b37623867c1db8a4ae4370 Mon Sep 17 00:00:00 2001 From: Jordan Miao Date: Mon, 2 Feb 2026 16:42:29 -0800 Subject: [PATCH 1/9] updated objective data --- .../src/language-server/netTransform/atxModels.ts | 1 + .../netTransform/atxNetTransformServer.ts | 4 +++- .../netTransform/atxTransformHandler.ts | 11 ++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) 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 a71f41139a..5738b2acd5 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -50,6 +50,7 @@ export interface AtxListOrCreateWorkspaceResponse { export interface AtxStartTransformRequest extends ExecuteCommandParams { WorkspaceId: string JobName?: string + InteractiveMode?: boolean StartTransformRequest: object // Original RTS-style request for ZIP creation } 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 76f6073412..eae748b05e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts @@ -36,7 +36,8 @@ export const AtxNetTransformServerToken = return result } case AtxStartTransformCommand: { - const { WorkspaceId, JobName, StartTransformRequest } = params as AtxStartTransformRequest + const { WorkspaceId, JobName, InteractiveMode, StartTransformRequest } = + params as AtxStartTransformRequest if (!WorkspaceId) { throw new Error('WorkspaceId is required for startTransform') @@ -45,6 +46,7 @@ export const AtxNetTransformServerToken = const result = await atxTransformHandler.startTransform({ workspaceId: WorkspaceId, jobName: JobName, + interactiveMode: InteractiveMode, startTransformRequest: StartTransformRequest, }) 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 7408b67197..30778d4ffd 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -339,6 +339,7 @@ export class ATXTransformHandler { workspaceId: string jobName?: string targetFramework?: string + interactiveMode?: boolean }): Promise<{ jobId: string; status: string } | null> { try { this.logging.log(`ATX: Starting CreateJob for workspace: ${request.workspaceId}`) @@ -348,9 +349,15 @@ export class ATXTransformHandler { throw new Error('ATX FES client not initialized') } + // Build objective object with target_framework and optionally interactive_mode + const objective: any = { + target_framework: request.targetFramework || 'net10.0', + interactive_mode: request.interactiveMode || false, + } + const command = new CreateJobCommand({ workspaceId: request.workspaceId, - objective: JSON.stringify({ target_framework: request.targetFramework || 'net10.0' }), + objective: JSON.stringify(objective), jobType: 'DOTNET_IDE' as any, jobName: request.jobName || `transform-job-${Date.now()}`, intent: 'LANGUAGE_UPGRADE', @@ -522,6 +529,7 @@ export class ATXTransformHandler { async startTransform(request: { workspaceId: string jobName?: string + interactiveMode?: boolean startTransformRequest: object }): Promise<{ TransformationJobId: string; ArtifactPath: string; UploadId: string } | null> { try { @@ -532,6 +540,7 @@ export class ATXTransformHandler { workspaceId: request.workspaceId, jobName: request.jobName || 'Transform Job', targetFramework: (request.startTransformRequest as any).TargetFramework, + interactiveMode: request.interactiveMode, }) if (!createJobResponse?.jobId) { From 52ffe00bd1caa10e01d5c45c86117e4f26aa86fe Mon Sep 17 00:00:00 2001 From: Jordan Miao Date: Wed, 4 Feb 2026 11:25:08 -0800 Subject: [PATCH 2/9] feat: added setting breakpoint functionality --- .../language-server/netTransform/atxModels.ts | 13 ++ .../netTransform/atxNetTransformServer.ts | 20 +++ .../netTransform/atxTransformHandler.ts | 151 ++++++++++++++++++ 3 files changed, 184 insertions(+) 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 5738b2acd5..bb91f7961f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -94,3 +94,16 @@ export interface AtxUploadPlanResponse { PlanPath?: string ReportPath?: string } + +// ATX Set Breakpoints request/response (interactive mode) +export interface AtxSetBreakpointsRequest extends ExecuteCommandParams { + TransformationJobId: string + WorkspaceId: string + SolutionRootPath: string + Breakpoints: Record +} + +export interface AtxSetBreakpointsResponse { + Success: boolean + Error?: string +} 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 eae748b05e..2b55efb773 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts @@ -12,6 +12,7 @@ import { AtxGetTransformInfoRequest, AtxStopJobRequest, AtxUploadPlanRequest, + AtxSetBreakpointsRequest, } from './atxModels' // ATX FES Commands - Consolidated APIs @@ -20,6 +21,7 @@ const AtxStartTransformCommand = 'aws/atxTransform/startTransform' const AtxGetTransformInfoCommand = 'aws/atxTransform/getTransformInfo' const AtxStopJobCommand = 'aws/atxTransform/stopJob' const AtxUploadPlanCommand = 'aws/atxTransform/uploadPlan' +const AtxSetBreakpointsCommand = 'aws/atxTransform/setBreakpoints' export const AtxNetTransformServerToken = (): Server => @@ -81,6 +83,23 @@ export const AtxNetTransformServerToken = const result = await atxTransformHandler.stopJob(WorkspaceId, JobId) return { Status: result } } + case AtxSetBreakpointsCommand: { + const { WorkspaceId, TransformationJobId, SolutionRootPath, Breakpoints } = + params as AtxSetBreakpointsRequest + + if (!WorkspaceId || !TransformationJobId || !SolutionRootPath) { + throw new Error( + 'WorkspaceId, TransformationJobId, and SolutionRootPath are required for setBreakpoints' + ) + } + + return await atxTransformHandler.setBreakpoints( + WorkspaceId, + TransformationJobId, + SolutionRootPath, + Breakpoints || {} + ) + } default: { throw new Error(`Unknown ATX FES command: ${params.command}`) } @@ -108,6 +127,7 @@ export const AtxNetTransformServerToken = AtxGetTransformInfoCommand, AtxUploadPlanCommand, AtxStopJobCommand, + AtxSetBreakpointsCommand, ], }, }, 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 30778d4ffd..a1a089f500 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -15,6 +15,7 @@ import { GetHitlTaskCommand, ListHitlTasksCommand, SubmitCriticalHitlTaskCommand, + UpdateHitlTaskCommand, GetJobCommand, ListJobPlanStepsCommand, ListWorklogsCommand, @@ -36,6 +37,7 @@ import { AtxTransformationJob, AtxUploadPlanRequest, AtxUploadPlanResponse, + AtxSetBreakpointsResponse, } from './atxModels' import { v4 as uuidv4 } from 'uuid' import { request } from 'http' @@ -744,6 +746,35 @@ export class ATXTransformHandler { } } + async updateHitl(workspaceId: string, jobId: string, taskId: string, humanArtifactId: string): Promise { + try { + this.logging.log(`ATX: Starting UpdateHitl for task: ${taskId}`) + + if (!this.atxClient && !(await this.initializeAtxClient())) { + this.logging.error('ATX: Failed to initialize client for UpdateHitl') + return null + } + + const command = new UpdateHitlTaskCommand({ + workspaceId: workspaceId, + jobId: jobId, + taskId: taskId, + humanArtifact: { + artifactId: humanArtifactId, + }, + }) + + await this.addAuthToCommand(command) + const result = await this.atxClient!.send(command) + + this.logging.log(`ATX: UpdateHitl completed successfully`) + return result + } catch (error) { + this.logging.error(`ATX: UpdateHitl error: ${String(error)}`) + return null + } + } + async getHitl(workspaceId: string, jobId: string, taskId: string): Promise { try { this.logging.log(`ATX: Getting Hitl task: ${taskId}`) @@ -1460,4 +1491,124 @@ export class ATXTransformHandler { return null } } + + /** + * Set breakpoints for interactive mode transformation. + * Lists HITLs with "breakpoint-settings" tag, uploads breakpoints as JSON artifact, + * and updates the HITL task with the new artifact ID. + */ + async setBreakpoints( + workspaceId: string, + jobId: string, + solutionRootPath: string, + breakpoints: Record + ): Promise { + try { + this.logging.log(`ATX: Starting setBreakpoints for job: ${jobId}`) + + if (!this.atxClient && !(await this.initializeAtxClient())) { + return { Success: false, Error: 'ATX FES client not initialized' } + } + + // Step 1: List HITLs with "breakpoint-settings" tag + const hitlTask = await this.findBreakpointSettingsHitl(workspaceId, jobId) + + if (!hitlTask) { + return { Success: false, Error: 'No HITL task found with breakpoint-settings tag' } + } + + this.logging.log(`ATX: Found breakpoint-settings HITL task: ${hitlTask.taskId}`) + + // Step 2: Create JSON file with breakpoints mapping in artifact workspace + const artifactDir = path.join(solutionRootPath, workspaceFolderName, jobId) + if (!fs.existsSync(artifactDir)) { + fs.mkdirSync(artifactDir, { recursive: true }) + } + + const jsonFilePath = path.join(artifactDir, 'breakpoint-settings.json') + fs.writeFileSync(jsonFilePath, JSON.stringify(breakpoints, null, 2)) + + // Step 3: Upload the JSON artifact + const uploadInfo = await this.createArtifactUploadUrl( + workspaceId, + jobId, + jsonFilePath, + CategoryType.HITL_FROM_USER, + FileType.JSON + ) + + if (!uploadInfo) { + return { Success: false, Error: 'Failed to create artifact upload URL' } + } + + const uploadSuccess = await Utils.uploadArtifact( + uploadInfo.uploadUrl, + jsonFilePath, + uploadInfo.requestHeaders, + this.logging + ) + + if (!uploadSuccess) { + return { Success: false, Error: 'Failed to upload breakpoints artifact to S3' } + } + + // Step 4: Complete artifact upload + const completeResponse = await this.completeArtifactUpload(workspaceId, jobId, uploadInfo.uploadId) + + if (!completeResponse?.success) { + return { Success: false, Error: 'Failed to complete artifact upload' } + } + + // Step 5: Update HITL task with the new artifact ID + const updateResult = await this.updateHitl(workspaceId, jobId, hitlTask.taskId, uploadInfo.uploadId) + + if (!updateResult) { + return { Success: false, Error: 'Failed to update HITL task with breakpoints artifact' } + } + + this.logging.log(`ATX: setBreakpoints completed successfully`) + return { Success: true } + } catch (error) { + this.logging.error(`ATX: setBreakpoints error: ${String(error)}`) + return { Success: false, Error: String(error) } + } + } + + /** + * Find HITL task with "breakpoint-settings" tag + */ + private async findBreakpointSettingsHitl(workspaceId: string, jobId: string): Promise { + try { + this.logging.log(`ATX: Looking for HITL task with breakpoint-settings tag`) + + if (!this.atxClient && !(await this.initializeAtxClient())) { + this.logging.error('ATX: Failed to initialize client for findBreakpointSettingsHitl') + return null + } + + const command = new ListHitlTasksCommand({ + workspaceId: workspaceId, + jobId: jobId, + taskType: 'NORMAL', + taskFilter: { + taskStatuses: ['AWAITING_HUMAN_INPUT'], + tag: 'breakpoint-settings', + }, + }) + + await this.addAuthToCommand(command) + const result = await this.atxClient!.send(command) + + if (result.hitlTasks && result.hitlTasks.length > 0) { + this.logging.log(`ATX: Found ${result.hitlTasks.length} HITL task(s) with breakpoint-settings tag`) + return result.hitlTasks[0] + } + + this.logging.log('ATX: No HITL task found with breakpoint-settings tag') + return null + } catch (error) { + this.logging.error(`ATX: findBreakpointSettingsHitl error: ${String(error)}`) + return null + } + } } From 9fa97c4752fa4bf8e35960df681a26207556050c Mon Sep 17 00:00:00 2001 From: Jordan Miao Date: Wed, 4 Feb 2026 11:25:08 -0800 Subject: [PATCH 3/9] feat: added setting breakpoint functionality --- .../src/language-server/netTransform/atxTransformHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a1a089f500..f759574fcf 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -1592,7 +1592,7 @@ export class ATXTransformHandler { taskType: 'NORMAL', taskFilter: { taskStatuses: ['AWAITING_HUMAN_INPUT'], - tag: 'breakpoint-settings', + tag: `${jobId}-checkpoint`, }, }) From 053c44470dd52cb26a82b996bdff2a3111e10395 Mon Sep 17 00:00:00 2001 From: Jordan Miao Date: Wed, 4 Feb 2026 15:37:40 -0800 Subject: [PATCH 4/9] fix: renamed breakpont to checkpoint --- .../language-server/netTransform/atxModels.ts | 8 +-- .../netTransform/atxNetTransformServer.ts | 18 +++---- .../netTransform/atxTransformHandler.ts | 50 +++++++++---------- 3 files changed, 38 insertions(+), 38 deletions(-) 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 bb91f7961f..c4caeae716 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -95,15 +95,15 @@ export interface AtxUploadPlanResponse { ReportPath?: string } -// ATX Set Breakpoints request/response (interactive mode) -export interface AtxSetBreakpointsRequest extends ExecuteCommandParams { +// ATX Set Checkpoints request/response (interactive mode) +export interface AtxSetCheckpointsRequest extends ExecuteCommandParams { TransformationJobId: string WorkspaceId: string SolutionRootPath: string - Breakpoints: Record + Checkpoints: Record } -export interface AtxSetBreakpointsResponse { +export interface AtxSetCheckpointsResponse { Success: boolean Error?: string } 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 2b55efb773..483d8823f0 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts @@ -12,7 +12,7 @@ import { AtxGetTransformInfoRequest, AtxStopJobRequest, AtxUploadPlanRequest, - AtxSetBreakpointsRequest, + AtxSetCheckpointsRequest, } from './atxModels' // ATX FES Commands - Consolidated APIs @@ -21,7 +21,7 @@ const AtxStartTransformCommand = 'aws/atxTransform/startTransform' const AtxGetTransformInfoCommand = 'aws/atxTransform/getTransformInfo' const AtxStopJobCommand = 'aws/atxTransform/stopJob' const AtxUploadPlanCommand = 'aws/atxTransform/uploadPlan' -const AtxSetBreakpointsCommand = 'aws/atxTransform/setBreakpoints' +const AtxSetCheckpointsCommand = 'aws/atxTransform/setCheckpoints' export const AtxNetTransformServerToken = (): Server => @@ -83,21 +83,21 @@ export const AtxNetTransformServerToken = const result = await atxTransformHandler.stopJob(WorkspaceId, JobId) return { Status: result } } - case AtxSetBreakpointsCommand: { - const { WorkspaceId, TransformationJobId, SolutionRootPath, Breakpoints } = - params as AtxSetBreakpointsRequest + case AtxSetCheckpointsCommand: { + const { WorkspaceId, TransformationJobId, SolutionRootPath, Checkpoints } = + params as AtxSetCheckpointsRequest if (!WorkspaceId || !TransformationJobId || !SolutionRootPath) { throw new Error( - 'WorkspaceId, TransformationJobId, and SolutionRootPath are required for setBreakpoints' + 'WorkspaceId, TransformationJobId, and SolutionRootPath are required for setCheckpoints' ) } - return await atxTransformHandler.setBreakpoints( + return await atxTransformHandler.setCheckpoints( WorkspaceId, TransformationJobId, SolutionRootPath, - Breakpoints || {} + Checkpoints || {} ) } default: { @@ -127,7 +127,7 @@ export const AtxNetTransformServerToken = AtxGetTransformInfoCommand, AtxUploadPlanCommand, AtxStopJobCommand, - AtxSetBreakpointsCommand, + AtxSetCheckpointsCommand, ], }, }, 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 f759574fcf..e4fc2eb11d 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -37,7 +37,7 @@ import { AtxTransformationJob, AtxUploadPlanRequest, AtxUploadPlanResponse, - AtxSetBreakpointsResponse, + AtxSetCheckpointsResponse, } from './atxModels' import { v4 as uuidv4 } from 'uuid' import { request } from 'http' @@ -1493,40 +1493,40 @@ export class ATXTransformHandler { } /** - * Set breakpoints for interactive mode transformation. - * Lists HITLs with "breakpoint-settings" tag, uploads breakpoints as JSON artifact, + * Set checkpoints for interactive mode transformation. + * Lists HITLs with "checkpoint-settings" tag, uploads checkpoints as JSON artifact, * and updates the HITL task with the new artifact ID. */ - async setBreakpoints( + async setCheckpoints( workspaceId: string, jobId: string, solutionRootPath: string, - breakpoints: Record - ): Promise { + checkpoints: Record + ): Promise { try { - this.logging.log(`ATX: Starting setBreakpoints for job: ${jobId}`) + this.logging.log(`ATX: Starting setCheckpoints for job: ${jobId}`) if (!this.atxClient && !(await this.initializeAtxClient())) { return { Success: false, Error: 'ATX FES client not initialized' } } - // Step 1: List HITLs with "breakpoint-settings" tag - const hitlTask = await this.findBreakpointSettingsHitl(workspaceId, jobId) + // Step 1: List HITLs with "checkpoint-settings" tag + const hitlTask = await this.findCheckpointSettingsHitl(workspaceId, jobId) if (!hitlTask) { - return { Success: false, Error: 'No HITL task found with breakpoint-settings tag' } + return { Success: false, Error: 'No HITL task found with checkpoint-settings tag' } } - this.logging.log(`ATX: Found breakpoint-settings HITL task: ${hitlTask.taskId}`) + this.logging.log(`ATX: Found checkpoint-settings HITL task: ${hitlTask.taskId}`) - // Step 2: Create JSON file with breakpoints mapping in artifact workspace + // Step 2: Create JSON file with checkpoints mapping in artifact workspace const artifactDir = path.join(solutionRootPath, workspaceFolderName, jobId) if (!fs.existsSync(artifactDir)) { fs.mkdirSync(artifactDir, { recursive: true }) } - const jsonFilePath = path.join(artifactDir, 'breakpoint-settings.json') - fs.writeFileSync(jsonFilePath, JSON.stringify(breakpoints, null, 2)) + const jsonFilePath = path.join(artifactDir, 'checkpoint-settings.json') + fs.writeFileSync(jsonFilePath, JSON.stringify(checkpoints, null, 2)) // Step 3: Upload the JSON artifact const uploadInfo = await this.createArtifactUploadUrl( @@ -1549,7 +1549,7 @@ export class ATXTransformHandler { ) if (!uploadSuccess) { - return { Success: false, Error: 'Failed to upload breakpoints artifact to S3' } + return { Success: false, Error: 'Failed to upload checkpoints artifact to S3' } } // Step 4: Complete artifact upload @@ -1563,26 +1563,26 @@ export class ATXTransformHandler { const updateResult = await this.updateHitl(workspaceId, jobId, hitlTask.taskId, uploadInfo.uploadId) if (!updateResult) { - return { Success: false, Error: 'Failed to update HITL task with breakpoints artifact' } + return { Success: false, Error: 'Failed to update HITL task with checkpoints artifact' } } - this.logging.log(`ATX: setBreakpoints completed successfully`) + this.logging.log(`ATX: setCheckpoints completed successfully`) return { Success: true } } catch (error) { - this.logging.error(`ATX: setBreakpoints error: ${String(error)}`) + this.logging.error(`ATX: setCheckpoints error: ${String(error)}`) return { Success: false, Error: String(error) } } } /** - * Find HITL task with "breakpoint-settings" tag + * Find HITL task with "checkpoint-settings" tag */ - private async findBreakpointSettingsHitl(workspaceId: string, jobId: string): Promise { + private async findCheckpointSettingsHitl(workspaceId: string, jobId: string): Promise { try { - this.logging.log(`ATX: Looking for HITL task with breakpoint-settings tag`) + this.logging.log(`ATX: Looking for HITL task with checkpoint-settings tag`) if (!this.atxClient && !(await this.initializeAtxClient())) { - this.logging.error('ATX: Failed to initialize client for findBreakpointSettingsHitl') + this.logging.error('ATX: Failed to initialize client for findCheckpointSettingsHitl') return null } @@ -1600,14 +1600,14 @@ export class ATXTransformHandler { const result = await this.atxClient!.send(command) if (result.hitlTasks && result.hitlTasks.length > 0) { - this.logging.log(`ATX: Found ${result.hitlTasks.length} HITL task(s) with breakpoint-settings tag`) + this.logging.log(`ATX: Found ${result.hitlTasks.length} HITL task(s) with checkpoint-settings tag`) return result.hitlTasks[0] } - this.logging.log('ATX: No HITL task found with breakpoint-settings tag') + this.logging.log('ATX: No HITL task found with checkpoint-settings tag') return null } catch (error) { - this.logging.error(`ATX: findBreakpointSettingsHitl error: ${String(error)}`) + this.logging.error(`ATX: findCheckpointSettingsHitl error: ${String(error)}`) return null } } From 75ef69934669d8176208c06c4feb66aa3689e0a9 Mon Sep 17 00:00:00 2001 From: Jordan Miao Date: Mon, 9 Feb 2026 16:36:33 -0800 Subject: [PATCH 5/9] refactor: atx get plan now returns in its own class definition --- .../language-server/netTransform/atxModels.ts | 59 ++- .../netTransform/atxTransformHandler.ts | 366 +++++++----------- 2 files changed, 175 insertions(+), 250 deletions(-) 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 c4caeae716..9a037b64a2 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -1,19 +1,42 @@ import { ExecuteCommandParams } from 'vscode-languageserver' -import { TransformationPlan } from '@amzn/codewhisperer-runtime' - -// ATX Job Status enum (matches client-side C# definition) -export enum AtxJobStatus { - CREATED = 'CREATED', - STARTING = 'STARTING', - ASSESSING = 'ASSESSING', - PLANNING = 'PLANNING', - PLANNED = 'PLANNED', - EXECUTING = 'EXECUTING', - AWAITING_HUMAN_INPUT = 'AWAITING_HUMAN_INPUT', - COMPLETED = 'COMPLETED', - FAILED = 'FAILED', - STOPPING = 'STOPPING', - STOPPED = 'STOPPED', +import { PlanStepStatus } from '@amazon/elastic-gumby-frontend-client' + +// Re-export for convenience +export { PlanStepStatus } + +/** + * Represents a step in an ATX transformation plan tree structure. + * Matches C# AtxPlanStep class. + */ +export interface AtxPlanStep { + StepId: string + ParentStepId: string | null + StepName: string + Description: string + Status: PlanStepStatus + Children: AtxPlanStep[] +} + +/** + * Tree structure for transformation plan. + * Matches C# AtxTransformationPlan class. + */ +export interface AtxTransformationPlan { + Root: AtxPlanStep +} + +/** + * Creates an empty root node for the transformation plan tree. + */ +export function createEmptyRootNode(): AtxPlanStep { + return { + StepId: 'root', + ParentStepId: null, + StepName: 'Root', + Description: '', + Status: 'NOT_STARTED', + Children: [], + } } // ATX Workspace Models @@ -28,11 +51,11 @@ export interface AtxCreatedWorkspaceInfo { WorkspaceName: string } -// ATX Transformation Job (matches client-side C# definition) +// ATX Transformation Job export interface AtxTransformationJob { WorkspaceId: string JobId: string - Status: AtxJobStatus + Status: string FailureReason?: string } @@ -72,7 +95,7 @@ export interface AtxGetTransformInfoResponse { PlanPath?: string | null ReportPath?: string | null WorklogPath?: string | null - TransformationPlan?: TransformationPlan | null + TransformationPlan?: AtxTransformationPlan | null ArtifactPath?: string | null ErrorString?: string | null } 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 e4fc2eb11d..2d198221bb 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -38,10 +38,13 @@ import { AtxUploadPlanRequest, AtxUploadPlanResponse, AtxSetCheckpointsResponse, + AtxTransformationPlan, + AtxPlanStep, + PlanStepStatus, + createEmptyRootNode, } from './atxModels' import { v4 as uuidv4 } from 'uuid' import { request } from 'http' -import { TransformationPlan } from '@amzn/codewhisperer-runtime' import { Utils, workspaceFolderName } from './utils' @@ -1150,266 +1153,165 @@ export class ATXTransformHandler { workspaceId: string, jobId: string, solutionRootPath: string - ): Promise { + ): Promise { try { - // Get real plan steps from ATX FES (only if job status >= PLANNED) - const planSteps = await this.getATXFESJobPlanSteps(workspaceId, jobId) + const plan = await this.fetchPlanTree(workspaceId, jobId) - if (planSteps) { - this.logging.log(`ATX FES: Found ${planSteps.length} transformation steps`) - - // Sort steps by score (primary) and startTime (tiebreaker) to match RTS ordering - planSteps.sort((a: any, b: any) => { - const scoreDiff = (a.score || 0) - (b.score || 0) - if (scoreDiff !== 0) return scoreDiff + // Fetch worklogs in parallel (fire and forget, don't block plan return) + this.fetchWorklogs(workspaceId, jobId, solutionRootPath).catch(e => { + this.logging.log(`ATX: Could not get worklogs for workspace: ${workspaceId}, job: ${jobId}`) + }) - // Tiebreaker for identical scores: sort by startTime - const timeA = a.startTime ? new Date(a.startTime).getTime() : 0 - const timeB = b.startTime ? new Date(b.startTime).getTime() : 0 - return timeA - timeB - }) + this.logging.log(`ATX: Successfully built plan tree with ${plan.Root.Children.length} root steps`) + return plan + } catch (error) { + this.logging.error(`ATX: getTransformationPlan error: ${String(error)}`) + return { Root: createEmptyRootNode() } + } + } - // Return in exact same format as RTS with all required fields - const transformationPlan = { - transformationSteps: planSteps.map((step: any, index: number) => { - try { - // Map substeps to ProgressUpdates for IDE display - const progressUpdates = (step.substeps || []).map((substep: any) => { - // Map ATX substep status to IDE TransformationProgressUpdateStatus enum values - let substepStatus = 'IN_PROGRESS' // Default - no NOT_STARTED in this enum - switch (substep.status) { - case 'SUCCEEDED': - case 'COMPLETED': - substepStatus = 'COMPLETED' - break - case 'IN_PROGRESS': - case 'RUNNING': - substepStatus = 'IN_PROGRESS' - break - case 'FAILED': - substepStatus = 'FAILED' - break - case 'SKIPPED': - substepStatus = 'SKIPPED' - break - case 'NOT_STARTED': - case 'CREATED': - default: - substepStatus = 'IN_PROGRESS' // No NOT_STARTED option in ProgressUpdate enum - break - } - - // Map nested progress updates (3rd level) - const nestedProgressUpdates = (substep.substeps || []).map((nestedUpdate: any) => { - let nestedStatus = 'IN_PROGRESS' - switch (nestedUpdate.status) { - case 'SUCCEEDED': - case 'COMPLETED': - nestedStatus = 'COMPLETED' - break - case 'IN_PROGRESS': - case 'RUNNING': - nestedStatus = 'IN_PROGRESS' - break - case 'FAILED': - nestedStatus = 'FAILED' - break - case 'SKIPPED': - nestedStatus = 'SKIPPED' - break - default: - nestedStatus = 'IN_PROGRESS' - break - } - return { - name: nestedUpdate.stepName || 'Unknown Nested Update', - description: nestedUpdate.description || '', - status: nestedStatus, - stepId: nestedUpdate.stepId ?? undefined, - } - }) - - return { - name: substep.stepName || 'Unknown Substep', - description: substep.description || '', - status: substepStatus, - startTime: substep.startTime ? new Date(substep.startTime) : undefined, - endTime: substep.endTime ? new Date(substep.endTime) : undefined, - stepId: substep.stepId ?? undefined, - progressUpdates: nestedProgressUpdates, - } - }) - - // Use ATX status directly - IDE supports most values, minimal mapping needed - let mappedStatus = step.status || 'NOT_STARTED' - // Only map the few values IDE doesn't have - if (mappedStatus === 'SUCCEEDED') { - mappedStatus = 'COMPLETED' - } else if (mappedStatus === 'RUNNING') { - mappedStatus = 'IN_PROGRESS' - } else if (mappedStatus === 'CREATED') { - mappedStatus = 'NOT_STARTED' - } - - // Use ATX step data directly without hardcoded ordering - const stepNumber = index + 1 - const stepName = `Step ${stepNumber} - ${step.stepName || 'Unknown Step'}` - - return { - id: step.stepId || `step-${stepNumber}`, - name: stepName, - description: step.description || '', - status: mappedStatus, - progressUpdates: progressUpdates, - startTime: step.startTime ? new Date(step.startTime) : undefined, - endTime: step.endTime ? new Date(step.endTime) : undefined, - } - } catch (error) { - this.logging.error(`ATX FES: Error mapping step ${index}: ${String(error)}`) - // Return a safe fallback step - const stepNumber = index + 1 - return { - id: step.stepId || `fallback-${stepNumber}`, - name: `Step ${stepNumber} - ${step.stepName || `Step ${stepNumber}`}`, - description: step.description || '', - status: 'NOT_STARTED', - progressUpdates: [], - startTime: undefined, - endTime: undefined, - } - } - }), - } as TransformationPlan - try { - await this.listWorklogs(workspaceId, jobId, solutionRootPath) - } catch (e) { - this.logging.log(`ATX FES: Could not get worklog for workspaces: ${workspaceId}, job id: ${jobId}`) - } + /** + * Fetches all plan steps in a single API call and builds the tree locally. + */ + private async fetchPlanTree(workspaceId: string, jobId: string): Promise { + const root = createEmptyRootNode() - this.logging.log( - `ATX FES: Successfully mapped ${transformationPlan.transformationSteps?.length || 0} steps` - ) + if (!this.atxClient && !(await this.initializeAtxClient())) { + this.logging.error('ATX: Failed to initialize client for fetchPlanTree') + return { Root: root } + } - return transformationPlan - } else { - this.logging.log('ATX FES: No plan steps available yet - returning empty plan') - return { - transformationSteps: [] as any, - } as TransformationPlan - } - } catch (error) { - this.logging.error(`ATX FES getTransformationPlan error: ${String(error)}`) - // Return empty plan on error - return { - transformationSteps: [] as any, - } as TransformationPlan + // Fetch ALL steps in a single call (no parentStepId = returns all steps) + const allSteps = await this.fetchAllSteps(workspaceId, jobId) + if (!allSteps || allSteps.length === 0) { + this.logging.log('ATX: No plan steps available yet') + return { Root: root } } + + // Build tree from flat list + root.Children = this.buildTreeFromFlatList(allSteps) + + this.logging.log( + `ATX: fetchPlanTree completed - Built tree with ${root.Children.length} root steps from ${allSteps.length} total steps` + ) + return { Root: root } } - private async getATXFESJobPlanSteps(workspaceId: string, jobId: string): Promise { + /** + * Fetches all steps in a single API call (no parentStepId filter). + */ + private async fetchAllSteps(workspaceId: string, jobId: string): Promise { + const allSteps: any[] = [] + let nextToken: string | undefined + try { - this.logging.log(`ATX FES: getting plan steps with substeps...`) - const result = await this.listJobPlanSteps(workspaceId, jobId) - if (result) { - const steps = result || [] - this.logging.log(`ListJobPlanSteps: SUCCESS - Found ${steps.length} plan steps with substeps`) - return steps - } - return null + do { + const command = new ListJobPlanStepsCommand({ + workspaceId: workspaceId, + jobId: jobId, + maxResults: 100, + ...(nextToken && { nextToken }), + }) + + await this.addAuthToCommand(command) + const result = await this.atxClient!.send(command) + + if (result?.steps) { + allSteps.push(...result.steps) + } + nextToken = result?.nextToken + } while (nextToken) + + this.logging.log(`ATX: Fetched ${allSteps.length} total steps`) + return allSteps } catch (error) { - this.logging.error(`ListJobPlanSteps error: ${error instanceof Error ? error.message : 'Unknown error'}`) - return null + this.logging.error(`ATX: Error fetching all steps: ${String(error)}`) + return [] } } /** - * Lists job plan steps using FES client with recursive substep fetching + * Builds a tree structure from a flat list of steps using ParentStepId relationships. */ - private async listJobPlanSteps(workspaceId: string, jobId: string): Promise { - try { - this.logging.log(`ATX: Starting ListJobPlanSteps for job: ${jobId}`) + private buildTreeFromFlatList(flatSteps: any[]): AtxPlanStep[] { + // Create a map of StepId -> AtxPlanStep for quick lookup + const stepMap = new Map() - if (!this.atxClient && !(await this.initializeAtxClient())) { - this.logging.error('ATX: Failed to initialize client for ListJobPlanSteps') - return null + // First pass: convert all API steps to AtxPlanStep objects + for (const apiStep of flatSteps) { + const step = this.mapApiStepToNode(apiStep) + if (step.StepId) { + stepMap.set(step.StepId, step) } + } - // Get root steps first - const rootSteps = await this.getStepsRecursive(workspaceId, jobId, 'root') - - if (rootSteps && rootSteps.length > 0) { - // For each root step, get its substeps - for (const step of rootSteps) { - const substeps = await this.getStepsRecursive(workspaceId, jobId, step.stepId) - step.substeps = substeps || [] - - // Sort substeps by score (primary) and startTime (tiebreaker) to match RTS ordering - if (step.substeps.length > 0) { - step.substeps.sort((a: any, b: any) => { - const scoreDiff = (a.score || 0) - (b.score || 0) - if (scoreDiff !== 0) return scoreDiff - - // Tiebreaker for identical scores: sort by startTime - const timeA = a.startTime ? new Date(a.startTime).getTime() : 0 - const timeB = b.startTime ? new Date(b.startTime).getTime() : 0 - return timeA - timeB - }) - for (const substep of step.substeps) { - const superSubsteps = await this.getStepsRecursive(workspaceId, jobId, substep.stepId) - substep.substeps = superSubsteps || [] - - // Sort substeps by score (primary) and startTime (tiebreaker) to match RTS ordering - if (substep.substeps.length > 0) { - substep.substeps.sort((a: any, b: any) => { - const scoreDiff = (a.score || 0) - (b.score || 0) - if (scoreDiff !== 0) return scoreDiff - - // Tiebreaker for identical scores: sort by startTime - const timeA = a.startTime ? new Date(a.startTime).getTime() : 0 - const timeB = b.startTime ? new Date(b.startTime).getTime() : 0 - return timeA - timeB - }) - } - } - } - } + // Second pass: build parent-child relationships + const rootChildren: AtxPlanStep[] = [] - this.logging.log(`ATX: ListJobPlanSteps completed - Found ${rootSteps.length} steps with substeps`) - return rootSteps + for (const step of stepMap.values()) { + if (step.ParentStepId === 'root' || !step.ParentStepId) { + rootChildren.push(step) + } else { + const parent = stepMap.get(step.ParentStepId) + if (parent) { + parent.Children.push(step) + } else { + // Orphan step - treat as root level + rootChildren.push(step) + } } + } - this.logging.log('ATX: ListJobPlanSteps - No root steps found') - return null - } catch (error) { - this.logging.error(`ATX: ListJobPlanSteps error: ${String(error)}`) - return null + // Sort all children arrays by score + this.sortStepsByScore(rootChildren) + for (const step of stepMap.values()) { + if (step.Children.length > 0) { + this.sortStepsByScore(step.Children) + } } + + return rootChildren } /** - * Recursively gets steps for a given parent step ID + * Maps an API step response to AtxPlanStep. + * Converts from FES camelCase to C#-compatible PascalCase. */ - private async getStepsRecursive(workspaceId: string, jobId: string, parentStepId: string): Promise { - try { - const command = new ListJobPlanStepsCommand({ - workspaceId: workspaceId, - jobId: jobId, - parentStepId: parentStepId, - maxResults: 100, - }) + private mapApiStepToNode(apiStep: any): AtxPlanStep & { score?: number } { + return { + StepId: apiStep.stepId || '', + ParentStepId: apiStep.parentStepId === 'root' ? null : apiStep.parentStepId || null, + StepName: apiStep.stepName || '', + Description: apiStep.description || '', + Status: this.mapApiStatus(apiStep.status), + Children: [], + // Keep score for sorting (not sent to C#) + score: apiStep.score || 0, + } + } - await this.addAuthToCommand(command) - const result = await this.atxClient!.send(command) + /** + * Maps API status string to PlanStepStatus. + * Returns the status directly if valid, otherwise defaults to NOT_STARTED. + */ + private mapApiStatus(status: string | undefined): PlanStepStatus { + if (!status) return 'NOT_STARTED' + // The API returns valid PlanStepStatus values directly + return status as PlanStepStatus + } - if (result && result.steps && result.steps.length > 0) { - return result.steps - } + /** + * Sorts steps by score (primary). + */ + private sortStepsByScore(steps: (AtxPlanStep & { score?: number })[]): void { + steps.sort((a, b) => (a.score || 0) - (b.score || 0)) + } - return null - } catch (error) { - this.logging.error(`Error getting steps for parent ${parentStepId}: ${String(error)}`) - return null - } + /** + * Fetches worklogs for a job and saves them to disk. + */ + private async fetchWorklogs(workspaceId: string, jobId: string, solutionRootPath: string): Promise { + await this.listWorklogs(workspaceId, jobId, solutionRootPath) } /** @@ -1448,7 +1350,7 @@ export class ATXTransformHandler { } /** - * Lists artifacts using FES client with CUSTOMER_OUTPUT filtering + * Lists worklogs for a job and saves them to disk grouped by step ID. */ private async listWorklogs( workspaceId: string, From 360d0ba520ced1ace8aed449eeaef23d60561216 Mon Sep 17 00:00:00 2001 From: Jordan Miao Date: Wed, 11 Feb 2026 14:08:38 -0800 Subject: [PATCH 6/9] feat: added ability to handle step hitls for checkpointing --- .../language-server/netTransform/atxModels.ts | 9 + .../netTransform/atxTransformHandler.ts | 291 +++++++++++++++++- 2 files changed, 286 insertions(+), 14 deletions(-) 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 9a037b64a2..8b73bc6198 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -98,6 +98,15 @@ export interface AtxGetTransformInfoResponse { TransformationPlan?: AtxTransformationPlan | null ArtifactPath?: string | null ErrorString?: string | null + StepHitlInfo?: AtxStepHitlInfo | null +} + +/** + * Information about a step-level HITL during execution phase. + */ +export interface AtxStepHitlInfo { + StepId: string + DiffArtifactPath: string } // ATX Stop Job request 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 2d198221bb..2116773c48 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -42,6 +42,7 @@ import { AtxPlanStep, PlanStepStatus, createEmptyRootNode, + AtxStepHitlInfo, } from './atxModels' import { v4 as uuidv4 } from 'uuid' import { request } from 'http' @@ -770,7 +771,7 @@ export class ATXTransformHandler { await this.addAuthToCommand(command) const result = await this.atxClient!.send(command) - this.logging.log(`ATX: UpdateHitl completed successfully`) + this.logging.log(`ATX: UpdateHitl completed successfully with status ${result.status}`) return result } catch (error) { this.logging.error(`ATX: UpdateHitl error: ${String(error)}`) @@ -885,7 +886,7 @@ export class ATXTransformHandler { } /** - * Get transform info - dummy implementation + * Get transform info */ async getTransformInfo(request: AtxGetTransformInfoRequest): Promise { try { @@ -957,11 +958,9 @@ export class ATXTransformHandler { TransformationPlan: plan, } as AtxGetTransformInfoResponse } else if (jobStatus === 'AWAITING_HUMAN_INPUT') { - const response = await this.getHitlAgentArtifact( - request.WorkspaceId, - request.TransformationJobId, - request.SolutionRootPath - ) + return await this.handleAwaitingHumanInput(request) + } else { + await this.listWorklogs(request.WorkspaceId, request.TransformationJobId, request.SolutionRootPath) return { TransformationJob: { @@ -969,26 +968,290 @@ export class ATXTransformHandler { JobId: request.TransformationJobId, Status: jobStatus, } as AtxTransformationJob, - PlanPath: response?.PlanPath, - ReportPath: response?.ReportPath, } as AtxGetTransformInfoResponse - } else { - await this.listWorklogs(request.WorkspaceId, request.TransformationJobId, request.SolutionRootPath) + } + } catch (error) { + this.logging.error(`ATX: GetTransformInfo error: ${String(error)}`) + return null + } + } + + /** + * Handles AWAITING_HUMAN_INPUT status. + * Two scenarios: + * 1. Planning phase: No plan exists yet, need to get HITL artifacts (plan.md, report.md) + * 2. Execution phase: Plan exists, HITL raised for a specific step + */ + private async handleAwaitingHumanInput(request: AtxGetTransformInfoRequest): Promise { + // Try to get the transformation plan first + const plan = await this.getTransformationPlan( + request.WorkspaceId, + request.TransformationJobId, + request.SolutionRootPath + ) + + const hasPlan = plan.Root.Children.length > 0 + + if (hasPlan) { + // Execution phase: Plan exists, HITL raised during transformation + return await this.handleExecutionPhaseHitl(request, plan) + } else { + // Planning phase: No plan yet, get HITL artifacts for plan review + return await this.handlePlanningPhaseHitl(request) + } + } + + /** + * Handles HITL during planning phase - downloads plan.md and report.md for user review. + */ + private async handlePlanningPhaseHitl(request: AtxGetTransformInfoRequest): Promise { + const response = await this.getHitlAgentArtifact( + request.WorkspaceId, + request.TransformationJobId, + request.SolutionRootPath + ) + return { + TransformationJob: { + WorkspaceId: request.WorkspaceId, + JobId: request.TransformationJobId, + Status: 'AWAITING_HUMAN_INPUT', + } as AtxTransformationJob, + PlanPath: response?.PlanPath, + ReportPath: response?.ReportPath, + } as AtxGetTransformInfoResponse + } + + /** + * Handles HITL during execution phase - plan exists, step-level HITL raised. + */ + private async handleExecutionPhaseHitl( + request: AtxGetTransformInfoRequest, + plan: AtxTransformationPlan + ): Promise { + this.logging.log(`ATX: Execution phase HITL - plan has ${plan.Root.Children.length} steps`) + + try { + // Find the step with PENDING_HUMAN_INPUT status in the plan + const pendingStep = this.findPendingHumanInputStep(plan.Root) + + if (!pendingStep) { + this.logging.log('ATX: No step with PENDING_HUMAN_INPUT status found in plan') return { TransformationJob: { WorkspaceId: request.WorkspaceId, JobId: request.TransformationJobId, - Status: jobStatus, + Status: 'AWAITING_HUMAN_INPUT', } as AtxTransformationJob, + TransformationPlan: plan, } as AtxGetTransformInfoResponse } + + this.logging.log(`ATX: Found pending step: ${pendingStep.StepId}`) + + // Find the step-level HITL using tag: {stepId}-review + const stepHitl = await this.findStepLevelHitl( + request.WorkspaceId, + request.TransformationJobId, + pendingStep.StepId + ) + + if (!stepHitl) { + this.logging.log('ATX: No step-level HITL found, returning plan only') + return { + TransformationJob: { + WorkspaceId: request.WorkspaceId, + JobId: request.TransformationJobId, + Status: 'AWAITING_HUMAN_INPUT', + } as AtxTransformationJob, + TransformationPlan: plan, + } as AtxGetTransformInfoResponse + } + + // Download and parse the agent artifact JSON + const stepHitlInfo = await this.downloadAndParseStepHitlArtifact( + request.WorkspaceId, + request.TransformationJobId, + stepHitl, + pendingStep.StepId, + request.SolutionRootPath + ) + + return { + TransformationJob: { + WorkspaceId: request.WorkspaceId, + JobId: request.TransformationJobId, + Status: 'AWAITING_HUMAN_INPUT', + } as AtxTransformationJob, + TransformationPlan: plan, + StepHitlInfo: stepHitlInfo, + } as AtxGetTransformInfoResponse } catch (error) { - this.logging.error(`ATX: GetTransformInfo error: ${String(error)}`) + this.logging.error(`ATX: handleExecutionPhaseHitl error: ${String(error)}`) + return { + TransformationJob: { + WorkspaceId: request.WorkspaceId, + JobId: request.TransformationJobId, + Status: 'AWAITING_HUMAN_INPUT', + } as AtxTransformationJob, + TransformationPlan: plan, + } as AtxGetTransformInfoResponse + } + } + + /** + * Recursively finds a step with PENDING_HUMAN_INPUT status in the plan tree. + */ + private findPendingHumanInputStep(step: AtxPlanStep): AtxPlanStep | null { + if (step.Status === 'PENDING_HUMAN_INPUT') { + return step + } + + for (const child of step.Children) { + const found = this.findPendingHumanInputStep(child) + if (found) { + return found + } + } + + return null + } + + /** + * Finds the step-level HITL task by filtering for the "{stepId}-review" tag. + */ + private async findStepLevelHitl(workspaceId: string, jobId: string, stepId: string): Promise { + try { + this.logging.log(`ATX: Finding step-level HITL for job: ${jobId}, step: ${stepId}`) + + if (!this.atxClient && !(await this.initializeAtxClient())) { + this.logging.error('ATX: Failed to initialize client for findStepLevelHitl') + return null + } + + // List HITLs with "{stepId}-review" tag + const command = new ListHitlTasksCommand({ + workspaceId: workspaceId, + jobId: jobId, + taskType: 'NORMAL', + taskFilter: { + taskStatuses: ['AWAITING_HUMAN_INPUT'], + tag: `${stepId}-review`, + }, + }) + + await this.addAuthToCommand(command) + const result = await this.atxClient!.send(command) + + if (!result.hitlTasks || result.hitlTasks.length === 0) { + this.logging.log(`ATX: No step-level HITL found with tag ${stepId}-review`) + return null + } + + const stepHitl = result.hitlTasks[0] + this.logging.log(`ATX: Found step-level HITL: ${stepHitl.taskId}`) + + return stepHitl + } catch (error) { + this.logging.error(`ATX: findStepLevelHitl error: ${String(error)}`) + return null + } + } + + /** + * Downloads and parses the step HITL agent artifact JSON, and extracts the diff artifact. + */ + private async downloadAndParseStepHitlArtifact( + workspaceId: string, + jobId: string, + hitlTask: any, + stepId: string, + solutionRootPath: string + ): Promise { + try { + const taskId = hitlTask.taskId + const agentArtifactId = hitlTask.agentArtifact?.artifactId + + if (!agentArtifactId) { + this.logging.error('ATX: Step HITL has no agent artifact') + return null + } + + this.logging.log(`ATX: Downloading step HITL artifact: ${agentArtifactId}`) + + // Download the agent artifact JSON + const downloadInfo = await this.createArtifactDownloadUrl(workspaceId, jobId, agentArtifactId) + if (!downloadInfo) { + throw new Error('Failed to get download URL for step HITL artifact') + } + + // Download the JSON content + const response = await got.get(downloadInfo.s3PresignedUrl, { + headers: downloadInfo.requestHeaders || {}, + responseType: 'text', + }) + + const artifactJson = JSON.parse(response.body) + this.logging.log(`ATX: Parsed step HITL artifact JSON`) + + // Extract diff artifact if present + let diffArtifactPath = '' + if (artifactJson.diffArtifactId) { + diffArtifactPath = await this.downloadDiffArtifact( + workspaceId, + jobId, + artifactJson.diffArtifactId, + solutionRootPath, + taskId + ) + } + + return { + StepId: stepId, + DiffArtifactPath: diffArtifactPath, + } + } catch (error) { + this.logging.error(`ATX: downloadAndParseStepHitlArtifact error: ${String(error)}`) return null } } + /** + * Downloads and extracts the diff artifact ZIP. + */ + private async downloadDiffArtifact( + workspaceId: string, + jobId: string, + diffArtifactId: string, + solutionRootPath: string, + taskId: string + ): Promise { + try { + this.logging.log(`ATX: Downloading diff artifact: ${diffArtifactId}`) + + const downloadInfo = await this.createArtifactDownloadUrl(workspaceId, jobId, diffArtifactId) + if (!downloadInfo) { + throw new Error('Failed to get download URL for diff artifact') + } + + const pathToDownload = path.join(solutionRootPath, workspaceFolderName, jobId, 'hitl', taskId) + + await Utils.downloadAndExtractArchive( + downloadInfo.s3PresignedUrl, + downloadInfo.requestHeaders, + pathToDownload, + 'diff-artifact.zip', + this.logging + ) + + this.logging.log(`ATX: Diff artifact extracted to: ${pathToDownload}`) + return pathToDownload + } catch (error) { + this.logging.error(`ATX: downloadDiffArtifact error: ${String(error)}`) + return '' + } + } + async uploadPlan(request: AtxUploadPlanRequest): Promise { this.logging.log('ATX: Starting upload plan') @@ -1493,7 +1756,7 @@ export class ATXTransformHandler { jobId: jobId, taskType: 'NORMAL', taskFilter: { - taskStatuses: ['AWAITING_HUMAN_INPUT'], + taskStatuses: ['AWAITING_HUMAN_INPUT', 'IN_PROGRESS'], tag: `${jobId}-checkpoint`, }, }) From 7010ea6f9ea9e25235d5d38c041cff6934c24ece Mon Sep 17 00:00:00 2001 From: Jordan Miao Date: Thu, 12 Feb 2026 20:53:21 -0800 Subject: [PATCH 7/9] fix: changed step hitl folder --- .../language-server/netTransform/atxTransformHandler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 2116773c48..69164d4fc9 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -1202,7 +1202,7 @@ export class ATXTransformHandler { jobId, artifactJson.diffArtifactId, solutionRootPath, - taskId + stepId ) } @@ -1224,7 +1224,7 @@ export class ATXTransformHandler { jobId: string, diffArtifactId: string, solutionRootPath: string, - taskId: string + stepId: string ): Promise { try { this.logging.log(`ATX: Downloading diff artifact: ${diffArtifactId}`) @@ -1234,13 +1234,13 @@ export class ATXTransformHandler { throw new Error('Failed to get download URL for diff artifact') } - const pathToDownload = path.join(solutionRootPath, workspaceFolderName, jobId, 'hitl', taskId) + const pathToDownload = path.join(solutionRootPath, workspaceFolderName, jobId, 'checkpoints', stepId) await Utils.downloadAndExtractArchive( downloadInfo.s3PresignedUrl, downloadInfo.requestHeaders, pathToDownload, - 'diff-artifact.zip', + `${stepId}.zip`, this.logging ) From fecd00f8a4bb7e81eebaf42e5e13252f40038e55 Mon Sep 17 00:00:00 2001 From: Jordan Miao Date: Thu, 12 Feb 2026 21:00:44 -0800 Subject: [PATCH 8/9] fix: changed plan detection and changed model name to match toolkit --- .../src/language-server/netTransform/atxModels.ts | 6 +++--- .../netTransform/atxTransformHandler.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) 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 8b73bc6198..033dee2c93 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -98,13 +98,13 @@ export interface AtxGetTransformInfoResponse { TransformationPlan?: AtxTransformationPlan | null ArtifactPath?: string | null ErrorString?: string | null - StepHitlInfo?: AtxStepHitlInfo | null + StepInformation?: AtxStepInformation | null } /** - * Information about a step-level HITL during execution phase. + * Information about a step during execution phase HITL. */ -export interface AtxStepHitlInfo { +export interface AtxStepInformation { StepId: string DiffArtifactPath: string } 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 69164d4fc9..58c396446e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -42,7 +42,7 @@ import { AtxPlanStep, PlanStepStatus, createEmptyRootNode, - AtxStepHitlInfo, + AtxStepInformation, } from './atxModels' import { v4 as uuidv4 } from 'uuid' import { request } from 'http' @@ -990,7 +990,7 @@ export class ATXTransformHandler { request.SolutionRootPath ) - const hasPlan = plan.Root.Children.length > 0 + const hasPlan = plan.Root.Children[0].Children.length > 0 if (hasPlan) { // Execution phase: Plan exists, HITL raised during transformation @@ -1069,7 +1069,7 @@ export class ATXTransformHandler { } // Download and parse the agent artifact JSON - const stepHitlInfo = await this.downloadAndParseStepHitlArtifact( + const stepInformation = await this.downloadAndParseStepHitlArtifact( request.WorkspaceId, request.TransformationJobId, stepHitl, @@ -1084,7 +1084,7 @@ export class ATXTransformHandler { Status: 'AWAITING_HUMAN_INPUT', } as AtxTransformationJob, TransformationPlan: plan, - StepHitlInfo: stepHitlInfo, + StepInformation: stepInformation, } as AtxGetTransformInfoResponse } catch (error) { this.logging.error(`ATX: handleExecutionPhaseHitl error: ${String(error)}`) @@ -1167,7 +1167,7 @@ export class ATXTransformHandler { hitlTask: any, stepId: string, solutionRootPath: string - ): Promise { + ): Promise { try { const taskId = hitlTask.taskId const agentArtifactId = hitlTask.agentArtifact?.artifactId From 1e10f49bf630ce81d992ad4626e2df17df17ca39 Mon Sep 17 00:00:00 2001 From: Jordan Miao Date: Fri, 13 Feb 2026 16:16:09 -0800 Subject: [PATCH 9/9] feat: added handling checkpoint for user action --- .../language-server/netTransform/atxModels.ts | 19 +++ .../netTransform/atxNetTransformServer.ts | 22 ++- .../netTransform/atxTransformHandler.ts | 150 +++++++++++++++++- 3 files changed, 183 insertions(+), 8 deletions(-) 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 033dee2c93..8b0c89563a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxModels.ts @@ -107,6 +107,10 @@ export interface AtxGetTransformInfoResponse { export interface AtxStepInformation { StepId: string DiffArtifactPath: string + RetryInstruction?: string + IsInvalid?: boolean + InvalidInstruction?: string + InvalidReason?: string } // ATX Stop Job request @@ -139,3 +143,18 @@ export interface AtxSetCheckpointsResponse { Success: boolean Error?: string } + +// ATX Checkpoint Action request/response +export interface AtxCheckpointActionRequest extends ExecuteCommandParams { + Action: string // "APPLY" or "RETRY" + NewInstruction?: string // Only used when Action is "RETRY" + StepId: string + TransformationJobId: string + WorkspaceId: string + SolutionRootPath: string +} + +export interface AtxCheckpointActionResponse { + Success: boolean + Error?: string | null +} 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 483d8823f0..2c058830be 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxNetTransformServer.ts @@ -13,6 +13,7 @@ import { AtxStopJobRequest, AtxUploadPlanRequest, AtxSetCheckpointsRequest, + AtxCheckpointActionRequest, } from './atxModels' // ATX FES Commands - Consolidated APIs @@ -22,6 +23,7 @@ const AtxGetTransformInfoCommand = 'aws/atxTransform/getTransformInfo' const AtxStopJobCommand = 'aws/atxTransform/stopJob' const AtxUploadPlanCommand = 'aws/atxTransform/uploadPlan' const AtxSetCheckpointsCommand = 'aws/atxTransform/setCheckpoints' +const AtxCheckpointActionCommand = 'aws/atxTransform/checkpointAction' export const AtxNetTransformServerToken = (): Server => @@ -87,12 +89,6 @@ export const AtxNetTransformServerToken = const { WorkspaceId, TransformationJobId, SolutionRootPath, Checkpoints } = params as AtxSetCheckpointsRequest - if (!WorkspaceId || !TransformationJobId || !SolutionRootPath) { - throw new Error( - 'WorkspaceId, TransformationJobId, and SolutionRootPath are required for setCheckpoints' - ) - } - return await atxTransformHandler.setCheckpoints( WorkspaceId, TransformationJobId, @@ -100,6 +96,19 @@ export const AtxNetTransformServerToken = Checkpoints || {} ) } + case AtxCheckpointActionCommand: { + const { WorkspaceId, TransformationJobId, StepId, Action, NewInstruction, SolutionRootPath } = + params as AtxCheckpointActionRequest + + return await atxTransformHandler.checkpointAction( + WorkspaceId, + TransformationJobId, + StepId, + Action, + SolutionRootPath, + NewInstruction + ) + } default: { throw new Error(`Unknown ATX FES command: ${params.command}`) } @@ -128,6 +137,7 @@ export const AtxNetTransformServerToken = AtxUploadPlanCommand, AtxStopJobCommand, AtxSetCheckpointsCommand, + AtxCheckpointActionCommand, ], }, }, 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 58c396446e..a86455205a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -15,6 +15,7 @@ import { GetHitlTaskCommand, ListHitlTasksCommand, SubmitCriticalHitlTaskCommand, + SubmitStandardHitlTaskCommand, UpdateHitlTaskCommand, GetJobCommand, ListJobPlanStepsCommand, @@ -43,6 +44,7 @@ import { PlanStepStatus, createEmptyRootNode, AtxStepInformation, + AtxCheckpointActionResponse, } from './atxModels' import { v4 as uuidv4 } from 'uuid' import { request } from 'http' @@ -60,6 +62,7 @@ export class ATXTransformHandler { private runtime: Runtime private atxClient: ElasticGumbyFrontendClient | null = null private cachedHitl: string | null = null + private cachedStepHitl: string | null = null constructor(serviceManager: AtxTokenServiceManager, workspace: Workspace, logging: Logging, runtime: Runtime) { this.serviceManager = serviceManager @@ -1151,6 +1154,9 @@ export class ATXTransformHandler { const stepHitl = result.hitlTasks[0] this.logging.log(`ATX: Found step-level HITL: ${stepHitl.taskId}`) + // Cache the step HITL task ID for later use in checkpointAction + this.cachedStepHitl = stepHitl.taskId || null + return stepHitl } catch (error) { this.logging.error(`ATX: findStepLevelHitl error: ${String(error)}`) @@ -1209,6 +1215,10 @@ export class ATXTransformHandler { return { StepId: stepId, DiffArtifactPath: diffArtifactPath, + ...(artifactJson.retryInstruction && { RetryInstruction: artifactJson.retryInstruction }), + ...(artifactJson.isInvalid !== undefined && { IsInvalid: artifactJson.isInvalid }), + ...(artifactJson.invalidInstruction && { InvalidInstruction: artifactJson.invalidInstruction }), + ...(artifactJson.invalidReason && { InvalidReason: artifactJson.invalidReason }), } } catch (error) { this.logging.error(`ATX: downloadAndParseStepHitlArtifact error: ${String(error)}`) @@ -1684,8 +1694,8 @@ export class ATXTransformHandler { this.logging.log(`ATX: Found checkpoint-settings HITL task: ${hitlTask.taskId}`) - // Step 2: Create JSON file with checkpoints mapping in artifact workspace - const artifactDir = path.join(solutionRootPath, workspaceFolderName, jobId) + // Step 2: Create JSON file with checkpoints mapping in checkpoints folder + const artifactDir = path.join(solutionRootPath, workspaceFolderName, jobId, 'checkpoints') if (!fs.existsSync(artifactDir)) { fs.mkdirSync(artifactDir, { recursive: true }) } @@ -1776,4 +1786,140 @@ export class ATXTransformHandler { return null } } + + /** + * Handle checkpoint action (APPLY or RETRY) for a step-level HITL. + */ + async checkpointAction( + workspaceId: string, + jobId: string, + stepId: string, + action: string, + solutionRootPath: string, + newInstruction?: string + ): Promise { + try { + this.logging.log(`ATX: Starting checkpointAction for job: ${jobId}, step: ${stepId}, action: ${action}`) + + if (!this.atxClient && !(await this.initializeAtxClient())) { + return { Success: false, Error: 'ATX FES client not initialized' } + } + + // Get the cached step HITL task ID, or query for it if not cached + let taskId: string | null = this.cachedStepHitl + if (!taskId) { + this.logging.log('ATX: No cached step HITL, querying for active step HITL') + const stepHitl = await this.findStepLevelHitl(workspaceId, jobId, stepId) + if (!stepHitl || !stepHitl.taskId) { + return { Success: false, Error: 'No active step HITL found' } + } + taskId = stepHitl.taskId + } + + // At this point taskId is guaranteed to be a string + const validTaskId = taskId as string + + // Create the human artifact JSON + const artifactContent: any = { + action: action, + } + if (action === 'RETRY' && newInstruction) { + artifactContent.newInstruction = newInstruction + } + + // Create the JSON file at {solutionRootPath}/{workspaceFolderName}/{jobId}/checkpoints/checkpoint-action.json + const artifactDir = path.join(solutionRootPath, workspaceFolderName, jobId, 'checkpoints') + if (!fs.existsSync(artifactDir)) { + fs.mkdirSync(artifactDir, { recursive: true }) + } + + const jsonFilePath = path.join(artifactDir, 'checkpoint-action.json') + fs.writeFileSync(jsonFilePath, JSON.stringify(artifactContent, null, 2)) + + // Upload the JSON artifact + const uploadInfo = await this.createArtifactUploadUrl( + workspaceId, + jobId, + jsonFilePath, + CategoryType.HITL_FROM_USER, + FileType.JSON + ) + + if (!uploadInfo) { + return { Success: false, Error: 'Failed to create artifact upload URL' } + } + + const uploadSuccess = await Utils.uploadArtifact( + uploadInfo.uploadUrl, + jsonFilePath, + uploadInfo.requestHeaders, + this.logging + ) + + if (!uploadSuccess) { + return { Success: false, Error: 'Failed to upload checkpoint action artifact to S3' } + } + + // Complete artifact upload + const completeResponse = await this.completeArtifactUpload(workspaceId, jobId, uploadInfo.uploadId) + + if (!completeResponse?.success) { + return { Success: false, Error: 'Failed to complete artifact upload' } + } + + // Submit the standard HITL task with the human artifact + const submitResult = await this.submitStandardHitl(workspaceId, jobId, validTaskId, uploadInfo.uploadId) + + if (!submitResult) { + return { Success: false, Error: 'Failed to submit checkpoint action' } + } + + // Clear the cached step HITL after successful submission + this.cachedStepHitl = null + + this.logging.log(`ATX: checkpointAction completed successfully`) + return { Success: true } + } catch (error) { + this.logging.error(`ATX: checkpointAction error: ${String(error)}`) + return { Success: false, Error: String(error) } + } + } + + /** + * Submit a standard HITL task with a human artifact. + */ + private async submitStandardHitl( + workspaceId: string, + jobId: string, + taskId: string, + humanArtifactId: string + ): Promise { + try { + this.logging.log(`ATX: Starting SubmitStandardHitl for task: ${taskId}`) + + if (!this.atxClient && !(await this.initializeAtxClient())) { + this.logging.error('ATX: Failed to initialize client for SubmitStandardHitl') + return null + } + + const command = new SubmitStandardHitlTaskCommand({ + workspaceId: workspaceId, + jobId: jobId, + taskId: taskId, + action: 'APPROVE', + humanArtifact: { + artifactId: humanArtifactId, + }, + }) + + await this.addAuthToCommand(command) + const result = await this.atxClient!.send(command) + + this.logging.log(`ATX: SubmitStandardHitl completed - task status: ${result.status || 'UNKNOWN'}`) + return result + } catch (error) { + this.logging.error(`ATX: SubmitStandardHitl error: ${String(error)}`) + return null + } + } }