FN-7274: document workflow tools in Fusion skill

Document the workflow-authoring tool surface in the packaged Fusion skill references.

- Add workflow tools to the generated Fusion skill category and capability tables.
- Teach the skill sync script to read workflow tool specs from extension and engine agent tool sources.
- Cover workflow tool documentation and cache inputs with sync tests.
- Add a changeset for the published CLI skill update.

Files changed:
 .changeset/fn-7274-workflow-skill-tools.md         |   7 +
 packages/cli/skill/fusion/SKILL.md                 |   1 +
 .../cli/skill/fusion/references/engine-tools.md    |   4 +-
 .../cli/skill/fusion/references/extension-tools.md |  83 +++++++++-
 .../skill/fusion/references/fusion-capabilities.md |   8 +
 packages/cli/src/__tests__/skill-sync.test.ts      |  60 ++++++-
 scripts/__tests__/skill-sync-cache.test.mjs        |   5 +
 scripts/sync-fusion-skill-tools.mjs                | 172 ++++++++++++++++++---
 8 files changed, 312 insertions(+), 28 deletions(-)

Fusion-Task-Id: FN-7274

Fusion-Task-Lineage: e1cd6bd8-aecc-49ca-96ed-a937a15a18a4

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 08:04:12 -07:00
parent a039c93f57
commit 94ddfe1064
8 changed files with 312 additions and 28 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Document workflow-authoring tools in the packaged Fusion skill.
category: fix
dev: Syncs workflow extension registrations into generated skill references and capability tables.

View File

