FN-6091: add workflow_id support to task tools

Add workflow selection support across task creation, delegation, and updates.

- add workflow_id parameters and response text for engine and CLI task-creation/delegation tools
- allow fn_task_update to select or clear task workflows with reconciliation and validation
- cover workflow selection behavior with engine and extension tests and document the new tool parameters
- add a changeset for the published CLI package

Files changed:
 .changeset/fn-6091-workflow-id-agent-tools.md      |   5 +
 .../cli/skill/fusion/references/engine-tools.md    |   4 +-
 .../cli/skill/fusion/references/extension-tools.md |   9 +-
 .../skill/fusion/references/fusion-capabilities.md |   6 +-
 packages/cli/src/__tests__/extension.test.ts       | 189 +++++++++++++++++++++
 packages/cli/src/extension.ts                      |  71 +++++++-
 packages/engine/src/__tests__/agent-tools.test.ts  |  78 +++++++++
 packages/engine/src/agent-tools.ts                 |  32 +++-
 8 files changed, 374 insertions(+), 20 deletions(-)

Fusion-Task-Id: FN-6091

Fusion-Task-Lineage: 7ab2f36a-feeb-4888-9845-65294bacf1d0
This commit is contained in:
gsxdsm
2026-06-09 11:20:08 -07:00
parent 49bd88f4d0
commit 6271778dde
8 changed files with 374 additions and 20 deletions

View File

