feat(FN-4074): add priority option to task creation in engine and extension

Adds task priority support to the task creation flow, spanning the engine runtime (`agent-tools.ts`, `triage.ts`), the CLI extension interface (`extension.ts`), and corresponding documentation and tests. The changeset bumps `@runfusion/fusion` as a minor version.

Fusion-Task-Id: FN-4074

Fusion-Task-Lineage: 726c0ae6-90fb-4c13-ace1-215a2becb2a9
This commit is contained in:
Fusion
2026-05-11 23:31:19 -07:00
committed by gsxdsm
parent 9d52cc4163
commit e9b8ba5c54
15 changed files with 126 additions and 5 deletions

View File

@@ -114,6 +114,7 @@ describe("createTaskCreateTool", () => {
description: "Follow-up task",
dependencies: ["PROJ-001"],
column: "triage",
priority: undefined,
source: undefined,
}, {
settings: { autoSummarizeTitles: false },
@@ -125,6 +126,21 @@ describe("createTaskCreateTool", () => {
expect(responseText).toContain("(depends on: PROJ-001)");
});
it("passes explicit priority to store.createTask", async () => {
const store = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({ id: "PROJ-098", description: "Test", dependencies: [], column: "triage" }),
};
const tool = createTaskCreateTool(store as any);
await tool.execute("call-1", { description: "Test", priority: "high" } as any, undefined, undefined, {} as any);
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
priority: "high",
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
});
it("passes explicit provenance to store.createTask", async () => {
const store = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),

View File

@@ -2603,6 +2603,7 @@ describe("executeHeartbeat", () => {
description: "Follow-up task",
dependencies: undefined,
column: "triage",
priority: undefined,
source: {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-001",
@@ -2610,6 +2611,27 @@ describe("executeHeartbeat", () => {
},
}, expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
});
it("forwards explicit priority when fn_task_create tool is called", async () => {
const store = createStoreWithAgentForExec();
let capturedCreateTool: any;
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
capturedCreateTool = opts.customTools[0];
return { session: mockSession as any };
});
mockSession.prompt = vi.fn().mockImplementation(async () => {
await capturedCreateTool.execute("call-1", { description: "Follow-up task", priority: "high" });
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(mockTaskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
priority: "high",
}), expect.any(Object));
});
});
describe("error handling", () => {

View File

@@ -134,6 +134,7 @@ describe("createHeartbeatTools", () => {
description: "Follow-up task",
dependencies: undefined,
column: "triage",
priority: undefined,
source: {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-001",
@@ -147,6 +148,18 @@ describe("createHeartbeatTools", () => {
expect(result.details).toEqual({ taskId: "FN-100" });
});
it("fn_task_create forwards explicit priority to TaskStore.createTask", async () => {
const store = createMockStore();
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
await tools[0]!.execute("call-1", { description: "Follow-up task", priority: "urgent" }, undefined as any, undefined as any, undefined as any);
expect(mockTaskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
priority: "urgent",
}), expect.any(Object));
});
it("fn_task_create details includes taskId matching mock store return", async () => {
const store = createMockStore();
const matchingStore = createMockTaskStoreForTools({

View File

@@ -20,6 +20,10 @@ function initRepo(dir: string): void {
git(dir, 'git commit -m "chore: initial"');
}
function testTempParent(): string {
return process.env.FUSION_TEST_WORKER_ROOT ?? tmpdir();
}
function assertIsolatedWorkspace(dir: string): void {
const repoRoot = process.env.FUSION_TEST_REAL_ROOT;
if (!repoRoot) return;
@@ -51,7 +55,7 @@ describe("commitOrAmendMergeWithFixes ancestor/equivalent-content short-circuit"
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-ancestor-"));
dir = mkdtempSync(join(testTempParent(), "fusion-test-merger-ancestor-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});

View File

@@ -38,6 +38,10 @@ function createAutostash(dir: string, label: string, content: string): string {
return sha;
}
function testTempParent(): string {
return process.env.FUSION_TEST_WORKER_ROOT ?? tmpdir();
}
function assertIsolatedWorkspace(dir: string): void {
const repoRoot = process.env.FUSION_TEST_REAL_ROOT;
if (!repoRoot) return;
@@ -48,7 +52,7 @@ describe("autostash orphan surface", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-autostash-surface-"));
dir = mkdtempSync(join(testTempParent(), "fusion-test-merger-autostash-surface-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});

View File

@@ -26,11 +26,18 @@ import { computeApprovalDedupeKey } from "./agent-action-gate.js";
// ── Tool parameter schemas (canonical definitions) ────────────────────────
const TASK_CREATE_PRIORITY_VALUES = ["low", "normal", "high", "urgent"] as const;
export const taskCreateParams = Type.Object({
description: Type.String({ description: "What needs to be done" }),
dependencies: Type.Optional(
Type.Array(Type.String(), { description: "Task IDs this new task depends on (e.g. [\"KB-001\"])" }),
),
priority: Type.Optional(
Type.Union(TASK_CREATE_PRIORITY_VALUES.map((priority) => Type.Literal(priority)), {
description: "Task priority (low, normal, high, urgent)",
}),
),
});
export const taskLogParams = Type.Object({
@@ -628,6 +635,7 @@ export function createTaskCreateTool(
description: params.description,
dependencies: params.dependencies,
column: "triage",
priority: params.priority,
source: provenance ? {
sourceType: provenance.sourceType,
sourceAgentId: provenance.sourceAgentId,

View File

@@ -1574,12 +1574,18 @@ export class TriageProcessor {
const taskGetParams = Type.Object({
id: Type.String({ description: "Task ID (e.g. KB-001)" }),
});
const taskCreatePriorityValues = ["low", "normal", "high", "urgent"] as const;
const taskCreateParams = Type.Object({
title: Type.Optional(Type.String({ description: "Short child task title" })),
description: Type.String({ description: "Child task description/mission" }),
dependencies: Type.Optional(
Type.Array(Type.String({ description: "Task ID dependency (e.g. KB-001)" })),
),
priority: Type.Optional(
Type.Union(taskCreatePriorityValues.map((priority) => Type.Literal(priority)), {
description: "Task priority (low, normal, high, urgent)",
}),
),
});
const taskList: ToolDefinition = {
@@ -1740,6 +1746,7 @@ export class TriageProcessor {
description: params.description,
dependencies: validDeps,
column: "triage",
priority: params.priority,
// Inherit parent's model settings if available
modelProvider: parentTask?.modelProvider,
modelId: parentTask?.modelId,