feat(engine): agent-native workflow tools — promote, reconcile-aware select, CRUD, trait catalog (#1408)

This commit is contained in:
gsxdsm
2026-06-04 07:03:53 -07:00
parent 571768f03a
commit c21485a65a
3 changed files with 469 additions and 8 deletions

View File

@@ -17,6 +17,11 @@ import {
createResearchTools,
createWorkflowListTool,
createWorkflowSelectTool,
createTaskPromoteTool,
createWorkflowCreateTool,
createWorkflowUpdateTool,
createWorkflowDeleteTool,
createTraitListTool,
qmdAgentMemoryCollectionName,
readAgentMemoryWorkspaceLongTerm,
sendMessageParams,
@@ -26,6 +31,12 @@ import * as core from "@fusion/core";
import { ChatStore, Database } from "@fusion/core";
import type { MessageStore, Message } from "@fusion/core";
import { getEnabledPluginTools, getResearchToolSurfaceStatus } from "../tool-availability.js";
import { promoteHeldTask } from "../hold-release.js";
vi.mock("../hold-release.js", () => ({
promoteHeldTask: vi.fn(),
}));
const mockPromoteHeldTask = vi.mocked(promoteHeldTask);
const loggerSpies = vi.hoisted(() => ({
log: vi.fn(),
@@ -384,24 +395,40 @@ describe("createWorkflowListTool", () => {
describe("createWorkflowSelectTool", () => {
it("selects for the current task by default and reports enabled step count", async () => {
const store = { selectTaskWorkflow: vi.fn().mockResolvedValue(["workflow:WF-003:lint"]) };
const store = {
selectTaskWorkflowAndReconcile: vi.fn().mockResolvedValue({ enabledWorkflowSteps: ["workflow:WF-003:lint"] }),
};
const tool = createWorkflowSelectTool(store as any, "FN-200");
const result = await tool.execute("call-1", { workflow_id: "WF-003" } as any, undefined, undefined, {} as any);
expect(store.selectTaskWorkflow).toHaveBeenCalledWith("FN-200", "WF-003");
expect(store.selectTaskWorkflowAndReconcile).toHaveBeenCalledWith("FN-200", "WF-003");
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toContain("Selected workflow WF-003 for FN-200 (1 step enabled)");
expect(result.details).toMatchObject({ taskId: "FN-200", workflowId: "WF-003" });
});
it("honors an explicit task_id override", async () => {
const store = { selectTaskWorkflow: vi.fn().mockResolvedValue([]) };
const store = { selectTaskWorkflowAndReconcile: vi.fn().mockResolvedValue({ enabledWorkflowSteps: [] }) };
const tool = createWorkflowSelectTool(store as any, "FN-200");
await tool.execute("call-1", { workflow_id: "builtin:coding", task_id: "FN-999" } as any, undefined, undefined, {} as any);
expect(store.selectTaskWorkflow).toHaveBeenCalledWith("FN-999", "builtin:coding");
expect(store.selectTaskWorkflowAndReconcile).toHaveBeenCalledWith("FN-999", "builtin:coding");
});
it("surfaces the reconciliation re-home outcome", async () => {
const store = {
selectTaskWorkflowAndReconcile: vi.fn().mockResolvedValue({
enabledWorkflowSteps: [],
reconciliation: { preserved: false, fromColumn: "review", toColumn: "intake" },
}),
};
const tool = createWorkflowSelectTool(store as any, "FN-200");
const result = await tool.execute("call-1", { workflow_id: "WF-003" } as any, undefined, undefined, {} as any);
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toContain("Re-homed from 'review' to 'intake'");
expect(result.details).toMatchObject({ reconciliation: { preserved: false, fromColumn: "review", toColumn: "intake" } });
});
it("returns an error result when selection fails", async () => {
const store = { selectTaskWorkflow: vi.fn().mockRejectedValue(new Error("Workflow not found: WF-404")) };
const store = { selectTaskWorkflowAndReconcile: vi.fn().mockRejectedValue(new Error("Workflow not found: WF-404")) };
const tool = createWorkflowSelectTool(store as any, "FN-200");
const result = await tool.execute("call-1", { workflow_id: "WF-404" } as any, undefined, undefined, {} as any);
expect((result as { isError?: boolean }).isError).toBe(true);
@@ -410,6 +437,124 @@ describe("createWorkflowSelectTool", () => {
});
});
describe("createTaskPromoteTool", () => {
beforeEach(() => mockPromoteHeldTask.mockReset());
it("promotes the current task by default and reports the destination column", async () => {
const store = {} as any;
mockPromoteHeldTask.mockResolvedValue({ released: true, toColumn: "ready" });
const tool = createTaskPromoteTool(store, "FN-200");
const result = await tool.execute("c", {} as any, undefined, undefined, {} as any);
expect(mockPromoteHeldTask).toHaveBeenCalledWith(store, "FN-200");
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toContain("Promoted FN-200 to column 'ready'");
expect(result.details).toMatchObject({ taskId: "FN-200", released: true, toColumn: "ready" });
});
it("honors an explicit task_id and surfaces a rejection as an error", async () => {
const store = {} as any;
mockPromoteHeldTask.mockResolvedValue({ released: false, rejection: "not-held" });
const tool = createTaskPromoteTool(store, "FN-200");
const result = await tool.execute("c", { task_id: "FN-999" } as any, undefined, undefined, {} as any);
expect(mockPromoteHeldTask).toHaveBeenCalledWith(store, "FN-999");
expect((result as { isError?: boolean }).isError).toBe(true);
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toMatch(/not-held/);
});
});
describe("createWorkflowCreateTool", () => {
it("creates a workflow and returns the new id", async () => {
const store = { createWorkflowDefinition: vi.fn().mockResolvedValue({ id: "WF-010", name: "QA" }) };
const tool = createWorkflowCreateTool(store as any);
const result = await tool.execute("c", { name: "QA", ir: { columns: [] } } as any, undefined, undefined, {} as any);
expect(store.createWorkflowDefinition).toHaveBeenCalledWith(
expect.objectContaining({ name: "QA", ir: { columns: [] } }),
);
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toContain("Created workflow WF-010 (QA)");
});
it("returns an error result when creation fails", async () => {
const store = { createWorkflowDefinition: vi.fn().mockRejectedValue(new Error("Workflow name is required")) };
const tool = createWorkflowCreateTool(store as any);
const result = await tool.execute("c", { name: "", ir: {} } as any, undefined, undefined, {} as any);
expect((result as { isError?: boolean }).isError).toBe(true);
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toMatch(/name is required/);
});
});
describe("createWorkflowUpdateTool", () => {
it("updates a workflow and reports the name", async () => {
const store = { updateWorkflowDefinition: vi.fn().mockResolvedValue({ id: "WF-010", name: "QA v2" }) };
const tool = createWorkflowUpdateTool(store as any);
const result = await tool.execute("c", { workflow_id: "WF-010", name: "QA v2" } as any, undefined, undefined, {} as any);
expect(store.updateWorkflowDefinition).toHaveBeenCalledWith("WF-010", expect.objectContaining({ name: "QA v2" }));
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toContain("Updated workflow WF-010 (QA v2)");
});
it("forwards rehome_to as rehomeTo", async () => {
const store = { updateWorkflowDefinition: vi.fn().mockResolvedValue({ id: "WF-010", name: "QA" }) };
const tool = createWorkflowUpdateTool(store as any);
await tool.execute("c", { workflow_id: "WF-010", ir: { columns: [] }, rehome_to: "intake" } as any, undefined, undefined, {} as any);
expect(store.updateWorkflowDefinition).toHaveBeenCalledWith("WF-010", expect.objectContaining({ rehomeTo: "intake" }));
});
it("surfaces an OccupiedColumnsError as a structured retryable response", async () => {
const err = new core.OccupiedColumnsError("WF-010", [{ columnId: "review", count: 2 }]);
const store = { updateWorkflowDefinition: vi.fn().mockRejectedValue(err) };
const tool = createWorkflowUpdateTool(store as any);
const result = await tool.execute("c", { workflow_id: "WF-010", ir: { columns: [] } } as any, undefined, undefined, {} as any);
expect((result as { isError?: boolean }).isError).toBe(true);
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toContain("review (2)");
expect(text).toMatch(/rehome_to/);
expect(result.details).toMatchObject({
occupiedColumns: [{ columnId: "review", count: 2 }],
workflowId: "WF-010",
retryWith: "rehome_to",
});
});
});
describe("createWorkflowDeleteTool", () => {
it("deletes a workflow", async () => {
const store = { deleteWorkflowDefinition: vi.fn().mockResolvedValue(undefined) };
const tool = createWorkflowDeleteTool(store as any);
const result = await tool.execute("c", { workflow_id: "WF-010" } as any, undefined, undefined, {} as any);
expect(store.deleteWorkflowDefinition).toHaveBeenCalledWith("WF-010");
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toContain("Deleted workflow WF-010");
});
it("surfaces a built-in protection error", async () => {
const store = {
deleteWorkflowDefinition: vi.fn().mockRejectedValue(new Error("Built-in workflows cannot be deleted")),
};
const tool = createWorkflowDeleteTool(store as any);
const result = await tool.execute("c", { workflow_id: "builtin:coding" } as any, undefined, undefined, {} as any);
expect((result as { isError?: boolean }).isError).toBe(true);
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toMatch(/cannot be deleted/);
});
});
describe("createTraitListTool", () => {
it("lists the trait catalog with ids, names, and flags in details", async () => {
const tool = createTraitListTool();
const result = await tool.execute("c", {} as any, undefined, undefined, {} as any);
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toMatch(/Available traits:/);
const traits = (result.details as { traits?: Array<{ id: string; name: string; flags: unknown }> }).traits ?? [];
expect(traits.length).toBeGreaterThan(0);
expect(traits[0]).toHaveProperty("id");
expect(traits[0]).toHaveProperty("name");
expect(traits[0]).toHaveProperty("flags");
});
});
describe("createTaskLogToolWithContext", () => {
it("returns a graceful archived read-only message instead of throwing", async () => {
const store = {

View File

@@ -12,6 +12,8 @@ import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { join, relative, resolve } from "node:path";
import type { AgentStore, AgentState, AgentCapability, AgentUpdateInput, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput, ReflectionStore, ApprovalRequestStore, ProjectSettings, ChatStore } from "@fusion/core";
import { listTraits } 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 { ResearchOrchestrator } from "./research-orchestrator.js";
import { ResearchProviderRegistry } from "./research/provider-registry.js";
@@ -74,6 +76,47 @@ export const workflowSelectParams = Type.Object({
),
});
export const taskPromoteParams = Type.Object({
task_id: Type.Optional(
Type.String({ description: "Held task to promote. Defaults to the current task." }),
),
});
export const workflowCreateParams = Type.Object({
name: Type.String({ description: "Workflow name (required, non-empty)." }),
description: Type.Optional(Type.String({ description: "Optional human-readable description." })),
ir: Type.Unknown({
description:
"Workflow graph (intermediate representation). Validated server-side; a malformed graph is rejected.",
}),
layout: Type.Optional(
Type.Record(Type.String(), Type.Unknown(), {
description: "Optional node layout map keyed by node id.",
}),
),
});
export const workflowUpdateParams = Type.Object({
workflow_id: Type.String({ description: "The workflow definition ID to update (built-ins cannot be edited)." }),
name: Type.Optional(Type.String({ description: "New name." })),
description: Type.Optional(Type.String({ description: "New description." })),
ir: Type.Optional(Type.Unknown({ description: "Replacement workflow graph (validated server-side)." })),
layout: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Replacement node layout map." })),
rehome_to: Type.Optional(
Type.String({
description:
"When an IR update removes a column that still holds cards, supply the column id to re-home those occupants into. " +
"Required to resolve an OccupiedColumns conflict; the target must exist in the new IR.",
}),
),
});
export const workflowDeleteParams = Type.Object({
workflow_id: Type.String({ description: "The workflow definition ID to delete (built-ins cannot be deleted)." }),
});
export const traitListParams = Type.Object({});
export const reflectOnPerformanceParams = Type.Object({
focus_area: Type.Optional(
Type.String({ description: "Optional focus area for reflection (e.g., 'code quality', 'speed', 'testing')" }),
@@ -1006,13 +1049,23 @@ export function createWorkflowSelectTool(store: TaskStore, currentTaskId: string
execute: async (_id: string, params: Static<typeof workflowSelectParams>) => {
const taskId = params.task_id?.trim() || currentTaskId;
try {
const enabled = await store.selectTaskWorkflow(taskId, params.workflow_id);
const { enabledWorkflowSteps: enabled, reconciliation } =
await store.selectTaskWorkflowAndReconcile(taskId, params.workflow_id);
const stepSummary = `${enabled.length} step${enabled.length === 1 ? "" : "s"} enabled`;
// Surface the reconciliation outcome so the agent observes any re-home:
// a preserved card stays put; an unpreserved card moves fromColumn→toColumn.
const rehomeNote =
reconciliation && !reconciliation.preserved && reconciliation.fromColumn !== reconciliation.toColumn
? ` Re-homed from '${reconciliation.fromColumn}' to '${reconciliation.toColumn}'.`
: reconciliation
? ` Card preserved in '${reconciliation.toColumn}'.`
: "";
return {
content: [{
type: "text" as const,
text: `Selected workflow ${params.workflow_id} for ${taskId} (${enabled.length} step${enabled.length === 1 ? "" : "s"} enabled).`,
text: `Selected workflow ${params.workflow_id} for ${taskId} (${stepSummary}).${rehomeNote}`,
}],
details: { taskId, workflowId: params.workflow_id, enabledWorkflowSteps: enabled },
details: { taskId, workflowId: params.workflow_id, enabledWorkflowSteps: enabled, reconciliation },
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
@@ -1026,6 +1079,239 @@ export function createWorkflowSelectTool(store: TaskStore, currentTaskId: string
};
}
/**
* Create a `fn_task_promote` tool that manually releases a held task out of its
* hold column — the agent-native equivalent of the dashboard's "promote" action.
* Defaults to the current task. Wraps {@link promoteHeldTask}.
*/
export function createTaskPromoteTool(store: TaskStore, currentTaskId: string): ToolDefinition {
return {
name: "fn_task_promote",
label: "Promote Held Task",
description:
"Manually promote a held task out of its hold column, releasing it regardless of the " +
"hold's release kind (the explicit operator action a 'manual' hold waits for). Defaults " +
"to the current task. Returns the destination column, or a rejection reason when the task " +
"is not held or the destination is full.",
parameters: taskPromoteParams,
execute: async (_id: string, params: Static<typeof taskPromoteParams>) => {
const taskId = params.task_id?.trim() || currentTaskId;
try {
const outcome = await promoteHeldTask(store, taskId);
if (outcome.released) {
return {
content: [{
type: "text" as const,
text: `Promoted ${taskId} to column '${outcome.toColumn}'.`,
}],
details: { taskId, released: true, toColumn: outcome.toColumn },
};
}
return {
content: [{
type: "text" as const,
text: `ERROR: Could not promote ${taskId}: ${outcome.rejection ?? "unknown"}.`,
}],
details: { taskId, released: false, rejection: outcome.rejection },
isError: true,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
return {
content: [{ type: "text" as const, text: `ERROR: Failed to promote task: ${err?.message ?? err}` }],
details: {},
isError: true,
};
}
},
};
}
/**
* Create a `fn_workflow_create` tool — a thin wrapper over the store's workflow
* definition create. The IR is validated server-side; a malformed graph rejects.
*/
export function createWorkflowCreateTool(store: TaskStore): ToolDefinition {
return {
name: "fn_workflow_create",
label: "Create Workflow",
description:
"Create a new custom workflow definition from a name and a workflow graph (IR). " +
"The IR is validated server-side. Returns the new workflow ID.",
parameters: workflowCreateParams,
execute: async (_id: string, params: Static<typeof workflowCreateParams>) => {
try {
const created = await store.createWorkflowDefinition({
name: params.name,
description: params.description,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ir: params.ir as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
layout: params.layout as any,
});
return {
content: [{ type: "text" as const, text: `Created workflow ${created.id} (${created.name}).` }],
details: { workflowId: created.id, name: created.name },
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
return {
content: [{ type: "text" as const, text: `ERROR: Failed to create workflow: ${err?.message ?? err}` }],
details: {},
isError: true,
};
}
},
};
}
/**
* Create a `fn_workflow_update` tool — a thin wrapper over the store's workflow
* definition update. When an IR change removes a still-occupied column, the store
* throws an OccupiedColumnsError; we surface it as a structured response carrying
* the per-column occupant counts so the agent can retry with `rehome_to`.
*/
export function createWorkflowUpdateTool(store: TaskStore): ToolDefinition {
return {
name: "fn_workflow_update",
label: "Update Workflow",
description:
"Update a custom workflow definition (name/description/ir/layout). Built-ins cannot be edited. " +
"If an IR change removes a column that still holds cards, the update is blocked and returns the " +
"occupied columns — retry with rehome_to set to a column id that survives in the new IR.",
parameters: workflowUpdateParams,
execute: async (_id: string, params: Static<typeof workflowUpdateParams>) => {
try {
const updated = await store.updateWorkflowDefinition(params.workflow_id, {
name: params.name,
description: params.description,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ir: params.ir as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
layout: params.layout as any,
rehomeTo: params.rehome_to,
});
return {
content: [{ type: "text" as const, text: `Updated workflow ${updated.id} (${updated.name}).` }],
details: { workflowId: updated.id, name: updated.name },
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
// Surface the typed OccupiedColumnsError as a structured, retryable result.
if (err?.name === "OccupiedColumnsError") {
const occupancies = err.occupancies ?? [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const summary = occupancies.map((o: any) => `${o.columnId} (${o.count})`).join(", ");
return {
content: [{
type: "text" as const,
text:
`ERROR: Update removes occupied column(s): ${summary}. ` +
`Retry with rehome_to set to a surviving column id.`,
}],
details: { occupiedColumns: occupancies, workflowId: err.workflowId, retryWith: "rehome_to" },
isError: true,
};
}
return {
content: [{ type: "text" as const, text: `ERROR: Failed to update workflow: ${err?.message ?? err}` }],
details: {},
isError: true,
};
}
},
};
}
/**
* Create a `fn_workflow_delete` tool — a thin wrapper over the store's workflow
* definition delete. Surfaces built-in protection and not-found errors as
* structured responses. (The store auto-re-homes occupants to the default
* workflow on delete, so no rehome target is required here.)
*/
export function createWorkflowDeleteTool(store: TaskStore): ToolDefinition {
return {
name: "fn_workflow_delete",
label: "Delete Workflow",
description:
"Delete a custom workflow definition. Built-ins cannot be deleted. Any tasks using it have " +
"their selection cleared and are re-homed to the default workflow's entry column.",
parameters: workflowDeleteParams,
execute: async (_id: string, params: Static<typeof workflowDeleteParams>) => {
try {
await store.deleteWorkflowDefinition(params.workflow_id);
return {
content: [{ type: "text" as const, text: `Deleted workflow ${params.workflow_id}.` }],
details: { workflowId: params.workflow_id },
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
if (err?.name === "OccupiedColumnsError") {
const occupancies = err.occupancies ?? [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const summary = occupancies.map((o: any) => `${o.columnId} (${o.count})`).join(", ");
return {
content: [{
type: "text" as const,
text: `ERROR: Delete blocked by occupied column(s): ${summary}.`,
}],
details: { occupiedColumns: occupancies, workflowId: err.workflowId },
isError: true,
};
}
return {
content: [{ type: "text" as const, text: `ERROR: Failed to delete workflow: ${err?.message ?? err}` }],
details: {},
isError: true,
};
}
},
};
}
/**
* Create a `fn_trait_list` tool that returns the trait catalog from
* {@link listTraits} — the column-behavior building blocks (id, name, flags)
* used when authoring workflow columns.
*/
export function createTraitListTool(): ToolDefinition {
return {
name: "fn_trait_list",
label: "List Traits",
description:
"List the available column traits (the behavior building blocks for workflow columns): " +
"id, name, description, and behavior flags. Use when authoring or updating a workflow IR.",
parameters: traitListParams,
execute: async () => {
try {
const traits = listTraits();
if (traits.length === 0) {
return {
content: [{ type: "text" as const, text: "No traits are registered." }],
details: { traits: [] },
};
}
const lines = traits.map(
(t) => `- ${t.id}: ${t.name}${t.description ? ` — ${t.description}` : ""}`,
);
return {
content: [{ type: "text" as const, text: `Available traits:\n${lines.join("\n")}` }],
details: {
traits: traits.map((t) => ({ id: t.id, name: t.name, description: t.description, flags: t.flags })),
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
return {
content: [{ type: "text" as const, text: `ERROR: Failed to list traits: ${err?.message ?? err}` }],
details: {},
isError: true,
};
}
},
};
}
export function createMemorySearchTool(rootDir: string, settings?: MemoryToolSettings, options?: MemoryToolOptions): ToolDefinition {
return {
name: "fn_memory_search",

View File

@@ -145,6 +145,11 @@ import {
createTaskLogTool as sharedCreateTaskLogTool,
createWorkflowListTool as sharedCreateWorkflowListTool,
createWorkflowSelectTool as sharedCreateWorkflowSelectTool,
createTaskPromoteTool as sharedCreateTaskPromoteTool,
createWorkflowCreateTool as sharedCreateWorkflowCreateTool,
createWorkflowUpdateTool as sharedCreateWorkflowUpdateTool,
createWorkflowDeleteTool as sharedCreateWorkflowDeleteTool,
createTraitListTool as sharedCreateTraitListTool,
} from "./agent-tools.js";
import { getTaskCompletionBlockerForStore } from "./task-completion.js";
import { createStreamingDeltaNormalizer } from "./streaming-delta.js";
@@ -4790,6 +4795,11 @@ export class TaskExecutor {
this.createTaskDocumentReadTool(task.id),
this.createWorkflowListTool(),
this.createWorkflowSelectTool(task.id),
this.createTaskPromoteTool(task.id),
this.createWorkflowCreateTool(),
this.createWorkflowUpdateTool(),
this.createWorkflowDeleteTool(),
this.createTraitListTool(),
...(isResearchToolSurfaceEnabled(settings)
? createResearchTools({
store: this.store,
@@ -6549,6 +6559,26 @@ export class TaskExecutor {
return sharedCreateWorkflowSelectTool(this.store, taskId);
}
private createTaskPromoteTool(taskId: string): ToolDefinition {
return sharedCreateTaskPromoteTool(this.store, taskId);
}
private createWorkflowCreateTool(): ToolDefinition {
return sharedCreateWorkflowCreateTool(this.store);
}
private createWorkflowUpdateTool(): ToolDefinition {
return sharedCreateWorkflowUpdateTool(this.store);
}
private createWorkflowDeleteTool(): ToolDefinition {
return sharedCreateWorkflowDeleteTool(this.store);
}
private createTraitListTool(): ToolDefinition {
return sharedCreateTraitListTool();
}
private createTaskAddDepTool(taskId: string): ToolDefinition {
const store = this.store;
return {