FN-6164: route agent task title summarization through the store
Ensure agent-created tasks use store-managed title summarization before creation hooks fire. - resolve a store-managed summarizer when task creation explicitly requests summarization - defer the task-created hook until store summarization finishes and persists the generated title - stop agent tools from wiring ad hoc summarizer callbacks and pass resolved settings through to createTask - update core and engine tests to cover store-managed summarization, hook deferral, and inherited settings expectations Files changed: packages/core/src/__tests__/store-create.test.ts | 83 +++++++++++++++++++++++ packages/core/src/store.ts | 7 +- packages/engine/src/__tests__/agent-tools.test.ts | 70 +++++++++---------- packages/engine/src/__tests__/triage.test.ts | 12 +++- packages/engine/src/agent-tools.ts | 10 +-- 5 files changed, 136 insertions(+), 46 deletions(-) Fusion-Task-Id: FN-6164 Fusion-Task-Lineage: f8da7bd7-b5d4-4505-98ed-60c19d82904c
This commit is contained in:
@@ -148,7 +148,6 @@ describe("createTaskCreateTool", () => {
|
||||
source: undefined,
|
||||
}, {
|
||||
settings: { autoSummarizeTitles: false },
|
||||
onSummarize: undefined,
|
||||
});
|
||||
expect(result.details).toEqual({ taskId: "PROJ-042" });
|
||||
const responseText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
||||
@@ -302,10 +301,15 @@ describe("createTaskCreateTool", () => {
|
||||
expect(store.createTask).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("passes summarize: true when no title provided", async () => {
|
||||
it("passes summarize: true and full settings when no title provided", async () => {
|
||||
const created = { id: "FN-201", description: "untitled follow-up", dependencies: [], column: "triage" };
|
||||
const settings = {
|
||||
autoSummarizeTitles: false,
|
||||
titleSummarizerProvider: "openai",
|
||||
titleSummarizerModelId: "gpt-4o-mini",
|
||||
};
|
||||
const store = {
|
||||
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
|
||||
getSettings: vi.fn().mockResolvedValue(settings),
|
||||
createTask: vi.fn().mockResolvedValue(created),
|
||||
};
|
||||
|
||||
@@ -314,13 +318,18 @@ describe("createTaskCreateTool", () => {
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
description: "untitled follow-up",
|
||||
summarize: true,
|
||||
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
|
||||
}), expect.objectContaining({ settings }));
|
||||
});
|
||||
|
||||
it("does not set summarize when title is provided", async () => {
|
||||
it("passes full settings and 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 settings = {
|
||||
autoSummarizeTitles: false,
|
||||
titleSummarizerProvider: "openai",
|
||||
titleSummarizerModelId: "gpt-4o-mini",
|
||||
};
|
||||
const store = {
|
||||
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
|
||||
getSettings: vi.fn().mockResolvedValue(settings),
|
||||
createTask: vi.fn().mockResolvedValue(created),
|
||||
};
|
||||
|
||||
@@ -329,36 +338,31 @@ describe("createTaskCreateTool", () => {
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: "Explicit title",
|
||||
summarize: undefined,
|
||||
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
|
||||
}), expect.objectContaining({ settings }));
|
||||
});
|
||||
|
||||
it("summarize triggers title generation even when autoSummarizeTitles is false", async () => {
|
||||
const summarizeSpy = vi.spyOn(core, "summarizeTitle").mockResolvedValue("Generated title");
|
||||
it("passes summarize: true without onSummarize so the store resolves title generation", async () => {
|
||||
const created = { id: "FN-203", description: "untitled follow-up", dependencies: [], column: "triage" };
|
||||
const settings = {
|
||||
autoSummarizeTitles: false,
|
||||
titleSummarizerProvider: "openai",
|
||||
titleSummarizerModelId: "gpt-4o-mini",
|
||||
};
|
||||
const store = {
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
autoSummarizeTitles: false,
|
||||
titleSummarizerProvider: "openai",
|
||||
titleSummarizerModelId: "gpt-4o-mini",
|
||||
}),
|
||||
getSettings: vi.fn().mockResolvedValue(settings),
|
||||
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<string | null> };
|
||||
const createOptions = vi.mocked(store.createTask).mock.calls[0]?.[1] as { onSummarize?: (description: string) => Promise<string | null>; settings?: unknown };
|
||||
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();
|
||||
expect(createOptions).toEqual(expect.objectContaining({ settings }));
|
||||
expect(createOptions.onSummarize).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -465,28 +469,26 @@ describe("createDelegateTaskTool", () => {
|
||||
expect(taskStore.createTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("wires title summarization callback when rootDir is provided", async () => {
|
||||
const summarizeSpy = vi.spyOn(core, "summarizeTitle").mockResolvedValue("Short title");
|
||||
it("does not wire a title summarization callback when rootDir is provided", async () => {
|
||||
const agentStore = {
|
||||
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
|
||||
};
|
||||
const settings = {
|
||||
autoSummarizeTitles: true,
|
||||
titleSummarizerProvider: "openai",
|
||||
titleSummarizerModelId: "gpt-4o-mini",
|
||||
};
|
||||
const taskStore = {
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
autoSummarizeTitles: true,
|
||||
titleSummarizerProvider: "openai",
|
||||
titleSummarizerModelId: "gpt-4o-mini",
|
||||
}),
|
||||
getSettings: vi.fn().mockResolvedValue(settings),
|
||||
createTask: vi.fn().mockResolvedValue({ id: "FN-101", dependencies: [], description: "Delegated" }),
|
||||
};
|
||||
|
||||
const tool = createDelegateTaskTool(agentStore as any, taskStore as any, { rootDir: "/repo" });
|
||||
await tool.execute("call-1", { agent_id: "agent-1", description: "Delegated" } as any, undefined, undefined, {} as any);
|
||||
|
||||
const options = vi.mocked(taskStore.createTask).mock.calls[0]?.[1] as { onSummarize?: (description: string) => Promise<string | null> };
|
||||
expect(options.onSummarize).toBeTypeOf("function");
|
||||
await options.onSummarize?.("Long description");
|
||||
expect(summarizeSpy).toHaveBeenCalledWith("Long description", "/repo", "openai", "gpt-4o-mini");
|
||||
summarizeSpy.mockRestore();
|
||||
const options = vi.mocked(taskStore.createTask).mock.calls[0]?.[1] as { onSummarize?: (description: string) => Promise<string | null>; settings?: unknown };
|
||||
expect(options).toEqual(expect.objectContaining({ settings }));
|
||||
expect(options.onSummarize).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2044,7 +2044,10 @@ describe("taskCreate tool model inheritance", () => {
|
||||
title: "Child Task",
|
||||
description: "Child task description",
|
||||
}), expect.objectContaining({
|
||||
settings: { autoSummarizeTitles: false },
|
||||
settings: expect.objectContaining({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -2143,7 +2146,12 @@ describe("taskCreate tool model inheritance", () => {
|
||||
// The second createTask call should have the resolved sibling id preserved.
|
||||
expect(createTaskMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ dependencies: ["FN-701"] }),
|
||||
expect.objectContaining({ settings: { autoSummarizeTitles: false } }),
|
||||
expect.objectContaining({
|
||||
settings: expect.objectContaining({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(createdSubtasksRef.current).toEqual(["FN-701", "FN-702"]);
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import { join, relative, resolve } from "node:path";
|
||||
import type { AgentState, AgentCapability, AgentUpdateInput, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput, ReflectionStore, ApprovalRequestStore, ProjectSettings, ChatStore, WorkflowSettingDefinition, GoalStatus } from "@fusion/core";
|
||||
import { listTraits, isBuiltinWorkflowId, AgentStore, validateColumnAgentBindings, ColumnAgentBindingError, stripApprovalBypassFlags, WorkflowSettingRejectionError, resolveEffectiveSettingsById, resolveWorkflowIrById, findOrphanedSettingValues, BUILTIN_WORKFLOW_SETTINGS } from "@fusion/core";
|
||||
import { promoteHeldTask } from "./hold-release.js";
|
||||
import { DASHBOARD_USER_ID, canAgentTakeImplementationTaskForExplicitRouting, dailyMemoryPath, ensureOpenClawMemoryFiles, extractAgentProvisioningRequest, formatRoleMismatchReason, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, reconcileDeterministicDuplicate, resolveAgentProvisioningPolicy, resolveMemoryBackend, resolveResearchSettings, resolveTaskGithubTracking, resolveTitleSummarizerSettingsModel, runDeterministicDuplicateGuard, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
|
||||
import { DASHBOARD_USER_ID, canAgentTakeImplementationTaskForExplicitRouting, dailyMemoryPath, ensureOpenClawMemoryFiles, extractAgentProvisioningRequest, formatRoleMismatchReason, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, reconcileDeterministicDuplicate, resolveAgentProvisioningPolicy, resolveMemoryBackend, resolveResearchSettings, resolveTaskGithubTracking, runDeterministicDuplicateGuard, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh } from "@fusion/core";
|
||||
import { ResearchOrchestrator } from "./research-orchestrator.js";
|
||||
import { ResearchProviderRegistry } from "./research/provider-registry.js";
|
||||
import { ResearchStepRunner } from "./research-step-runner.js";
|
||||
@@ -793,13 +793,7 @@ export async function createAgentTask(
|
||||
};
|
||||
|
||||
const createdTask = await store.createTask(createInput, {
|
||||
settings: { autoSummarizeTitles: settings.autoSummarizeTitles === true },
|
||||
onSummarize: rootDir
|
||||
? async (description: string) => {
|
||||
const resolved = resolveTitleSummarizerSettingsModel(settings);
|
||||
return summarizeTitle(description, rootDir, resolved.provider, resolved.modelId);
|
||||
}
|
||||
: undefined,
|
||||
settings,
|
||||
});
|
||||
|
||||
const reconcile = await reconcileDeterministicDuplicate(store, {
|
||||
|
||||
Reference in New Issue
Block a user