Skip to content

Commit d694d52

Browse files
Copilotpelikhan
andauthored
Suppress spurious report_incomplete failures after successful Copilot task output (#45599)
* Initial plan * Suppress spurious incomplete failure reports Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Tighten spurious incomplete suppression heuristic Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent 985e75b commit d694d52

2 files changed

Lines changed: 93 additions & 5 deletions

File tree

actions/setup/js/handle_agent_failure.cjs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,6 +1518,42 @@ function loadReportIncompleteMessages(items) {
15181518
}
15191519
}
15201520

1521+
const DIAGNOSTIC_AGENT_OUTPUT_TYPES = new Set(["noop", "missing_tool", "missing_data", "report_incomplete"]);
1522+
const TASK_COMPLETE_REGISTRATION_SIGNALS = ["not registering", "not registered", "not recognizing", "not recognized", "recognition issue", "not yet marked", "haven't marked", "tool calls are not registering"];
1523+
const TASK_COMPLETE_COMPLETION_SIGNALS = ["completed successfully", "safe-output", "safe output", "no remaining work"];
1524+
const TASK_COMPLETE_COMPLETION_REGEXPS = [/\banalysis steps? completed\b/, /\ball steps completed\b/];
1525+
1526+
/**
1527+
* Determine whether agent output contains at least one real task-level item.
1528+
* @param {Array<any> | undefined} items
1529+
* @returns {boolean}
1530+
*/
1531+
function hasTaskLevelAgentOutput(items) {
1532+
if (!Array.isArray(items)) {
1533+
return false;
1534+
}
1535+
return items.some(item => item && typeof item.type === "string" && !DIAGNOSTIC_AGENT_OUTPUT_TYPES.has(item.type));
1536+
}
1537+
1538+
/**
1539+
* Detect a spurious report_incomplete caused only by task_complete registration/recognition
1540+
* trouble after the agent already finished the real task work.
1541+
* @param {{reason?: string, details?: string} | undefined} item
1542+
* @returns {boolean}
1543+
*/
1544+
function isTaskCompleteRegistrationIssue(item) {
1545+
if (!item) {
1546+
return false;
1547+
}
1548+
const text = `${item.reason || ""}\n${item.details || ""}`.toLowerCase();
1549+
if (!text.includes("task_complete")) {
1550+
return false;
1551+
}
1552+
const hasRegistrationSignal = TASK_COMPLETE_REGISTRATION_SIGNALS.some(signal => text.includes(signal));
1553+
const hasCompletionSignal = TASK_COMPLETE_COMPLETION_SIGNALS.some(signal => text.includes(signal)) || TASK_COMPLETE_COMPLETION_REGEXPS.some(regexp => regexp.test(text));
1554+
return hasRegistrationSignal && hasCompletionSignal;
1555+
}
1556+
15211557
/**
15221558
* Build report_incomplete context string for display in failure issues/comments.
15231559
* This surfaces the agent's structured incompletion signal so maintainers can
@@ -2964,6 +3000,13 @@ async function main() {
29643000
let hasCompletedDespiteJobFailure = false;
29653001
const { loadAgentOutput } = require("./load_agent_output.cjs");
29663002
const agentOutputResult = loadAgentOutput();
3003+
const taskCompleteRegistrationIssueOnlyReportIncomplete =
3004+
agentOutputResult.success &&
3005+
agentOutputResult.items &&
3006+
agentOutputResult.items.some(item => item.type === "report_incomplete") &&
3007+
hasAgentTerminalReasonCompleted() &&
3008+
hasTaskLevelAgentOutput(agentOutputResult.items) &&
3009+
agentOutputResult.items.filter(item => item.type === "report_incomplete").every(isTaskCompleteRegistrationIssue);
29673010

29683011
if (agentConclusion === "success") {
29693012
if (!agentOutputResult.success || !agentOutputResult.items || agentOutputResult.items.length === 0) {
@@ -2987,7 +3030,7 @@ async function main() {
29873030
if (nonNoopItems.length === 0) {
29883031
hasOnlyNoopOutputs = true;
29893032
core.info("Agent failed with exit code 1 but produced only noop outputs - treating as successful no-action (transient AI model error)");
2990-
} else if (!nonNoopItems.some(item => item.type === "report_incomplete")) {
3033+
} else if (!nonNoopItems.some(item => item.type === "report_incomplete" && !isTaskCompleteRegistrationIssue(item))) {
29913034
// The agent produced valid non-noop safe outputs (e.g. create_discussion) but the
29923035
// job exit code is non-zero. If terminal_reason: completed is present in the log,
29933036
// the failure was a transient error after the agent finished its task — do not report
@@ -3008,10 +3051,14 @@ async function main() {
30083051
if (agentOutputResult.success && agentOutputResult.items && agentOutputResult.items.length > 0) {
30093052
const reportIncompleteItems = agentOutputResult.items.filter(item => item.type === "report_incomplete");
30103053
if (reportIncompleteItems.length > 0) {
3011-
hasReportIncomplete = true;
3012-
core.info(`Agent emitted ${reportIncompleteItems.length} report_incomplete signal(s) - activating failure handling`);
3013-
for (const item of reportIncompleteItems) {
3014-
core.info(` report_incomplete reason: ${item.reason}`);
3054+
if (taskCompleteRegistrationIssueOnlyReportIncomplete) {
3055+
core.info("Ignoring report_incomplete signal(s) caused only by task_complete registration trouble after successful task-level outputs");
3056+
} else {
3057+
hasReportIncomplete = true;
3058+
core.info(`Agent emitted ${reportIncompleteItems.length} report_incomplete signal(s) - activating failure handling`);
3059+
for (const item of reportIncompleteItems) {
3060+
core.info(` report_incomplete reason: ${item.reason}`);
3061+
}
30153062
}
30163063
}
30173064
}

actions/setup/js/handle_agent_failure.test.cjs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4093,6 +4093,47 @@ describe("handle_agent_failure", () => {
40934093
// report_incomplete overrides the hasCompletedDespiteJobFailure exemption
40944094
expect(createIssueMock).toHaveBeenCalled();
40954095
});
4096+
4097+
it("ignores report_incomplete when it only describes task_complete registration trouble after successful outputs", async () => {
4098+
fs.writeFileSync(
4099+
path.join(tmpDir, "agent_output.json"),
4100+
JSON.stringify({
4101+
items: [
4102+
{ type: "add_comment", body: "done" },
4103+
{ type: "submit_pull_request_review", event: "APPROVE", body: "done" },
4104+
{
4105+
type: "report_incomplete",
4106+
reason: "Test Quality Sentinel analysis completed successfully. However, task_complete tool calls are not registering with the system despite repeated attempts and no remaining work identified.",
4107+
},
4108+
],
4109+
})
4110+
);
4111+
fs.writeFileSync(path.join(tmpDir, "agent-stdio.log"), '{"type":"result","subtype":"success","terminal_reason":"completed","num_turns":10}\n');
4112+
4113+
const createIssueMock = vi.fn();
4114+
const createCommentMock = vi.fn();
4115+
4116+
global.github = {
4117+
rest: {
4118+
search: {
4119+
issuesAndPullRequests: vi.fn(async () => ({ data: { total_count: 0, items: [] } })),
4120+
},
4121+
issues: {
4122+
create: createIssueMock,
4123+
createComment: createCommentMock,
4124+
},
4125+
pulls: { get: vi.fn() },
4126+
},
4127+
graphql: vi.fn(),
4128+
};
4129+
4130+
vi.resetModules();
4131+
const { main: mainFn } = require("./handle_agent_failure.cjs");
4132+
await mainFn();
4133+
4134+
expect(createIssueMock).not.toHaveBeenCalled();
4135+
expect(createCommentMock).not.toHaveBeenCalled();
4136+
});
40964137
});
40974138

40984139
describe("parseFirewallAuthErrors", () => {

0 commit comments

Comments
 (0)