diff --git a/.changeset/fn-6375-workflow-attachment-recovery-prompt.md b/.changeset/fn-6375-workflow-attachment-recovery-prompt.md new file mode 100644 index 0000000000..6ec4583ee9 --- /dev/null +++ b/.changeset/fn-6375-workflow-attachment-recovery-prompt.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Workflow step execution now surfaces task attachment locations in the context-recovery prompt path and no longer tells autonomous agents to ask for context. diff --git a/packages/engine/src/__tests__/step-session-executor.test.ts b/packages/engine/src/__tests__/step-session-executor.test.ts index 369fd35932..e16dae7b41 100644 --- a/packages/engine/src/__tests__/step-session-executor.test.ts +++ b/packages/engine/src/__tests__/step-session-executor.test.ts @@ -732,7 +732,7 @@ describe("buildReducedStepPrompt", () => { - [ ] Write unit tests `; - it("includes one-line attachment reference when attachments exist", () => { + it("includes compact attachment location and read instruction when attachments exist", () => { const task = makeTaskDetail({ id: "FN-123", prompt: reducedPrompt, @@ -754,11 +754,35 @@ describe("buildReducedStepPrompt", () => { ], }); - const result = buildReducedStepPrompt(task, 1); + const result = buildReducedStepPrompt(task, 1, "/repo/project"); expect(result).toContain( - "2 attachment(s) available at .fusion/tasks/FN-123/attachments/ — ask for context if needed.", + "2 attachment(s) available at `/repo/project/.fusion/tasks/FN-123/attachments/` — read the files there for context.", ); + expect(result).toContain("They live at the project root and are readable even when working in a worktree."); + expect(result).not.toContain("ask for context"); + }); + + it("falls back to project-relative attachment location when rootDir is omitted", () => { + const task = makeTaskDetail({ + id: "FN-123", + prompt: reducedPrompt, + attachments: [ + { + filename: "abc-shot.png", + originalName: "shot.png", + mimeType: "image/png", + size: 1024, + createdAt: new Date().toISOString(), + }, + ], + }); + + const result = buildReducedStepPrompt(task, 1); + + expect(result).toContain("1 attachment(s) available at `.fusion/tasks/FN-123/attachments/`"); + expect(result).toContain("read the files there for context"); + expect(result).not.toContain("ask for context"); }); it("places the attachment reference after the step and before the important block", () => { @@ -775,9 +799,8 @@ describe("buildReducedStepPrompt", () => { ], }); - const result = buildReducedStepPrompt(task, 1); - const attachmentReference = - "1 attachment(s) available at .fusion/tasks/FN-001/attachments/ — ask for context if needed."; + const result = buildReducedStepPrompt(task, 1, "/repo/project"); + const attachmentReference = "1 attachment(s) available at `/repo/project/.fusion/tasks/FN-001/attachments/`"; expect(result.indexOf(attachmentReference)).toBeGreaterThan(result.indexOf("Add exports")); expect(result.indexOf(attachmentReference)).toBeLessThan(result.indexOf("IMPORTANT:")); @@ -803,18 +826,49 @@ describe("buildReducedStepPrompt", () => { expect(result).not.toContain("shot.png"); }); + it("verifies context-limit recovery symptom is gone for image and non-image attachments", () => { + const task = makeTaskDetail({ + id: "FN-456", + prompt: reducedPrompt, + attachments: [ + { + filename: "abc-shot.png", + originalName: "shot.png", + mimeType: "image/png", + size: 1024, + createdAt: new Date().toISOString(), + }, + { + filename: "def-config.json", + originalName: "config.json", + mimeType: "application/json", + size: 256, + createdAt: new Date().toISOString(), + }, + ], + }); + + const result = buildReducedStepPrompt(task, 1, "/repo/project"); + + expect(result).not.toContain("ask for context"); + expect(result).toContain(".fusion/tasks/FN-456/attachments/"); + expect(result).toContain("/repo/project/.fusion/tasks/FN-456/attachments/"); + }); + it("omits attachment reference when attachments is undefined", () => { const task = makeTaskDetail({ prompt: reducedPrompt, attachments: undefined }); - const result = buildReducedStepPrompt(task, 1); + const result = buildReducedStepPrompt(task, 1, "/repo/project"); expect(result).not.toContain("attachment(s) available"); + expect(result).not.toContain(".fusion/tasks/FN-001/attachments/"); }); it("omits attachment reference when attachments is empty", () => { const task = makeTaskDetail({ prompt: reducedPrompt, attachments: [] }); - const result = buildReducedStepPrompt(task, 1); + const result = buildReducedStepPrompt(task, 1, "/repo/project"); expect(result).not.toContain("attachment(s) available"); + expect(result).not.toContain(".fusion/tasks/FN-001/attachments/"); }); }); diff --git a/packages/engine/src/step-session-executor.ts b/packages/engine/src/step-session-executor.ts index c6d844aa48..481adec506 100644 --- a/packages/engine/src/step-session-executor.ts +++ b/packages/engine/src/step-session-executor.ts @@ -555,14 +555,16 @@ function escapeRegex(str: string): string { * * @param taskDetail - The task to build a prompt for. * @param stepIndex - The 0-based step index. + * @param rootDir - Optional project root directory used to render absolute attachment paths. * @returns A reduced prompt string focused on the current step only. */ -export function buildReducedStepPrompt(taskDetail: TaskDetail, stepIndex: number): string { +export function buildReducedStepPrompt(taskDetail: TaskDetail, stepIndex: number, rootDir?: string): string { const { prompt, id, title, attachments } = taskDetail; // Extract the step-specific section const stepSection = extractStepSection(prompt, stepIndex); const hasAttachments = Boolean(attachments && attachments.length > 0); + const attachmentDir = rootDir ? `${rootDir}/.fusion/tasks/${id}/attachments/` : `.fusion/tasks/${id}/attachments/`; // Build a minimal prompt that focuses on the step without excessive context const parts: string[] = [ @@ -574,7 +576,7 @@ export function buildReducedStepPrompt(taskDetail: TaskDetail, stepIndex: number stepSection, "", hasAttachments - ? `${attachments?.length ?? 0} attachment(s) available at .fusion/tasks/${id}/attachments/ — ask for context if needed.` + ? `${attachments?.length ?? 0} attachment(s) available at \`${attachmentDir}\` — read the files there for context. They live at the project root and are readable even when working in a worktree.` : "", "", "IMPORTANT: Your previous attempt hit the context window limit.", @@ -937,7 +939,7 @@ export class StepSessionExecutor { const stepPrompt = buildStepPrompt(taskDetail, stepIndex, this.options.rootDir, settings, worktreePath); // Build reduced step prompt for context-limit recovery (simpler, shorter) - const reducedStepPrompt = buildReducedStepPrompt(taskDetail, stepIndex); + const reducedStepPrompt = buildReducedStepPrompt(taskDetail, stepIndex, this.options.rootDir); // Acquire semaphore if provided if (semaphore) {