feat(FN-4106): preserve rerun summaries in executor task-done output

Preserves task rerun summaries so they persist across retries rather than being overwritten, with tests validating the persistence behavior and a patch changeset for `@runfusion/fusion`.

Fusion-Task-Id: FN-4106
This commit is contained in:
Fusion
2026-05-12 07:59:41 -07:00
committed by gsxdsm
parent e1dc7e2da3
commit 37063a08ef
3 changed files with 165 additions and 3 deletions

View File

@@ -0,0 +1,6 @@
---
"@runfusion/fusion": patch
---
fn_task_done now appends to the existing task summary when a workflow step
forces a rerun, instead of overwriting the original completion summary.

View File

@@ -0,0 +1,143 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import {
createMockStore,
mockedCreateFnAgent,
mockedExistsSync,
resetExecutorMocks,
} from "./executor-test-helpers.js";
function createBaseTask() {
return {
id: "FN-001",
title: "Test",
description: "Test task",
column: "in-progress",
dependencies: [],
steps: [{ name: "Step 1", status: "in-progress" as const }],
currentStep: 0,
log: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
}
async function setupTaskDoneTool(currentTaskOverrides: Record<string, unknown> = {}) {
const store = createMockStore();
let capturedTool: any = null;
let currentTask: any = {
...createBaseTask(),
...currentTaskOverrides,
};
store.getTask.mockImplementation(async () => ({
...currentTask,
steps: currentTask.steps.map((step: any) => ({ ...step })),
workflowStepResults: currentTask.workflowStepResults?.map((result: any) => ({ ...result })),
}));
mockedCreateFnAgent.mockImplementation(async ({ customTools }: any) => {
capturedTool = customTools?.find((tool: any) => tool.name === "fn_task_done");
return {
session: {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
},
} as any;
});
const executor = new TaskExecutor(store, "/tmp/test");
await executor.execute(createBaseTask() as any);
return {
store,
capturedTool,
setCurrentTask(nextTask: Record<string, unknown>) {
currentTask = { ...currentTask, ...nextTask };
},
};
}
function getSummaryUpdateCalls(store: ReturnType<typeof createMockStore>) {
return store.updateTask.mock.calls.filter((call: any[]) => Object.hasOwn(call[1] ?? {}, "summary"));
}
describe("TaskExecutor fn_task_done summary persistence", () => {
beforeEach(() => {
resetExecutorMocks();
mockedExistsSync.mockReturnValue(true);
});
it("replaces the summary on the first completion when no prior summary or workflow results exist", async () => {
const { store, capturedTool } = await setupTaskDoneTool();
await capturedTool.execute("tool-1", { summary: "Initial summary" });
expect(getSummaryUpdateCalls(store)).toEqual([["FN-001", { summary: "Initial summary" }]]);
});
it("appends rerun summaries when a prior summary exists and workflow steps have already run", async () => {
const { store, capturedTool, setCurrentTask } = await setupTaskDoneTool({
summary: "Original completion summary",
workflowStepResults: [{ stepName: "FrontendUX", status: "revision-requested" }],
});
setCurrentTask({
summary: "Original completion summary",
workflowStepResults: [{ stepName: "FrontendUX", status: "revision-requested" }],
});
await capturedTool.execute("tool-1", { summary: "Addressed workflow feedback" });
const summaryUpdateCalls = getSummaryUpdateCalls(store);
expect(summaryUpdateCalls).toHaveLength(1);
expect(summaryUpdateCalls[0][1].summary).toContain("Original completion summary");
expect(summaryUpdateCalls[0][1].summary).toContain("---\nRerun after workflow step revision:\nAddressed workflow feedback");
expect(store.logEntry).toHaveBeenCalledWith(
"FN-001",
"fn_task_done summary appended to existing summary (workflow-step rerun)",
);
});
it("falls back to replace mode when a prior summary exists but no workflow steps have run yet", async () => {
const { store, capturedTool } = await setupTaskDoneTool({
summary: "Original completion summary",
workflowStepResults: [],
});
await capturedTool.execute("tool-1", { summary: "Replacement summary" });
expect(getSummaryUpdateCalls(store)).toEqual([["FN-001", { summary: "Replacement summary" }]]);
});
it("does not rewrite the summary when fn_task_done receives an empty or missing summary", async () => {
const { store, capturedTool } = await setupTaskDoneTool({
summary: "Original completion summary",
workflowStepResults: [{ stepName: "FrontendUX", status: "passed" }],
});
await capturedTool.execute("tool-1", {});
await capturedTool.execute("tool-2", { summary: " " });
expect(getSummaryUpdateCalls(store)).toHaveLength(0);
});
it("avoids duplicate appends when the rerun summary is already the existing suffix", async () => {
const existingSummary = [
"Original completion summary",
"",
"---",
"Rerun after workflow step revision:",
"Addressed workflow feedback",
].join("\n");
const { store, capturedTool } = await setupTaskDoneTool({
summary: existingSummary,
workflowStepResults: [{ stepName: "FrontendUX", status: "revision-requested" }],
});
await capturedTool.execute("tool-1", { summary: "Addressed workflow feedback" });
expect(getSummaryUpdateCalls(store)).toHaveLength(0);
});
});

View File

@@ -4549,9 +4549,22 @@ export class TaskExecutor {
await store.updateStep(taskId, i, "done");
}
}
// Save summary if provided
if (params.summary) {
await store.updateTask(taskId, { summary: params.summary });
// FN-4106: preserve the original completion summary on workflow-step reruns.
const newSummary = params.summary?.trim();
if (newSummary) {
const currentTask = await store.getTask(taskId);
const existingSummary = currentTask.summary?.trim();
const hasRunWorkflowSteps = (currentTask.workflowStepResults?.length ?? 0) > 0;
const rerunSuffix = `---\nRerun after workflow step revision:\n${newSummary}`;
if (existingSummary && hasRunWorkflowSteps && !existingSummary.endsWith(rerunSuffix)) {
await store.updateTask(taskId, {
summary: `${currentTask.summary}\n\n${rerunSuffix}`,
});
await store.logEntry(taskId, "fn_task_done summary appended to existing summary (workflow-step rerun)");
} else if (!existingSummary || !hasRunWorkflowSteps) {
await store.updateTask(taskId, { summary: params.summary });
}
}
const settings = await store.getSettings();
const hardPauseActive = Boolean(task.paused || settings.globalPause);