FN-6375: include attachment paths in recovery prompts
Workflow context-limit recovery prompts now direct agents to read task attachments from the project root. - Pass the project root into reduced workflow step prompts so attachment directories can be absolute when available. - Replace the autonomous-agent "ask for context" wording with direct file-reading guidance for attachments. - Cover root-aware, fallback, placement, and context-limit recovery attachment prompt behavior. - Add a patch changeset for the published Fusion package. Files changed: .../fn-6375-workflow-attachment-recovery-prompt.md | 5 ++ .../src/__tests__/step-session-executor.test.ts | 70 +++++++++++++++++++--- packages/engine/src/step-session-executor.ts | 8 ++- 3 files changed, 72 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-6375 Fusion-Task-Lineage: ed9a5367-1623-49cb-9c7f-116c59ce5117
This commit is contained in:
@@ -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.
|
||||
@@ -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/");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user