Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,33 @@
]
}
},
{
"name": "ecl-extension-getWorkunitErrors",
"tags": [
"workunit",
"errors",
"warnings",
"exceptions",
"ecl-extension"
],
"toolReferenceName": "getWorkunitErrors",
"displayName": "Get Workunit Errors/Warnings",
"modelDescription": "Fetch errors, warnings, and exceptions from a specific workunit. Returns detailed information about compilation errors, runtime exceptions, and warning messages. Requires a WUID (Workunit ID).",
"canBeReferencedInPrompt": true,
"icon": "$(warning)",
"inputSchema": {
"type": "object",
"properties": {
"wuid": {
"type": "string",
"description": "The Workunit ID (WUID) to fetch errors and warnings for"
}
},
"required": [
"wuid"
]
}
},
{
"name": "ecl-extension-syntaxCheck",
"tags": [
Expand Down
3 changes: 3 additions & 0 deletions src/ecl/lm/tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as vscode from "vscode";
import { FindWorkunitsTool } from "./tools/findWorkunits";
import { GetWorkunitErrorsTool } from "./tools/getWorkunitErrors";
import { FindLogicalFilesTool } from "./tools/findLogicalFiles";
import { SyntaxCheckTool } from "./tools/syntaxCheck";

Expand All @@ -9,6 +10,8 @@ export class ECLLMTools {

protected constructor(ctx: vscode.ExtensionContext) {
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-findWorkunits", new FindWorkunitsTool()));
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-getWorkunitErrors", new GetWorkunitErrorsTool()));

ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-findLogicalFiles", new FindLogicalFilesTool()));
ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-syntaxCheck", new SyntaxCheckTool()));
}
Expand Down
171 changes: 171 additions & 0 deletions src/ecl/lm/tools/getWorkunitErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import * as vscode from "vscode";
import { Workunit, type WsWorkunits } from "@hpcc-js/comms";
import { isPlatformConnected } from "../../../hpccplatform/session";
import { reporter } from "../../../telemetry";
import localize from "../../../util/localize";
import { createServiceOptions, logToolEvent, requireConnectedSession, throwIfCancellationRequested } from "../utils";

const SEVERITY = {
ERROR: ["Error", "error"],
WARNING: ["Warning", "warning"],
INFO: ["Info", "info", "Information"],
} as const;

interface FormattedException {
severity: string;
source: string;
message: string;
code: number;
fileName?: string;
lineNo?: number;
column?: number;
}

export interface IGetWorkunitErrorsParameters {
/**
* Workunit ID (WUID) to fetch errors and warnings for
*/
wuid: string;
}

function isSeverityType(exception: WsWorkunits.ECLException, severities: readonly string[]): boolean {
return severities.some(s => s === exception.Severity);
}

function formatException(exception: WsWorkunits.ECLException): FormattedException {
return {
severity: exception.Severity,
source: exception.Source,
message: exception.Message,
code: exception.Code,
fileName: exception.FileName,
lineNo: exception.LineNo,
column: exception.Column,
};
}

function addExceptionGroup(
parts: vscode.LanguageModelTextPart[],
title: string,
exceptions: WsWorkunits.ECLException[]
): void {
if (exceptions.length === 0) return;

parts.push(new vscode.LanguageModelTextPart(title));
for (const exception of exceptions) {
parts.push(new vscode.LanguageModelTextPart(JSON.stringify(formatException(exception), null, 2)));
}
}

function categorizeExceptions(exceptions: WsWorkunits.ECLException[]) {
const errors = exceptions.filter(e => isSeverityType(e, SEVERITY.ERROR));
const warnings = exceptions.filter(e => isSeverityType(e, SEVERITY.WARNING));
const infos = exceptions.filter(e => isSeverityType(e, SEVERITY.INFO));
const others = exceptions.filter(e =>
!isSeverityType(e, SEVERITY.ERROR) &&
!isSeverityType(e, SEVERITY.WARNING) &&
!isSeverityType(e, SEVERITY.INFO)
);

return { errors, warnings, infos, others };
}

export class GetWorkunitErrorsTool implements vscode.LanguageModelTool<IGetWorkunitErrorsParameters> {
async invoke(options: vscode.LanguageModelToolInvocationOptions<IGetWorkunitErrorsParameters>, token: vscode.CancellationToken) {
reporter?.sendTelemetryEvent("lmTool.invoke", { tool: "getWorkunitErrors" });
const params = options.input;

const wuid = typeof params.wuid === "string" ? params.wuid.trim() : "";
if (wuid.length === 0) {
throw new vscode.LanguageModelError(localize("WUID is required"), { cause: "invalid_parameters" });
}

logToolEvent("getWorkunitErrors", "invoke start", { wuid });

const session = requireConnectedSession();
const opts = await createServiceOptions(session);

try {
// Attach to the workunit and fetch its details
const wu = Workunit.attach(opts, wuid);
await wu.refresh();

throwIfCancellationRequested(token);

const parts: vscode.LanguageModelTextPart[] = [];

// Add workunit basic state information
const detailsUrl = session.wuDetailsUrl(wu.Wuid);
parts.push(new vscode.LanguageModelTextPart(localize("Errors/Warnings for Workunit {0}:", wuid)));

const summary = localize(
"{0} on {1} is {2}.",
wu.Wuid,
wu.Cluster || localize("unknown cluster"),
wu.State || localize("unknown state")
);
parts.push(new vscode.LanguageModelTextPart(summary));

// Fetch and add exceptions (errors and warnings)
const exceptions = await wu.fetchECLExceptions().catch(() => []);
throwIfCancellationRequested(token);

if (exceptions.length === 0) {
parts.push(new vscode.LanguageModelTextPart(localize("No errors or warnings found for this workunit.")));
} else {
const { errors, warnings, infos, others } = categorizeExceptions(exceptions);

parts.push(new vscode.LanguageModelTextPart(localize(
"Total: {0} exception(s) - {1} error(s), {2} warning(s), {3} info, {4} other",
exceptions.length.toString(),
errors.length.toString(),
warnings.length.toString(),
infos.length.toString(),
others.length.toString()
)));

addExceptionGroup(parts, localize("\nErrors ({0}):", errors.length.toString()), errors);
addExceptionGroup(parts, localize("\nWarnings ({0}):", warnings.length.toString()), warnings);
addExceptionGroup(parts, localize("\nInformational ({0}):", infos.length.toString()), infos);
addExceptionGroup(parts, localize("\nOther ({0}):", others.length.toString()), others);
}

if (detailsUrl) {
parts.push(new vscode.LanguageModelTextPart(`\n${localize("ECL Watch URL:")} ${detailsUrl}`));
}

const { errors, warnings } = categorizeExceptions(exceptions);
logToolEvent("getWorkunitErrors", "invoke success", {
wuid: wu.Wuid,
state: wu.State,
exceptionCount: exceptions.length,
errorCount: errors.length,
warningCount: warnings.length,
});

return new vscode.LanguageModelToolResult(parts);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logToolEvent("getWorkunitErrors", "invoke failed", { wuid, error: errorMessage });
throw new vscode.LanguageModelError(
localize("Failed to fetch workunit errors/warnings: {0}", errorMessage),
{ cause: error }
);
}
}

async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IGetWorkunitErrorsParameters>, _token: vscode.CancellationToken) {
const connected = isPlatformConnected();
const wuid = typeof options.input.wuid === "string" ? options.input.wuid.trim() : "";

return {
invocationMessage: connected
? localize("Fetching errors/warnings for workunit {0}", wuid || localize("(unspecified)"))
: localize("Cannot fetch: HPCC Platform not connected"),
confirmationMessages: connected ? undefined : {
title: localize("HPCC Platform not connected"),
message: new vscode.MarkdownString(localize("This tool requires an active HPCC connection.")),
}
};
}
}