From 0c41add6e91ae78c14d634d89dee289d182e8f85 Mon Sep 17 00:00:00 2001 From: Rob Marsal Date: Mon, 15 Jun 2026 13:41:26 +0100 Subject: [PATCH 1/2] feat(PRO-2874): AI decomp main workflow no longer triggers summaries --- .../ui/aidecompiler/AIDecompilationdWindow.java | 11 +++++++++++ .../core/services/api/TypedApiImplementation.java | 10 ++++++++++ .../ghidra/core/services/api/TypedApiInterface.java | 4 ++++ 3 files changed, 25 insertions(+) diff --git a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java index f2d8fea..476b14f 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java @@ -381,6 +381,7 @@ private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor) var api = service.getApi(); AIDecompilationStatus lastDecompStatus = null; boolean inlineCommentsTriggered = false; + boolean summaryTriggered = false; while (true) { var newStatus = api.pollAIDecompileStatus(id); if (lastDecompStatus == null @@ -414,6 +415,16 @@ private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor) logger.error("Inline comments generation failed for function %s".formatted(functionWithID.function().getName())); return; } + if (!summaryTriggered) { + // Summary generation is no longer automatic on the create-decompilation call; + // callers must POST to kick it off, like inline comments. + summaryTriggered = true; + try { + api.triggerAIDecompilationSummary(id); + } catch (RuntimeException e) { + logger.error("Failed to trigger summary: %s".formatted(e.getMessage())); + } + } if (!inlineCommentsTriggered) { // Trigger on first entry to COMPLETED regardless of reported status: if comments // haven't been requested (or the status endpoint was unreachable), POSTing kicks diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java index 9c9e842..9570b32 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java @@ -523,6 +523,16 @@ public void triggerAIDecompilationInlineComments(FunctionID functionID) { } } + @Override + public void triggerAIDecompilationSummary(FunctionID functionID) { + try { + // POST /v3/functions/{function_id}/ai-decompilation/summary + functionsAiDecompilationApi.regenerateAiDecompilationSummary(functionID.value()); + } catch (ApiException e) { + throw new RuntimeException("Failed to trigger AI decompilation summary", e); + } + } + /** * https://api.reveng.ai/v2/docs#tag/Functions-overview/operation/rename_function_id_v2_functions_rename__function_id__post * diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java index f3494cb..aa0dcd9 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java @@ -160,6 +160,10 @@ default void triggerAIDecompilationInlineComments(FunctionID functionID) { throw new UnsupportedOperationException("triggerAIDecompilationInlineComments not implemented yet"); } + default void triggerAIDecompilationSummary(FunctionID functionID) { + throw new UnsupportedOperationException("triggerAIDecompilationSummary not implemented yet"); + } + void renameFunction(FunctionID id, String newName, String newNameMangled); default FunctionNameScore getNameScore(FunctionMatch match) { From c49145379b26b7e8f7778f1dc6d52430e23c308c Mon Sep 17 00:00:00 2001 From: Rob Marsal Date: Tue, 16 Jun 2026 14:26:59 +0100 Subject: [PATCH 2/2] feat(PRO-2874): AI decomp main workflow no longer triggers summaries --- build.gradle | 2 +- .../aidecompiler/AIDecompilationdWindow.java | 15 ++++++++--- .../services/api/TypedApiImplementation.java | 27 ++++++++++++++----- .../api/types/AIDecompilationStatus.java | 7 +++-- .../api/types/binsync/FunctionArtifact.java | 1 - .../types/binsync/FunctionDependencies.java | 2 +- .../ai/reveng/AIDecompilerComponentTest.java | 10 ++++--- .../reveng/PortalAnalysisIntegrationTest.java | 1 - 8 files changed, 45 insertions(+), 20 deletions(-) diff --git a/build.gradle b/build.gradle index 2bc7b4d..c16f953 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ dependencies { implementation 'org.json:json:20250107' implementation "com.google.guava:guava:33.2.0-jre" implementation group: 'com.fifesoft', name: 'rsyntaxtextarea', version: '3.5.2' - implementation('ai.reveng:sdk:3.86.3') + implementation('ai.reveng:sdk:3.93.2') testImplementation('junit:junit:4.13.1') testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2") diff --git a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java index 476b14f..17020a6 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java @@ -386,6 +386,7 @@ private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor) var newStatus = api.pollAIDecompileStatus(id); if (lastDecompStatus == null || !Objects.equals(newStatus.status(), lastDecompStatus.status()) + || !Objects.equals(newStatus.summaryStatus(), lastDecompStatus.summaryStatus()) || !Objects.equals(newStatus.inlineCommentsStatus(), lastDecompStatus.inlineCommentsStatus()) || newStatus.inlineComments().size() != lastDecompStatus.inlineComments().size()) { lastDecompStatus = newStatus; @@ -415,6 +416,7 @@ private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor) logger.error("Inline comments generation failed for function %s".formatted(functionWithID.function().getName())); return; } + var summaryStatus = newStatus.summaryStatus(); if (!summaryTriggered) { // Summary generation is no longer automatic on the create-decompilation call; // callers must POST to kick it off, like inline comments. @@ -425,10 +427,15 @@ private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor) logger.error("Failed to trigger summary: %s".formatted(e.getMessage())); } } - if (!inlineCommentsTriggered) { - // Trigger on first entry to COMPLETED regardless of reported status: if comments - // haven't been requested (or the status endpoint was unreachable), POSTing kicks - // them off; if they're already running the server treats it as a regenerate. + if (summaryStatus == WorkflowProgress.StatusEnum.FAILED) { + // Server requires a summary to exist before inline comments can be generated; + // if the summary failed there's no point triggering comments. + logger.error("Summary generation failed for function %s; skipping inline comments".formatted(functionWithID.function().getName())); + return; + } + if (summaryStatus == WorkflowProgress.StatusEnum.COMPLETED && !inlineCommentsTriggered) { + // Gate the inline-comments POST on summary completion — the server rejects it + // with HTTP 400 otherwise ("A summary is required before inline comments can be generated"). inlineCommentsTriggered = true; try { api.triggerAIDecompilationInlineComments(id); diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java index 9570b32..fa24f38 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java @@ -445,7 +445,7 @@ public Optional getFunctionDataTypes(AnalysisID analysis public boolean triggerAIDecompilationForFunctionID(FunctionID functionID) { try { // POST /v3/functions/{function_id}/ai-decompilation - var result = functionsAiDecompilationApi.createAiDecompilation(functionID.value(), true, null); + var result = functionsAiDecompilationApi.createAiDecompilation(functionID.value(), false, null); return Boolean.TRUE.equals(result.getStatus()); } catch (ApiException e) { throw new RuntimeException("Failed to trigger AI decompilation", e); @@ -459,9 +459,17 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { DecompilationData data = functionsAiDecompilationApi.getAiDecompilation(functionID.value()); String summary = null; String predictedFunctionName = null; + WorkflowProgress.StatusEnum summaryStatus = null; WorkflowProgress.StatusEnum inlineCommentsStatus = null; List inlineComments = List.of(); if (data.getStatus() == DecompilationData.StatusEnum.COMPLETED) { + try { + // GET /v3/functions/{function_id}/ai-decompilation/summary/status + WorkflowProgress summaryProgress = functionsAiDecompilationApi.getAiDecompilationSummaryStatus(functionID.value()); + summaryStatus = summaryProgress.getStatus(); + } catch (ApiException | RuntimeException e) { + Msg.info(this, "Could not fetch summary status for function " + functionID.value() + ": " + e.getMessage()); + } try { // GET /v3/functions/{function_id}/ai-decompilation/summary SummaryData summaryData = functionsAiDecompilationApi.getAiDecompilationSummary(functionID.value()); @@ -473,9 +481,7 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { // GET /v3/functions/{function_id}/ai-decompilation/tokenised — carries the predicted name TokenisedData tokenised = functionsAiDecompilationApi.getAiDecompilationTokenised(functionID.value()); predictedFunctionName = tokenised.getPredictedFunctionName(); - } catch (ApiException | RuntimeException e) { - // RuntimeException covers IllegalArgumentException thrown by the SDK's JSON validator - // when the live response omits fields the generated model marks required (e.g. name_map). + } catch (ApiException e) { Msg.info(this, "Could not fetch predicted function name for function " + functionID.value() + ": " + e.getMessage()); } try { @@ -506,6 +512,7 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { data.getDecompilation(), summary, predictedFunctionName, + summaryStatus, inlineCommentsStatus, inlineComments); } catch (ApiException e) { @@ -519,7 +526,7 @@ public void triggerAIDecompilationInlineComments(FunctionID functionID) { // POST /v3/functions/{function_id}/ai-decompilation/inline-comments functionsAiDecompilationApi.regenerateAiDecompilationInlineComments(functionID.value()); } catch (ApiException e) { - throw new RuntimeException("Failed to trigger AI decompilation inline comments", e); + throw new RuntimeException("Failed to trigger AI decompilation inline comments: " + describeApiException(e), e); } } @@ -529,10 +536,16 @@ public void triggerAIDecompilationSummary(FunctionID functionID) { // POST /v3/functions/{function_id}/ai-decompilation/summary functionsAiDecompilationApi.regenerateAiDecompilationSummary(functionID.value()); } catch (ApiException e) { - throw new RuntimeException("Failed to trigger AI decompilation summary", e); + throw new RuntimeException("Failed to trigger AI decompilation summary: " + describeApiException(e), e); } } + private static String describeApiException(ApiException e) { + // The SDK's ApiException carries the HTTP status and response body separately from the message; + // surface both so callers logging only getMessage() can still diagnose server-side failures. + return "HTTP " + e.getCode() + " — " + (e.getResponseBody() != null ? e.getResponseBody() : e.getMessage()); + } + /** * https://api.reveng.ai/v2/docs#tag/Functions-overview/operation/rename_function_id_v2_functions_rename__function_id__post * @@ -672,7 +685,7 @@ public List searchCollections(String partialCollectionNa @Override public List searchBinaries(String partialBinaryName, String modelName) throws ApiException { - return this.searchApi.searchBinaries(1, 10, partialBinaryName, null, null, modelName, null).getData().getResults(); + return this.searchApi.searchBinaries(1, 10, partialBinaryName, null, null, modelName, null, null).getData().getResults(); } @Override diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java index 719873a..d806e3b 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java @@ -14,14 +14,17 @@ * endpoints; this record stitches them back together so callers can treat * polling as a single operation. * - * `summary`, `predictedFunctionName`, `inlineCommentsStatus` and - * `inlineComments` are only populated once `status` reaches `COMPLETED`. + * Summary/inline-comments fields are only populated once `status` reaches + * `COMPLETED`. The server rejects an inline-comments trigger until the + * summary has been generated, so consumers must gate that POST on + * `summaryStatus == COMPLETED`. */ public record AIDecompilationStatus( DecompilationData.StatusEnum status, @Nullable String decompilation, @Nullable String summary, @Nullable String predictedFunctionName, + @Nullable WorkflowProgress.StatusEnum summaryStatus, @Nullable WorkflowProgress.StatusEnum inlineCommentsStatus, List inlineComments ) { diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionArtifact.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionArtifact.java index 6b10509..6cb1e6d 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionArtifact.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionArtifact.java @@ -1,6 +1,5 @@ package ai.reveng.toolkit.ghidra.core.services.api.types.binsync; -import ai.reveng.model.FunctionTypeOutput; import org.json.JSONObject; import java.util.ArrayList; diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionDependencies.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionDependencies.java index 4346766..c7a620d 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionDependencies.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionDependencies.java @@ -21,7 +21,7 @@ public record FunctionDependencies( Struct[] structs ) { - public static FunctionDependencies fromOpenAPI(List deps) { + public static FunctionDependencies fromOpenAPI(List deps) { if (deps.isEmpty()) { return null; } diff --git a/src/test/java/ai/reveng/AIDecompilerComponentTest.java b/src/test/java/ai/reveng/AIDecompilerComponentTest.java index a6a4407..9ee8bc7 100644 --- a/src/test/java/ai/reveng/AIDecompilerComponentTest.java +++ b/src/test/java/ai/reveng/AIDecompilerComponentTest.java @@ -2,6 +2,7 @@ import ai.reveng.invoker.ApiException; import ai.reveng.model.DecompilationData; +import ai.reveng.model.WorkflowProgress; import ai.reveng.toolkit.ghidra.binarysimilarity.ui.aidecompiler.AIDecompilationdWindow; import ai.reveng.toolkit.ghidra.core.services.api.AnalysisOptionsBuilder; import ai.reveng.toolkit.ghidra.core.services.api.mocks.UnimplementedAPI; @@ -68,7 +69,8 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { "int func2(int a) { return a + 1; }", "Mocked Description Summary for func2", null, - null, + WorkflowProgress.StatusEnum.COMPLETED, + WorkflowProgress.StatusEnum.COMPLETED, java.util.List.of()); } else if (functionID.value() == 1) { return new AIDecompilationStatus( @@ -76,7 +78,8 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { "void func1() { return; }", "Mocked Description Summary", null, - null, + WorkflowProgress.StatusEnum.COMPLETED, + WorkflowProgress.StatusEnum.COMPLETED, java.util.List.of()); } else { throw new RuntimeException("Unknown FunctionID"); @@ -230,7 +233,8 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { "void func1() { return; }", "Mocked Description Summary", null, - null, + WorkflowProgress.StatusEnum.COMPLETED, + WorkflowProgress.StatusEnum.COMPLETED, java.util.List.of()); } diff --git a/src/test/java/ai/reveng/PortalAnalysisIntegrationTest.java b/src/test/java/ai/reveng/PortalAnalysisIntegrationTest.java index 6b918b7..4178d42 100644 --- a/src/test/java/ai/reveng/PortalAnalysisIntegrationTest.java +++ b/src/test/java/ai/reveng/PortalAnalysisIntegrationTest.java @@ -4,7 +4,6 @@ import ai.reveng.model.FunctionDataTypesList; import ai.reveng.model.FunctionDataTypesListItem; import ai.reveng.model.FunctionInfoOutput; -import ai.reveng.model.FunctionTypeOutput; import ai.reveng.toolkit.ghidra.core.RevEngAIAnalysisResultsLoaded; import ai.reveng.toolkit.ghidra.core.RevEngAIAnalysisStatusChangedEvent; import ai.reveng.toolkit.ghidra.core.services.api.AnalysisOptionsBuilder;