diff --git a/packages/engine/src/__tests__/agent-tools.test.ts b/packages/engine/src/__tests__/agent-tools.test.ts index d2436efdb2..3bd495269b 100644 --- a/packages/engine/src/__tests__/agent-tools.test.ts +++ b/packages/engine/src/__tests__/agent-tools.test.ts @@ -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 = { diff --git a/packages/engine/src/agent-tools.ts b/packages/engine/src/agent-tools.ts index 630cd99639..1b05ae665f 100644 --- a/packages/engine/src/agent-tools.ts +++ b/packages/engine/src/agent-tools.ts @@ -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) => { 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) => { + 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) => { + 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) => { + 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) => { + 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", diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index b2d88c30b7..22cdcf226f 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -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 {