diff --git a/packages/cli/skill/fusion/references/engine-tools.md b/packages/cli/skill/fusion/references/engine-tools.md index 79e3dae523..f8950725e8 100644 --- a/packages/cli/skill/fusion/references/engine-tools.md +++ b/packages/cli/skill/fusion/references/engine-tools.md @@ -16,11 +16,12 @@ These tools are **not** part of the user-invokable extension surface. They are i | `fn_task_document_write` | triage, executor, heartbeat | Save/update a named task document revision | `key` (string), `content` (string), `author?` (string) | | `fn_task_document_read` | triage, executor, heartbeat | Read one task document or list all | `key?` (string) | | `fn_workflow_list` | executor | List the project's custom workflows (read-only built-ins plus user definitions) | none | -| `fn_workflow_get` | executor | Fetch one workflow definition by id — name, description, builtin flag, and the full IR (nodes/edges/columns/artifacts/fields) as JSON | `workflow_id` (string) | +| `fn_workflow_get` | executor | Fetch one workflow definition by id — name, description, builtin flag, and the full IR (nodes/edges/columns/artifacts/fields/settings) as JSON | `workflow_id` (string) | | `fn_workflow_select` | executor | Assign a custom workflow to a task (defaults to the current task) | `workflow_id` (string), `task_id?` (string) | -| `fn_workflow_create` | executor | Create a custom workflow definition from a graph IR (validated server-side). v2 IR supports step-inversion constructs: `parse-steps`, `foreach` (mode/isolation/concurrency/maxReworkCycles), `step-execute`, `step-review`, `code` nodes, `rework` edges, plus `artifacts` and custom `fields` declarations | `name` (string), `description?` (string), `ir` (object), `layout?` (object) | -| `fn_workflow_update` | executor | Update a custom workflow definition's name/description/ir/layout (built-ins cannot be edited; same step-inversion IR constructs as create; editing `fields` orphans rather than destroys existing task values) | `workflow_id` (string), `name?` (string), `description?` (string), `ir?` (object), `layout?` (object), `rehome_to?` (string) | +| `fn_workflow_create` | executor | Create a custom workflow definition from a graph IR (validated server-side). v2 IR supports step-inversion constructs: `parse-steps`, `foreach` (mode/isolation/concurrency/maxReworkCycles), `step-execute`, `step-review`, `code` nodes, `rework` edges, plus `artifacts`, custom `fields`, and typed `settings` declarations | `name` (string), `description?` (string), `ir` (object), `layout?` (object) | +| `fn_workflow_update` | executor | Update a custom workflow definition's name/description/ir/layout (built-ins cannot be edited; same step-inversion IR constructs as create; editing `fields` orphans rather than destroys existing task values; editing `settings` declarations drops orphaned setting values on resolution) | `workflow_id` (string), `name?` (string), `description?` (string), `ir?` (object), `layout?` (object), `rehome_to?` (string) | | `fn_workflow_delete` | executor | Delete a custom workflow definition (built-ins cannot be deleted); selecting tasks are re-homed to the default workflow's entry column | `workflow_id` (string) | +| `fn_workflow_settings` | executor | Read/write a workflow's per-`(workflow, project)` setting **values** (`get` returns `{stored, effective}`; `set` writes `values`, with `null` clearing an override). Validated against the named workflow's declared settings; built-in **values** are writable though built-in **declarations** are not; invalid values return a typed rejection list and persist nothing | `action` (`get` \| `set`), `workflow_id` (string), `values?` (object keyed by setting id) | | `fn_task_promote` | executor | Promote a held task out of a manual-release hold column (defaults to the current task) | `task_id?` (string) | | `fn_trait_list` | executor | List the registered column trait catalog (built-in and plugin traits) | none | | `fn_memory_search` | triage, executor, heartbeat | Search project memory plus per-agent layered memory snippets | `query` (string), `limit?` (number) | @@ -76,3 +77,59 @@ Note: step-session execution (`step-session-executor.ts`) reuses executor coordi | Tool | Purpose | Parameters | |---|---|---| | `fn_heartbeat_done` | Signal end of heartbeat run with optional summary | `summary?` (string) | + +## Workflow settings: declarations vs. values + +Workflow settings split into two surfaces (the same split as custom task fields, one level up): + +- **Declarations** (the typed schema) live in the workflow IR's `settings` array and are authored with `fn_workflow_create` / `fn_workflow_update`. Built-in workflow declarations cannot be edited (the store's built-in guard rejects the IR edit with a `WorkflowIrError`/built-in error surfaced through the tool result). +- **Values** (the per-`(workflow, project)` data) are read/written with `fn_workflow_settings`. Built-in workflow **values** are writable so each project can tune `builtin:coding` differently. + +Declare a setting (custom workflow): + +```jsonc +// fn_workflow_create +{ + "name": "QA", + "ir": { + "version": "v2", + "name": "QA", + "columns": [{ "id": "intake", "name": "Intake", "traits": [] }], + "nodes": [], + "edges": [], + "settings": [ + { "id": "reviewHandoffPolicy", "name": "Review handoff", "type": "enum", + "default": "disabled", + "options": [ + { "value": "disabled", "label": "Disabled" }, + { "value": "always", "label": "Always" } + ] } + ] + } +} +``` + +Write a value (built-in workflow VALUE — accepted even though built-in declarations are read-only): + +```jsonc +// fn_workflow_settings +{ "action": "set", "workflow_id": "builtin:coding", + "values": { "workflowStepTimeoutMs": 600000, "reviewHandoffPolicy": "always" } } +``` + +An invalid value (e.g. an enum violation) is rejected with a typed list and persists nothing: + +```jsonc +// returns isError:true with details.rejections: +// [{ "code": "enum-violation", "settingId": "reviewHandoffPolicy", "message": "..." }] +``` + +Read values — `effective` is what the engine actually consumes (declaration defaults filled in, orphaned values dropped); `stored` is the raw override map: + +```jsonc +// fn_workflow_settings +{ "action": "get", "workflow_id": "builtin:coding" } +// → { "workflowId": "builtin:coding", +// "stored": { "workflowStepTimeoutMs": 600000 }, +// "effective": { "workflowStepTimeoutMs": 600000, "reviewHandoffPolicy": "disabled", ... } } +``` diff --git a/packages/engine/src/__tests__/agent-tools-workflow-settings.test.ts b/packages/engine/src/__tests__/agent-tools-workflow-settings.test.ts new file mode 100644 index 0000000000..5c45e2315b --- /dev/null +++ b/packages/engine/src/__tests__/agent-tools-workflow-settings.test.ts @@ -0,0 +1,208 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { mkdtempSync } from "node:fs"; +import { rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { TaskStore, resolveEffectiveSettingsById, BUILTIN_WORKFLOW_SETTINGS } from "@fusion/core"; +import { + createWorkflowCreateTool, + createWorkflowUpdateTool, + createWorkflowSettingsTool, +} from "../agent-tools.js"; + +/** + * U7 — agent-tool parity for workflow settings. These exercise the REAL store + * (not a vi.fn mock) so that declarations pass through `parseWorkflowIr` exactly + * as editor saves do, and value writes hit the same write authority + * (`updateWorkflowSettingValues`) with the same typed-rejection contract. + */ +function makeTmpDir(prefix: string): string { + return mkdtempSync(join(tmpdir(), prefix)); +} + +function textOf(result: { content: Array<{ type: string; text?: string }> }): string { + const first = result.content[0]; + return first && first.type === "text" ? (first.text ?? "") : ""; +} + +const callCtx = [undefined, undefined, {} as never] as const; + +describe("agent workflow-settings parity (U7)", () => { + let rootDir: string; + let globalDir: string; + let store: TaskStore; + + beforeEach(async () => { + rootDir = makeTmpDir("kb-engine-wf-settings-"); + globalDir = makeTmpDir("kb-engine-wf-settings-global-"); + store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); + await store.init(); + }); + + afterEach(async () => { + store.close(); + await rm(rootDir, { recursive: true, force: true }); + await rm(globalDir, { recursive: true, force: true }); + }); + + // ── Declaration parity: create with settings ──────────────────────────── + it("creates a workflow with settings declarations, validated and persisted identically to editor saves", async () => { + const create = createWorkflowCreateTool(store); + const ir = { + version: "v2", + name: "QA", + columns: [{ id: "intake", name: "Intake", traits: [] }], + nodes: [ + { id: "start", kind: "start", column: "intake" }, + { id: "end", kind: "end", column: "intake" }, + ], + edges: [{ from: "start", to: "end" }], + settings: [ + { + id: "reviewHandoffPolicy", + name: "Review handoff", + type: "enum", + default: "disabled", + options: [ + { value: "disabled", label: "Disabled" }, + { value: "always", label: "Always" }, + ], + }, + { id: "workflowStepTimeoutMs", name: "Step timeout", type: "number", default: 60000 }, + ], + }; + const result = await create.execute("c", { name: "QA", ir } as never, ...callCtx); + expect((result as { isError?: boolean }).isError).toBeFalsy(); + const workflowId = (result.details as { workflowId: string }).workflowId; + expect(workflowId).toBeTruthy(); + + // Round-trips through the store's parse/persist path with the settings intact. + const def = await store.getWorkflowDefinition(workflowId); + const persisted = def?.ir as { settings?: Array<{ id: string }> }; + expect(persisted.settings?.map((s) => s.id)).toEqual(["reviewHandoffPolicy", "workflowStepTimeoutMs"]); + }); + + it("rejects an invalid settings declaration with the WorkflowIr validation error surfaced through the tool result", async () => { + const create = createWorkflowCreateTool(store); + const ir = { + version: "v2", + name: "Bad", + columns: [{ id: "intake", name: "Intake", traits: [] }], + nodes: [ + { id: "start", kind: "start", column: "intake" }, + { id: "end", kind: "end", column: "intake" }, + ], + edges: [{ from: "start", to: "end" }], + // enum without options is invalid (parseWorkflowIr -> WorkflowIrError). + settings: [{ id: "mode", name: "Mode", type: "enum" }], + }; + const result = await create.execute("c", { name: "Bad", ir } as never, ...callCtx); + expect((result as { isError?: boolean }).isError).toBe(true); + expect(textOf(result)).toMatch(/must declare non-empty options/); + }); + + // ── Two-path contract: builtin VALUE write ok; builtin DECLARATION edit rejected ── + it("accepts a value write for (builtin:coding, project)", async () => { + const settingsTool = createWorkflowSettingsTool(store); + const result = await settingsTool.execute( + "c", + { action: "set", workflow_id: "builtin:coding", values: { workflowStepTimeoutMs: 600000 } } as never, + ...callCtx, + ); + expect((result as { isError?: boolean }).isError).toBeFalsy(); + const projectId = store.getWorkflowSettingsProjectId(); + expect(store.getWorkflowSettingValues("builtin:coding", projectId)).toMatchObject({ + workflowStepTimeoutMs: 600000, + }); + }); + + it("rejects a builtin DECLARATION edit with the distinct built-in error (the other half of the two-path contract)", async () => { + const update = createWorkflowUpdateTool(store); + const ir = { + version: "v2", + name: "Coding", + columns: [{ id: "intake", name: "Intake", traits: [] }], + nodes: [], + edges: [], + settings: [{ id: "workflowStepTimeoutMs", name: "Step timeout", type: "number", default: 1 }], + }; + const result = await update.execute( + "c", + { workflow_id: "builtin:coding", ir } as never, + ...callCtx, + ); + expect((result as { isError?: boolean }).isError).toBe(true); + expect(textOf(result)).toMatch(/Built-in workflows cannot be edited/); + }); + + // ── Typed rejection surfaced through the tool result ──────────────────── + it("surfaces an enum-violation value write as a typed rejection list, persisting nothing", async () => { + const settingsTool = createWorkflowSettingsTool(store); + const result = await settingsTool.execute( + "c", + { action: "set", workflow_id: "builtin:coding", values: { reviewHandoffPolicy: "nope" } } as never, + ...callCtx, + ); + expect((result as { isError?: boolean }).isError).toBe(true); + const rejections = (result.details as { rejections?: Array<{ code: string; settingId: string }> }).rejections; + expect(rejections).toEqual([ + expect.objectContaining({ code: "enum-violation", settingId: "reviewHandoffPolicy" }), + ]); + // Write boundary: nothing persisted. + const projectId = store.getWorkflowSettingsProjectId(); + expect(store.getWorkflowSettingValues("builtin:coding", projectId)).not.toHaveProperty("reviewHandoffPolicy"); + }); + + it("rejects an unknown-setting value write with the typed code", async () => { + const settingsTool = createWorkflowSettingsTool(store); + const result = await settingsTool.execute( + "c", + { action: "set", workflow_id: "builtin:coding", values: { totallyUnknown: 1 } } as never, + ...callCtx, + ); + expect((result as { isError?: boolean }).isError).toBe(true); + const rejections = (result.details as { rejections?: Array<{ code: string }> }).rejections; + expect(rejections?.[0]?.code).toBe("unknown-setting"); + }); + + // ── Read path returns { stored, effective } matching resolveEffectiveSettingsById ── + it("read returns stored + effective values matching resolveEffectiveSettingsById", async () => { + const settingsTool = createWorkflowSettingsTool(store); + const projectId = store.getWorkflowSettingsProjectId(); + + // Seed one override. + await settingsTool.execute( + "c", + { action: "set", workflow_id: "builtin:coding", values: { workflowStepTimeoutMs: 123456 } } as never, + ...callCtx, + ); + + const result = await settingsTool.execute( + "c", + { action: "get", workflow_id: "builtin:coding" } as never, + ...callCtx, + ); + expect((result as { isError?: boolean }).isError).toBeFalsy(); + const details = result.details as { stored: Record; effective: Record }; + expect(details.stored).toMatchObject({ workflowStepTimeoutMs: 123456 }); + + const expectedEffective = await resolveEffectiveSettingsById(store, "builtin:coding", projectId); + expect(details.effective).toEqual(expectedEffective); + // The override is reflected in the effective map; an untouched declaration + // default still resolves from BUILTIN_WORKFLOW_SETTINGS. + expect(details.effective.workflowStepTimeoutMs).toBe(123456); + const handoffDefault = BUILTIN_WORKFLOW_SETTINGS.find((s) => s.id === "reviewHandoffPolicy")?.default; + expect(details.effective.reviewHandoffPolicy).toBe(handoffDefault); + }); + + it("set with an empty values map is a tool error", async () => { + const settingsTool = createWorkflowSettingsTool(store); + const result = await settingsTool.execute( + "c", + { action: "set", workflow_id: "builtin:coding", values: {} } as never, + ...callCtx, + ); + expect((result as { isError?: boolean }).isError).toBe(true); + expect(textOf(result)).toMatch(/requires a non-empty `values` map/); + }); +}); diff --git a/packages/engine/src/agent-tools.ts b/packages/engine/src/agent-tools.ts index e680955baa..53253b5e91 100644 --- a/packages/engine/src/agent-tools.ts +++ b/packages/engine/src/agent-tools.ts @@ -12,7 +12,7 @@ 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, isBuiltinWorkflowId } from "@fusion/core"; +import { listTraits, isBuiltinWorkflowId, WorkflowSettingRejectionError, resolveEffectiveSettingsById } 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"; @@ -123,6 +123,30 @@ export const workflowDeleteParams = Type.Object({ workflow_id: Type.String({ description: "The workflow definition ID to delete (built-ins cannot be deleted)." }), }); +export const workflowSettingsParams = Type.Object({ + action: Type.Union([Type.Literal("get"), Type.Literal("set")], { + description: + "`get` reads the stored setting VALUES plus the engine-effective values for the workflow; " + + "`set` writes values (requires `values`).", + }), + workflow_id: Type.String({ + description: + "The workflow whose setting VALUES to read/write (e.g. 'WF-003', or a 'builtin:*' id). " + + "Values are scoped per (workflow, project). Built-in workflow VALUES are writable even though " + + "built-in DECLARATIONS are not (declarations are edited via the workflow IR's `settings`). " + + "Values are validated against THIS workflow's declared settings (use fn_workflow_get to inspect them).", + }), + values: Type.Optional( + Type.Record(Type.String(), Type.Unknown(), { + description: + "For action='set': a map of settingId → value to write. A `null` value DELETES the override " + + "(null-as-delete). Each value is validated against the named workflow's declaration; on ANY " + + "rejection (unknown-setting/type-mismatch/enum-violation/no-settings-defined) nothing is " + + "persisted and the typed rejection list is returned.", + }), + ), +}); + export const traitListParams = Type.Object({}); export const reflectOnPerformanceParams = Type.Object({ @@ -1222,7 +1246,11 @@ export function createWorkflowCreateTool(store: TaskStore): ToolDefinition { "`code` node {source, timeoutMs?} runs sandboxed TypeScript returning {outcome?, contextPatch?, customFields?}. " + "Declare task documents via `artifacts: [{key, title?, producedBy?, role?}]` and custom task fields via " + "`fields: [{id, name, type, required?, default?, options?, render?}]` (types: string/text/number/boolean/" + - "enum/multi-enum/date/url; render.placement card|detail|detail-section, render.badge for card chips).", + "enum/multi-enum/date/url; render.placement card|detail|detail-section, render.badge for card chips).\n" + + "Declare typed workflow SETTINGS via `settings: [{id, name, type, default?, options?, description?, render?}]` " + + "(types: string/text/number/boolean/enum/multi-enum; settings have no card/detail placement — widget only). " + + "Settings carry workflow-scoped policy (step timeouts, review gates, model lanes); their per-project VALUES are " + + "read/written via fn_workflow_settings, not here.", parameters: workflowCreateParams, execute: async (_id: string, params: Static) => { try { @@ -1265,8 +1293,11 @@ export function createWorkflowUpdateTool(store: TaskStore): ToolDefinition { "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. " + "The IR accepts the same step-inversion constructs as fn_workflow_create (foreach with mode/isolation/" + - "concurrency, step-execute, step-review, parse-steps, code nodes, rework edges, artifacts, fields). " + - "Editing `fields` orphans (never destroys) existing task values for removed/incompatible fields.", + "concurrency, step-execute, step-review, parse-steps, code nodes, rework edges, artifacts, fields, settings). " + + "Editing `fields` orphans (never destroys) existing task values for removed/incompatible fields. " + + "Editing `settings` declarations changes the schema; orphaned setting VALUES are dropped on resolution " + + "(the engine never sees a value that no longer validates). Built-in workflow declarations cannot be edited; " + + "their per-project VALUES are written via fn_workflow_settings.", parameters: workflowUpdateParams, execute: async (_id: string, params: Static) => { try { @@ -1357,6 +1388,128 @@ export function createWorkflowDeleteTool(store: TaskStore): ToolDefinition { }; } +/** + * Create a `fn_workflow_settings` tool — read/write the per-`(workflow, project)` + * setting VALUES that tune a workflow's policy (step timeouts, review gates, model + * lanes). This is the agent-native parity with the editor's workflow settings + * panel and mirrors the two-path contract from U2: + * + * - DECLARATIONS (the typed schema) live in the workflow IR's `settings` array and + * are authored via fn_workflow_create/update; built-in declarations are not + * editable (the store's built-in guard rejects an IR edit). + * - VALUES are written here against the NAMED workflow's declarations via + * {@link store.updateWorkflowSettingValues}. Built-in workflow VALUES are + * writable (per-project tuning of `builtin:coding`). An invalid value surfaces + * the typed rejection list ({@link WorkflowSettingRejectionError}) and persists + * nothing — the same contract HTTP/dashboard writers see. + * + * `action: "get"` returns both the raw `stored` values and the engine `effective` + * values (post drop-on-orphan), so an agent sees what it wrote and what the engine + * will actually consume. + */ +export function createWorkflowSettingsTool(store: TaskStore): ToolDefinition { + return { + name: "fn_workflow_settings", + label: "Workflow Settings", + description: + "Read or write a workflow's setting VALUES (the per-(workflow, project) policy knobs: step " + + "timeouts, review/approval gates, per-phase model lanes). action='get' returns both the raw " + + "`stored` values and the engine `effective` values (declaration defaults filled in, orphaned " + + "values dropped). action='set' writes `values` against the NAMED workflow's declared settings; " + + "a `null` value clears an override. Built-in workflow VALUES are writable, but built-in " + + "DECLARATIONS are not — declarations are authored in the workflow IR's `settings` array via " + + "fn_workflow_create/update. An invalid value returns the typed rejection list and persists nothing.", + parameters: workflowSettingsParams, + execute: async (_id: string, params: Static) => { + const workflowId = params.workflow_id?.trim(); + if (!workflowId) { + return { + content: [{ type: "text" as const, text: "ERROR: workflow_id is required." }], + details: {}, + isError: true, + }; + } + let projectId: string; + try { + // Resolve the project key the same way the engine resolver does, so agent + // reads/writes share the store's single project scope. + projectId = store.getWorkflowSettingsProjectId(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + return { + content: [{ type: "text" as const, text: `ERROR: Failed to resolve project: ${err?.message ?? err}` }], + details: {}, + isError: true, + }; + } + + if (params.action === "get") { + try { + const stored = store.getWorkflowSettingValues(workflowId, projectId); + const effective = await resolveEffectiveSettingsById(store, workflowId, projectId); + return { + content: [{ + type: "text" as const, + text: JSON.stringify({ workflowId, stored, effective }, null, 2), + }], + details: { workflowId, stored, effective }, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + return { + content: [{ type: "text" as const, text: `ERROR: Failed to read workflow settings: ${err?.message ?? err}` }], + details: {}, + isError: true, + }; + } + } + + // action === "set" + const values = params.values; + if (!values || Object.keys(values).length === 0) { + return { + content: [{ type: "text" as const, text: "ERROR: action='set' requires a non-empty `values` map." }], + details: { error: "No values provided" }, + isError: true, + }; + } + try { + const next = await store.updateWorkflowSettingValues(workflowId, projectId, values); + return { + content: [{ + type: "text" as const, + text: `Updated workflow settings for ${workflowId}: ${JSON.stringify(next)}`, + }], + details: { workflowId, stored: next }, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + // Surface the typed rejection list the same way other value writers do + // (mirrors the custom-field rejection contract) — flat, JSON-safe, with + // machine-stable codes the agent can branch on and retry. + if (err instanceof WorkflowSettingRejectionError || err?.name === "WorkflowSettingRejectionError") { + const rejections = err.rejections ?? []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const summary = rejections.map((r: any) => `${r.settingId} (${r.code})`).join(", "); + return { + content: [{ + type: "text" as const, + text: `ERROR: Rejected workflow setting value(s): ${summary}. Nothing was persisted.`, + }], + details: { workflowId, rejections }, + isError: true, + }; + } + return { + content: [{ type: "text" as const, text: `ERROR: Failed to write workflow settings: ${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) diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index bc7295f663..d7737c6aed 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -164,6 +164,7 @@ import { createWorkflowCreateTool as sharedCreateWorkflowCreateTool, createWorkflowUpdateTool as sharedCreateWorkflowUpdateTool, createWorkflowDeleteTool as sharedCreateWorkflowDeleteTool, + createWorkflowSettingsTool as sharedCreateWorkflowSettingsTool, createTraitListTool as sharedCreateTraitListTool, } from "./agent-tools.js"; import { getTaskCompletionBlockerForStore } from "./task-completion.js"; @@ -5702,6 +5703,7 @@ export class TaskExecutor { this.createWorkflowCreateTool(), this.createWorkflowUpdateTool(), this.createWorkflowDeleteTool(), + this.createWorkflowSettingsTool(), this.createTraitListTool(), ...(isResearchToolSurfaceEnabled(settings) ? createResearchTools({ @@ -7592,6 +7594,10 @@ export class TaskExecutor { return sharedCreateWorkflowDeleteTool(this.store); } + private createWorkflowSettingsTool(): ToolDefinition { + return sharedCreateWorkflowSettingsTool(this.store); + } + private createTraitListTool(): ToolDefinition { return sharedCreateTraitListTool(); } diff --git a/packages/plugin-sdk/src/index.ts b/packages/plugin-sdk/src/index.ts index d9e87b586e..44bfa80e13 100644 --- a/packages/plugin-sdk/src/index.ts +++ b/packages/plugin-sdk/src/index.ts @@ -116,6 +116,12 @@ export type { WorkflowFieldType, WorkflowFieldOption, WorkflowFieldRender, + // Workflow settings (typed, workflow-declared policy schema; values persist + // per-(workflowId, projectId) — mirrors the custom-field surface one level up). + WorkflowSettingDefinition, + WorkflowSettingType, + WorkflowSettingOption, + WorkflowSettingRender, // Step-parser contract. StepParser, StepParseResult,