feat(engine): fn_workflow_settings agent tool + settings declaration parity, plugin-sdk types, engine-tools docs

This commit is contained in:
gsxdsm
2026-06-04 22:57:01 -07:00
parent 9089e18705
commit 9a4343be26
5 changed files with 437 additions and 7 deletions

View File

@@ -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", ... } }
```

View File

@@ -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<string, unknown>; effective: Record<string, unknown> };
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/);
});
});

View File

@@ -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<typeof workflowCreateParams>) => {
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<typeof workflowUpdateParams>) => {
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<typeof workflowSettingsParams>) => {
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)

View File

@@ -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();
}

View File

@@ -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,