@@ -28,6 +28,7 @@ Mission → Milestone → Slice → Feature → Task
**Tool categories:**
<!-- BEGIN: tool-categories (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->
- **Task tools** — `fn_task_create`, `fn_task_update`, `fn_task_list`, `fn_task_show`, `fn_task_attach`, `fn_task_pause`, `fn_task_unpause`, `fn_task_retry`, `fn_task_duplicate`, `fn_task_refine`, `fn_task_archive`, `fn_task_unarchive`, `fn_task_delete`, `fn_task_plan`
- **Workflow tools** — `fn_workflow_list`, `fn_workflow_get`, `fn_workflow_create`, `fn_workflow_update`, `fn_workflow_delete`, `fn_workflow_settings`, `fn_trait_list`, `fn_workflow_select`
- **GitHub tools** — `fn_task_import_github`, `fn_task_import_github_issue`, `fn_task_browse_github_issues`
- **Mission tools** — `fn_mission_create`, `fn_mission_list`, `fn_mission_show`, `fn_mission_list_goals`, `fn_mission_link_goal`, `fn_mission_unlink_goal`, `fn_mission_backfill_assertions`, `fn_mission_delete`, `fn_mission_update`, `fn_milestone_add`, `fn_slice_add`, `fn_feature_add`, `fn_feature_delete`, `fn_slice_delete`, `fn_milestone_delete`, `fn_slice_activate`, `fn_feature_link_task`, `fn_feature_update`, `fn_milestone_update`
- **Goal tools** — `fn_goal_list`, `fn_goal_create`, `fn_goal_archive`, `fn_goal_show`

View File

@@ -1,11 +1,11 @@
# Engine Session-Scoped Tools
These tools are **not** part of the user-invokable extension surface. They are injected by the engine at runtime for specific agent session types.
This reference documents tools injected by the engine at runtime for specific agent session types. Some shared workflow tools are also part of the public extension surface; use `references/extension-tools.md` as the canonical user-invokable extension reference and this page for runtime agent-role availability.
- Source files: `packages/engine/src/agent-tools.ts`, `triage.ts`, `executor.ts`, `merger.ts`, `agent-heartbeat.ts`
- Availability: only when the engine creates a session for the matching agent role
- Runtime contract: engine sessions now forward requested skill names (`skillSelection.requestedSkillNames`) into the generic runtime `skills` field so non-pi runtimes can still receive Fusion skill intent.
- Important: do not tell users to call these directly from the generic extension tool list
- Important: do not tell users to call runtime-only tools directly from the generic extension tool list
## Shared runtime tools (`agent-tools.ts`)

View File

@@ -18,7 +18,7 @@ Create a new task on the Fusion task board. The task enters the planning column
| `depends` | array | — | Task IDs this depends on (e.g. ['FN-001', 'FN-002']) |
| `agentId` | string | — | Agent ID to assign this task to (e.g. 'agent-abc123') |
| `priority` | string(enum) | — | Task priority (low, normal, high, urgent) |
| `workflow_id` | string | — | Workflow ID to select for the new task (e.g. 'WF-003' or 'builtin:coding'). |
| `workflow_id` | string | — | Workflow ID to select for the new task (e.g. 'WF-003' or 'builtin:coding'). Omit to inherit the project default workflow. Use fn_workflow_list to discover valid IDs. |
### fn_task_update
@@ -33,7 +33,7 @@ Update fields on an existing task. Supports modifying the title, description, de
| `agentId` | union | — | Agent ID to assign this task to, or null to clear (e.g. 'agent-abc123') |
| `nodeId` | union | — | Node ID override for this task, or null to clear |
| `priority` | string(enum) | — | Task priority (low, normal, high, urgent) |
| `workflow_id` | union | — | Workflow ID to select for this task (e.g. 'WF-003' or 'builtin:coding'), |
| `workflow_id` | union | — | Workflow ID to select for this task (e.g. 'WF-003' or 'builtin:coding'), or null to clear the workflow selection and revert to the project default. Use fn_workflow_list to discover valid IDs. |
### fn_task_list
@@ -136,6 +136,81 @@ Create a task via AI-guided planning mode — interactive conversation to refine
| `description` | string | — | Initial plan description (optional) — the AI will ask clarifying questions if not provided |
| `baseBranch` | string | — | Optional base branch for the task created from this planning session |
## Workflow Tools
### fn_workflow_list
List built-in and custom Fusion workflow definitions available in this project.
No parameters.
### fn_workflow_get
Fetch a Fusion workflow definition by ID, including its resolved workflow IR.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `workflow_id` | string | ✓ | The workflow definition ID to fetch (e.g. 'WF-003', or a 'builtin:*' id). Use fn_workflow_list to discover available IDs. |
### fn_workflow_create
Create a custom Fusion workflow definition from a validated workflow IR.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | ✓ | Workflow name (required, non-empty). |
| `description` | string | — | Optional human-readable description. |
| `ir` | unknown | ✓ | Workflow graph (intermediate representation). Validated server-side; a malformed graph is rejected. |
| `layout` | record | — | Optional node layout map keyed by node id. |
| `confirm_policy_escalation` | boolean | — | Set true to confirm binding a column to an agent whose permission policy is broader (more privileged) than the project default. Required when such a binding is present; the create is otherwise rejected naming the offending column. |
### fn_workflow_update
Update a custom Fusion workflow definition's metadata, IR, or layout.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `workflow_id` | string | ✓ | The workflow definition ID to update (built-ins cannot be edited). |
| `name` | string | — | New name. |
| `description` | string | — | New description. |
| `ir` | unknown | — | Replacement workflow graph (validated server-side). |
| `layout` | record | — | Replacement node layout map. |
| `rehome_to` | string | — | 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. |
| `confirm_policy_escalation` | boolean | — | Set true to confirm binding a column to an agent whose permission policy is broader (more privileged) than the project default. Required when such a binding is present; the update is otherwise rejected naming the offending column. |
### fn_workflow_delete
Delete a custom Fusion workflow definition; built-in workflows are protected.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `workflow_id` | string | ✓ | The workflow definition ID to delete (built-ins cannot be deleted). |
### fn_workflow_settings
Read or write per-project values for a workflow's declared settings.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `action` | union | ✓ | `get` reads the stored setting VALUES plus the engine-effective values for the workflow; `set` writes values (requires `values`). |
| `workflow_id` | string | ✓ | 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` | record | — | 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. |
### fn_trait_list
List column traits available when authoring Fusion workflow IR columns.
No parameters.
### fn_workflow_select
Assign a workflow definition to a task by workflow ID.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `workflow_id` | string | ✓ | The workflow definition ID to select (e.g. 'WF-003', or a 'builtin:*' id). Use fn_workflow_list to discover available IDs. |
| `task_id` | string | — | Task to assign the workflow to. Defaults to the current task. |
## GitHub Tools
### fn_task_import_github
@@ -456,8 +531,8 @@ Create a new task and assign it to a specific agent for execution. The task goes
|-----------|------|----------|-------------|
| `agent_id` | string | ✓ | The agent ID to delegate work to |
| `description` | string | ✓ | What needs to be done |
| `dependencies` | array | — | Task IDs this new task depends on (e.g. [\"KB-001\"] |
| `workflow_id` | string | — | Workflow ID to select for the new task (e.g. 'WF-003' or 'builtin:coding'). |
| `dependencies` | array | — | Task IDs this new task depends on (e.g. ["KB-001"] |
| `workflow_id` | string | — | Workflow ID to select for the new task (e.g. 'WF-003' or 'builtin:coding'). Omit to inherit the project default workflow. Use fn_workflow_list to discover valid IDs. |
| `override` | boolean | — | Set true to bypass executor-role assignment policy |
### fn_agent_show

View File

@@ -12,6 +12,14 @@ All skill/extension tool invocations in this catalog use the public `fn_*` names
<!-- BEGIN: fusion-capabilities-tool-table (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->
| Tool | Purpose |
|------|---------|
| `fn_workflow_list` | List built-in and custom Fusion workflow definitions available in this project. |
| `fn_workflow_get` | Fetch a Fusion workflow definition by ID, including its resolved workflow IR. |
| `fn_workflow_create` | Create a custom Fusion workflow definition from a validated workflow IR. |
| `fn_workflow_update` | Update a custom Fusion workflow definition's metadata, IR, or layout. |
| `fn_workflow_delete` | Delete a custom Fusion workflow definition; built-in workflows are protected. |
| `fn_workflow_settings` | Read or write per-project values for a workflow's declared settings. |
| `fn_trait_list` | List column traits available when authoring Fusion workflow IR columns. |
| `fn_workflow_select` | Assign a workflow definition to a task by workflow ID. |
| `fn_task_create` | Create a new task on the Fusion task board. The task enters the planning column where the AI planning agent will plan it into a full prompt with steps, file scope, and acceptance criteria. Optionally pass workflow_id to select a workflow at creation time; use fn_workflow_list to discover valid IDs. |
| `fn_task_update` | Update fields on an existing task. Supports modifying the title, description, dependencies, assigned agent, priority, and workflow_id after task creation. Set workflow_id to a workflow ID to select it, or null to clear the workflow selection. |
| `fn_task_list` | List all tasks on the Fusion board, grouped by column. |

View File

@@ -9,6 +9,7 @@ const cliRoot = resolve(__dirname, "../..");
const repoRoot = resolve(cliRoot, "../..");
const skillDir = resolve(cliRoot, "skill/fusion");
const extensionPath = resolve(cliRoot, "src/extension.ts");
const engineAgentToolsPath = resolve(repoRoot, "packages/engine/src/agent-tools.ts");
const EXT_TOOLS_BEGIN =
"<!-- BEGIN: extension-tools (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
@@ -186,6 +187,36 @@ function splitTopLevelProperties(objectBody: string): string[] {
return props;
}
function getWorkflowExtensionSpecBlocks(): Array<{ name: string; paramsRef: string }> {
const src = readFileSync(extensionPath, "utf-8");
const specStart = src.indexOf("const workflowExtensionToolSpecs");
if (specStart === -1) return [];
const specEnd = src.indexOf("// ── Extension entry point", specStart);
const body = src.slice(specStart, specEnd === -1 ? undefined : specEnd);
const blocks: Array<{ name: string; paramsRef: string }> = [];
for (const match of body.matchAll(/\{[\s\S]*?name:\s*"(fn_[a-z_]+)"[\s\S]*?parameters:\s*([A-Za-z_][A-Za-z0-9_]*)[\s\S]*?\}/g)) {
blocks.push({ name: match[1], paramsRef: match[2] });
}
return blocks;
}
function getExportedTypeObjectParamNames(exportName: string): string[] {
const src = readFileSync(engineAgentToolsPath, "utf-8");
const exportStart = src.indexOf(`export const ${exportName} = Type.Object(`);
if (exportStart === -1) return [];
const braceStart = src.indexOf("{", exportStart);
if (braceStart === -1) return [];
const braceEnd = findMatchingBrace(src, braceStart);
const body = src.slice(braceStart + 1, braceEnd);
return splitTopLevelProperties(body)
.map((prop) => prop.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:/)?.[1])
.filter((value): value is string => Boolean(value))
.sort();
}
function getRegisterToolBlocks(): Array<{ name: string; block: string }> {
const src = readFileSync(extensionPath, "utf-8");
const blocks: Array<{ name: string; block: string }> = [];
@@ -213,13 +244,17 @@ function getRegisterToolBlocks(): Array<{ name: string; block: string }> {
* from the extension source code.
*/
function getExtensionToolNames(): string[] {
return getRegisterToolBlocks()
.map((entry) => entry.name)
.sort();
return [
...getWorkflowExtensionSpecBlocks().map((entry) => entry.name),
...getRegisterToolBlocks().map((entry) => entry.name),
].sort();
}
function getExtensionToolParamNames(): Map<string, string[]> {
const result = new Map<string, string[]>();
for (const { name, paramsRef } of getWorkflowExtensionSpecBlocks()) {
result.set(name, getExportedTypeObjectParamNames(paramsRef));
}
for (const { name, block } of getRegisterToolBlocks()) {
const paramsStart = block.indexOf("parameters:");
if (paramsStart === -1) {
@@ -383,6 +418,15 @@ describe("Skill-Extension Sync", () => {
expect(missingFromSkill).toEqual([]);
});
it("SKILL.md documents workflow authoring tools in a dedicated category", () => {
const skillMd = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8");
const workflowCategory = skillMd.match(/- \*\*Workflow tools\*\* — ([^\n]+)/)?.[1] ?? "";
expect(workflowCategory).toContain("`fn_workflow_create`");
expect(workflowCategory).toContain("`fn_workflow_update`");
expect(workflowCategory).toContain("`fn_trait_list`");
});
it("fusion-capabilities.md tool table includes all registered tools", () => {
const extensionTools = getExtensionToolNames();
const capTools = getCapabilitiesToolNames();
@@ -406,6 +450,16 @@ describe("Skill-Extension Sync", () => {
expect(doc).toContain(EXT_TOOLS_END);
});
it("extension-tools.md includes workflow tool reference sections", () => {
const doc = readFileSync(resolve(skillDir, "references/extension-tools.md"), "utf-8");
const workflowSection = doc.match(/## Workflow Tools\n([\s\S]*?)\n## GitHub Tools/)?.[1] ?? "";
expect(workflowSection).toContain("### fn_workflow_create");
expect(workflowSection).toContain("### fn_workflow_settings");
expect(workflowSection).toContain("### fn_trait_list");
expect(workflowSection).toContain("| `ir` | unknown | ✓ |");
});
it("engine-tools.md documents all engine session-scoped tools", () => {
const engineTools = getEngineSessionToolNames();
const documented = getDocumentedEngineToolNames();

View File

@@ -47,6 +47,11 @@ function fakeGit(shaByPath, { dirty = [] } = {}) {
const baseShas = Object.fromEntries(SKILL_SYNC_INPUT_PATHS.map((p, i) => [p, `sha${i}`]));
test("skill sync cache watches extension and engine workflow tool sources", () => {
assert.ok(SKILL_SYNC_INPUT_PATHS.includes("packages/cli/src/extension.ts"));
assert.ok(SKILL_SYNC_INPUT_PATHS.includes("packages/engine/src/agent-tools.ts"));
});
test("recordSkillSyncCheckPass then isSkillSyncCheckCached returns true on unchanged inputs", () => {
withRoot((root) => {
const deps = { gitFn: fakeGit(baseShas), readFn: () => Buffer.from("") };

View File

@@ -26,6 +26,7 @@ import {
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, "..");
const extensionPath = resolve(repoRoot, "packages/cli/src/extension.ts");
const engineAgentToolsPath = resolve(repoRoot, "packages/engine/src/agent-tools.ts");
const skillPath = resolve(repoRoot, "packages/cli/skill/fusion/SKILL.md");
const extensionToolsPath = resolve(
repoRoot,
@@ -51,6 +52,7 @@ const capabilitiesPath = resolve(
/** Repo-relative input paths whose content determines the --check result. */
export const SKILL_SYNC_INPUT_PATHS = [
"packages/cli/src/extension.ts",
"packages/engine/src/agent-tools.ts",
"packages/cli/skill/fusion/SKILL.md",
"packages/cli/skill/fusion/references/extension-tools.md",
"packages/cli/skill/fusion/references/fusion-capabilities.md",
@@ -129,9 +131,10 @@ const CAP_TABLE_BEGIN =
"<!-- BEGIN: fusion-capabilities-tool-table (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
const CAP_TABLE_END = "<!-- END: fusion-capabilities-tool-table -->";
const CATEGORY_ORDER = ["Task", "GitHub", "Mission", "Goal", "Agent", "Skills", "Insight", "Other"];
const CATEGORY_ORDER = ["Task", "Workflow", "GitHub", "Mission", "Goal", "Agent", "Skills", "Insight", "Other"];
const CATEGORY_LABELS = {
Task: "Task tools",
Workflow: "Workflow tools",
GitHub: "GitHub tools",
Mission: "Mission tools",
Goal: "Goal tools",
@@ -143,6 +146,7 @@ const CATEGORY_LABELS = {
const CATEGORY_HEADERS = {
Task: "## Task Tools",
Workflow: "## Workflow Tools",
GitHub: "## GitHub Tools",
Mission: "## Mission Tools",
Goal: "## Goal Tools",
@@ -155,6 +159,11 @@ const CATEGORY_HEADERS = {
function categorize(name) {
if (name.includes("github")) return "GitHub";
if (name.startsWith("fn_task_")) return "Task";
/*
* FNXC:SkillSync 2026-06-30-00:00:
* FN-7245 made workflow authoring tools part of the public pi extension surface. Keep fn_workflow_* plus the trait vocabulary grouped as Workflow so generated skill docs teach agents to create, update, inspect, configure, and select workflows instead of hiding those tools under Other.
*/
if (name.startsWith("fn_workflow_") || name === "fn_trait_list") return "Workflow";
if (
name.startsWith("fn_mission_") ||
name.startsWith("fn_milestone_") ||
@@ -189,7 +198,7 @@ function normalizeWhitespace(text) {
return text.replace(/\s+/g, " ").trim();
}
function findMatchingBrace(source, openIndex) {
function findMatchingDelimiter(source, openIndex, openChar, closeChar) {
let depth = 0;
let inSingle = false;
let inDouble = false;
@@ -253,14 +262,22 @@ function findMatchingBrace(source, openIndex) {
continue;
}
if (char === "{") depth++;
if (char === "}") {
if (char === openChar) depth++;
if (char === closeChar) {
depth--;
if (depth === 0) return i;
}
}
throw new Error("Unbalanced braces while parsing extension.ts");
throw new Error(`Unbalanced ${openChar}${closeChar} while parsing extension.ts`);
}
function findMatchingBrace(source, openIndex) {
return findMatchingDelimiter(source, openIndex, "{", "}");
}
function findMatchingBracket(source, openIndex) {
return findMatchingDelimiter(source, openIndex, "[", "]");
}
function splitTopLevelProperties(objectBody) {
@@ -372,7 +389,10 @@ function mapType(raw) {
if (/^Type\.Boolean\(/.test(value)) return "boolean";
if (/^Type\.Array\(/.test(value)) return "array";
if (/^Type\.Union\(/.test(value)) return "union";
if (/^Type\.Record\(/.test(value)) return "record";
if (/^Type\.Unknown\(/.test(value)) return "unknown";
if (/^StringEnum\(/.test(value)) return "string(enum)";
if (/^Type\.Literal\(/.test(value)) return "literal";
if (/^Type\.Null\(/.test(value)) return "null";
return "unknown";
}
@@ -387,8 +407,77 @@ function parseParameters(block) {
const braceStart = block.indexOf("{", objectStart);
if (braceStart === -1) return [];
const braceEnd = findMatchingBrace(block, braceStart);
const body = block.slice(braceStart + 1, braceEnd);
return parseTypeObjectParameterBody(block.slice(braceStart + 1, braceEnd));
}
function slicePropertyExpression(source, propertyName) {
const propertyStart = source.indexOf(`${propertyName}:`);
if (propertyStart === -1) return "";
let start = propertyStart + propertyName.length + 1;
while (/\s/.test(source[start] ?? "")) start++;
let depthParen = 0;
let depthBrace = 0;
let depthBracket = 0;
let inSingle = false;
let inDouble = false;
let inTemplate = false;
let escaped = false;
for (let i = start; i < source.length; i++) {
const ch = source[i];
if (inSingle || inDouble || inTemplate) {
if (escaped) {
escaped = false;
continue;
}
if (ch === "\\") {
escaped = true;
continue;
}
if (inSingle && ch === "'") inSingle = false;
else if (inDouble && ch === '"') inDouble = false;
else if (inTemplate && ch === "`") inTemplate = false;
continue;
}
if (ch === "'") {
inSingle = true;
continue;
}
if (ch === '"') {
inDouble = true;
continue;
}
if (ch === "`") {
inTemplate = true;
continue;
}
if (ch === "(") depthParen++;
else if (ch === ")") depthParen--;
else if (ch === "{") depthBrace++;
else if (ch === "}") depthBrace--;
else if (ch === "[") depthBracket++;
else if (ch === "]") depthBracket--;
else if (ch === "," && depthParen === 0 && depthBrace === 0 && depthBracket === 0) {
return source.slice(start, i);
}
}
return source.slice(start);
}
function extractParameterDescription(rawValue) {
const descriptionExpression = slicePropertyExpression(rawValue, "description");
const concatenated = normalizeWhitespace(parseStringLiterals(descriptionExpression).join(" "));
if (concatenated) return concatenated;
const inlineDescription = rawValue.match(/description:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
return inlineDescription ? normalizeWhitespace(inlineDescription[1].replace(/\\n/g, " ")) : "";
}
function parseTypeObjectParameterBody(body) {
const params = [];
for (const prop of splitTopLevelProperties(body)) {
const match = prop.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([\s\S]+)$/);
@@ -399,24 +488,65 @@ function parseParameters(block) {
? rawValue.trim().replace(/^Type\.Optional\(/, "").replace(/\)\s*$/, "")
: rawValue.trim();
const descMatch = rawValue.match(/description:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
const description = descMatch
? normalizeWhitespace(descMatch[1].replace(/\\n/g, " "))
: "";
params.push({
name,
type: mapType(inner),
required: !optional,
description,
description: extractParameterDescription(rawValue),
});
}
return params;
}
function extractTools(source) {
function getExportedTypeObjectParameters(source, exportName) {
const exportStart = source.indexOf(`export const ${exportName} = Type.Object(`);
if (exportStart === -1) return [];
const braceStart = source.indexOf("{", exportStart);
if (braceStart === -1) return [];
const braceEnd = findMatchingBrace(source, braceStart);
return parseTypeObjectParameterBody(source.slice(braceStart + 1, braceEnd));
}
function extractWorkflowExtensionSpecTools(source, engineSource) {
const specStart = source.indexOf("const workflowExtensionToolSpecs");
if (specStart === -1) return [];
const equalsStart = source.indexOf("=", specStart);
const arrayStart = source.indexOf("[", equalsStart);
if (arrayStart === -1) return [];
const arrayEnd = findMatchingBracket(source, arrayStart);
const body = source.slice(arrayStart + 1, arrayEnd);
const tools = [];
let fromIndex = 0;
while (true) {
const objectStart = body.indexOf("{", fromIndex);
if (objectStart === -1) break;
const objectEnd = findMatchingBrace(body, objectStart);
const block = body.slice(objectStart, objectEnd + 1);
const nameMatch = block.match(/name:\s*"(fn_[a-z_]+)"/);
if (nameMatch) {
const labelMatch = block.match(/label:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
const descriptionMatch = block.match(/description:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
const paramsRefMatch = block.match(/parameters:\s*([A-Za-z_][A-Za-z0-9_]*)/);
tools.push({
name: nameMatch[1],
label: labelMatch ? labelMatch[1] : "",
description: descriptionMatch ? normalizeWhitespace(descriptionMatch[1]) : "",
parameters: paramsRefMatch
? getExportedTypeObjectParameters(engineSource, paramsRefMatch[1])
: [],
});
}
fromIndex = objectEnd + 1;
}
return tools;
}
function extractTools(source, engineSource = "") {
const tools = extractWorkflowExtensionSpecTools(source, engineSource);
const seen = new Set(tools.map((tool) => tool.name));
const registerToken = "pi.registerTool(";
let fromIndex = 0;
@@ -430,11 +560,14 @@ function extractTools(source) {
const nameMatch = block.match(/name:\s*"(fn_[a-z_]+)"/);
if (nameMatch) {
const name = nameMatch[1];
const labelMatch = block.match(/label:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
const label = labelMatch ? labelMatch[1] : "";
const description = extractDescription(block);
const parameters = parseParameters(block);
tools.push({ name, label, description, parameters });
if (!seen.has(name)) {
const labelMatch = block.match(/label:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/);
const label = labelMatch ? labelMatch[1] : "";
const description = extractDescription(block);
const parameters = parseParameters(block);
tools.push({ name, label, description, parameters });
seen.add(name);
}
}
fromIndex = braceEnd + 1;
@@ -538,7 +671,8 @@ function main() {
const checkOnly = process.argv.includes("--check");
const extensionSource = readFileSync(extensionPath, "utf-8");
const tools = extractTools(extensionSource);
const engineSource = readFileSync(engineAgentToolsPath, "utf-8");
const tools = extractTools(extensionSource, engineSource);
const files = [
{