@@ -170,6 +170,42 @@ describe("createTaskCreateTool", () => {
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
});
it("passes workflow_id through as workflowId", async () => {
const store = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({ id: "PROJ-099", description: "Test", dependencies: [], column: "triage" }),
};
const tool = createTaskCreateTool(store as any);
const result = await tool.execute(
"call-1",
{ description: "Test", workflow_id: " WF-003 " } as any,
undefined,
undefined,
{} as any,
);
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
workflowId: "WF-003",
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
const responseText = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(responseText).toContain("(workflow: WF-003)");
});
it("omits workflowId when workflow_id is not provided", async () => {
const store = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({ id: "PROJ-100", description: "Test", dependencies: [], column: "triage" }),
};
const tool = createTaskCreateTool(store as any);
await tool.execute("call-1", { description: "Test" } as any, undefined, undefined, {} as any);
expect(store.createTask).toHaveBeenCalledWith(expect.not.objectContaining({
workflowId: expect.anything(),
}), expect.anything());
});
it("passes explicit provenance to store.createTask", async () => {
const store = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
@@ -300,6 +336,48 @@ describe("createDelegateTaskTool", () => {
}), expect.any(Object));
});
it("passes workflow_id through as workflowId for delegated tasks", async () => {
const agentStore = {
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
};
const taskStore = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({ id: "FN-103", dependencies: [], description: "Delegated" }),
};
const tool = createDelegateTaskTool(agentStore as any, taskStore as any);
const result = await tool.execute(
"call-1",
{ agent_id: "agent-1", description: "Delegated", workflow_id: " builtin:coding " } as any,
undefined,
undefined,
{} as any,
);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
workflowId: "builtin:coding",
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
const responseText = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(responseText).toContain("(workflow: builtin:coding)");
});
it("omits workflowId for delegated tasks when workflow_id is not provided", async () => {
const agentStore = {
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
};
const taskStore = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({ id: "FN-104", dependencies: [], description: "Delegated" }),
};
const tool = createDelegateTaskTool(agentStore as any, taskStore as any);
await tool.execute("call-1", { agent_id: "agent-1", description: "Delegated" } as any, undefined, undefined, {} as any);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.not.objectContaining({
workflowId: expect.anything(),
}), expect.anything());
});
it("uses linked-existing wording when delegated task is a deterministic duplicate", async () => {
vi.spyOn(core, "runDeterministicDuplicateGuard").mockResolvedValueOnce({
action: "duplicate",

View File

@@ -43,6 +43,13 @@ export const taskCreateParams = Type.Object({
description: "Task priority (low, normal, high, urgent)",
}),
),
workflow_id: Type.Optional(
Type.String({
description:
"Workflow ID to select for the new task (e.g. 'WF-003' or 'builtin:coding'). " +
"Omit to inherit the project default workflow. Use fn_workflow_list to discover valid IDs.",
}),
),
});
export const taskLogParams = Type.Object({
@@ -212,6 +219,13 @@ export const delegateTaskParams = Type.Object({
dependencies: Type.Optional(
Type.Array(Type.String(), { description: "Task IDs this new task depends on (e.g. [\"KB-001\"])" }),
),
workflow_id: Type.Optional(
Type.String({
description:
"Workflow ID to select for the new task (e.g. 'WF-003' or 'builtin:coding'). " +
"Omit to inherit the project default workflow. Use fn_workflow_list to discover valid IDs.",
}),
),
override: Type.Optional(Type.Boolean({ description: "Set true to bypass executor-role assignment policy" })),
});
@@ -819,15 +833,19 @@ export function createTaskCreateTool(
"Before creating, scan existing open tasks for similar work — if an open task " +
"already covers this, do not create a duplicate. " +
"Optionally set dependencies (e.g., the new task depends on the current one, " +
"or the current task should wait for the new one).",
"or the current task should wait for the new one). " +
"Optionally pass workflow_id to select a workflow at creation time; use " +
"fn_workflow_list to discover valid IDs.",
parameters: taskCreateParams,
execute: async (_id: string, params: Static<typeof taskCreateParams>) => {
try {
const workflowId = params.workflow_id?.trim() || undefined;
const { task, wasDuplicate } = await createAgentTask(store, {
description: params.description,
dependencies: params.dependencies,
column: "triage",
priority: params.priority,
...(workflowId ? { workflowId } : {}),
source: provenance ? {
sourceType: provenance.sourceType,
sourceAgentId: provenance.sourceAgentId,
@@ -836,10 +854,11 @@ export function createTaskCreateTool(
} : undefined,
}, options);
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
const workflow = workflowId ? ` (workflow: ${workflowId})` : "";
return {
content: [{
type: "text" as const,
text: `${wasDuplicate ? "Linked existing" : "Created"} ${task.id}: ${params.description}${deps}`,
text: `${wasDuplicate ? "Linked existing" : "Created"} ${task.id}: ${params.description}${deps}${workflow}`,
}],
details: { taskId: task.id },
};
@@ -2787,7 +2806,9 @@ export function createDelegateTaskTool(
description:
"Create a new task and assign it to a specific agent for execution. The task goes to " +
"'todo' and will be picked up by the target agent on their next heartbeat cycle. " +
"Use fn_list_agents first to find available agents and their capabilities.",
"Use fn_list_agents first to find available agents and their capabilities. " +
"Optionally pass workflow_id to select a workflow at creation time; use " +
"fn_workflow_list to discover valid IDs.",
parameters: delegateTaskParams,
execute: async (_id: string, params: Static<typeof delegateTaskParams>) => {
// Validate target agent exists
@@ -2817,12 +2838,14 @@ export function createDelegateTaskTool(
}
try {
const workflowId = params.workflow_id?.trim() || undefined;
// Create task assigned to the target agent
const { task, wasDuplicate } = await createAgentTask(taskStore, {
description: params.description,
dependencies: params.dependencies,
column: "todo",
assignedAgentId: params.agent_id,
...(workflowId ? { workflowId } : {}),
source: {
sourceType: "api",
...(override ? { sourceMetadata: { executorRoleOverride: true } } : {}),
@@ -2830,10 +2853,11 @@ export function createDelegateTaskTool(
}, options);
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
const workflow = workflowId ? ` (workflow: ${workflowId})` : "";
return {
content: [{
type: "text" as const,
text: `Delegated to ${agent.name} (${agent.id}): ${wasDuplicate ? "Linked existing" : "Created"} ${task.id}${deps}. ` +
text: `Delegated to ${agent.name} (${agent.id}): ${wasDuplicate ? "Linked existing" : "Created"} ${task.id}${deps}${workflow}. ` +
`The task will be picked up by ${agent.name} on their next heartbeat cycle.`,
}],
details: { taskId: task.id, agentId: agent.id, agentName: agent.name },