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:
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user