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:
6
.changeset/FN-4106-workflow-summary-append.md
Normal file
6
.changeset/FN-4106-workflow-summary-append.md
Normal 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.
|
||||
143
packages/engine/src/__tests__/executor-task-done-summary.test.ts
Normal file
143
packages/engine/src/__tests__/executor-task-done-summary.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user