diff --git a/.changeset/agent-task-title-summaries.md b/.changeset/agent-task-title-summaries.md new file mode 100644 index 0000000000..95fd81fef9 --- /dev/null +++ b/.changeset/agent-task-title-summaries.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Agent-created tasks without explicit titles now request AI title summarization regardless of the project auto-summarize setting. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index d414e740ec..0527e03f00 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -472,7 +472,7 @@ Default notes: | `memoryBackupRetention` | `number` | `14` | Number of memory backups to retain. | | `memoryBackupDir` | `string` | `".fusion/backups/memory"` | Relative memory backup directory path. | | `memoryBackupScope` | `"project" \| "agents" \| "all"` | `"all"` | Backup scope: project memory, agent memory, or both. | -| `autoSummarizeTitles` | `boolean` | `false` | Auto-generate titles for long untitled descriptions across dashboard/API task creation and agent/tool-created tasks. | +| `autoSummarizeTitles` | `boolean` | `false` | Auto-generate titles for long untitled descriptions across dashboard/API task creation. Agent-created tasks from `fn_task_create` and `fn_delegate_task` always request summarization for untitled tasks, regardless of this setting. | | `useAiMergeCommitSummary` | `boolean` | `true` | Use AI-generated merge commit summaries (subject + bullet body + diff-stat) instead of raw step-commit subject lists. | | `titleSummarizerProvider` | `string` | `undefined` | Provider for title summarization. | | `titleSummarizerModelId` | `string` | `undefined` | Model ID for title summarization. | diff --git a/packages/engine/src/__tests__/agent-tools.test.ts b/packages/engine/src/__tests__/agent-tools.test.ts index 522a69567b..8e52400f89 100644 --- a/packages/engine/src/__tests__/agent-tools.test.ts +++ b/packages/engine/src/__tests__/agent-tools.test.ts @@ -144,6 +144,7 @@ describe("createTaskCreateTool", () => { dependencies: ["PROJ-001"], column: "triage", priority: undefined, + summarize: true, source: undefined, }, { settings: { autoSummarizeTitles: false }, @@ -167,6 +168,7 @@ describe("createTaskCreateTool", () => { expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ priority: "high", + summarize: true, }), expect.objectContaining({ settings: { autoSummarizeTitles: false } })); }); @@ -220,6 +222,7 @@ describe("createTaskCreateTool", () => { await tool.execute("call-1", { description: "Test" } as any, undefined, undefined, {} as any); expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ + summarize: true, source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-123", @@ -245,6 +248,7 @@ describe("createTaskCreateTool", () => { await tool.execute("call-1", { description: "spawn follow-up" } as any, undefined, undefined, {} as any); expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ + summarize: true, source: { sourceType: "agent_heartbeat", sourceAgentId: "agent-123", @@ -297,6 +301,65 @@ describe("createTaskCreateTool", () => { expect(second.wasDuplicate).toBe(true); expect(store.createTask).toHaveBeenCalledTimes(1); }); + + it("passes summarize: true when no title provided", async () => { + const created = { id: "FN-201", description: "untitled follow-up", dependencies: [], column: "triage" }; + const store = { + getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }), + createTask: vi.fn().mockResolvedValue(created), + }; + + await createAgentTask(store as any, { description: "untitled follow-up" } as any); + + expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ + description: "untitled follow-up", + summarize: true, + }), expect.objectContaining({ settings: { autoSummarizeTitles: false } })); + }); + + it("does not set summarize when title is provided", async () => { + const created = { id: "FN-202", title: "Explicit title", description: "titled follow-up", dependencies: [], column: "triage" }; + const store = { + getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }), + createTask: vi.fn().mockResolvedValue(created), + }; + + await createAgentTask(store as any, { title: "Explicit title", description: "titled follow-up" } as any); + + expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ + title: "Explicit title", + summarize: undefined, + }), expect.objectContaining({ settings: { autoSummarizeTitles: false } })); + }); + + it("summarize triggers title generation even when autoSummarizeTitles is false", async () => { + const summarizeSpy = vi.spyOn(core, "summarizeTitle").mockResolvedValue("Generated title"); + const created = { id: "FN-203", description: "untitled follow-up", dependencies: [], column: "triage" }; + const store = { + getSettings: vi.fn().mockResolvedValue({ + autoSummarizeTitles: false, + titleSummarizerProvider: "openai", + titleSummarizerModelId: "gpt-4o-mini", + }), + createTask: vi.fn().mockResolvedValue(created), + }; + + await createAgentTask(store as any, { description: "untitled follow-up" } as any, { rootDir: "/repo" }); + + const createInput = vi.mocked(store.createTask).mock.calls[0]?.[0]; + const createOptions = vi.mocked(store.createTask).mock.calls[0]?.[1] as { onSummarize?: (description: string) => Promise }; + expect(createInput).toEqual(expect.objectContaining({ + description: "untitled follow-up", + summarize: true, + })); + expect(createOptions).toEqual(expect.objectContaining({ + settings: { autoSummarizeTitles: false }, + })); + expect(createOptions.onSummarize).toBeTypeOf("function"); + await expect(createOptions.onSummarize?.("Long agent-created task description")).resolves.toBe("Generated title"); + expect(summarizeSpy).toHaveBeenCalledWith("Long agent-created task description", "/repo", "openai", "gpt-4o-mini"); + summarizeSpy.mockRestore(); + }); }); describe("createDelegateTaskTool", () => { diff --git a/packages/engine/src/agent-tools.ts b/packages/engine/src/agent-tools.ts index 52de3d83d1..5acc0da0be 100644 --- a/packages/engine/src/agent-tools.ts +++ b/packages/engine/src/agent-tools.ts @@ -779,6 +779,7 @@ export async function createAgentTask( input.githubTracking?.enabled !== false && resolvedTracking.enabled; const createInput: TaskCreateInput = { ...input, + summarize: !input.title?.trim() ? true : undefined, source: nextSource, githubTracking: shouldPrefillGithubTrackingEnabled ? {