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:
7
.changeset/fn-7274-workflow-skill-tools.md
Normal file
7
.changeset/fn-7274-workflow-skill-tools.md
Normal 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.
|
||||
@@ -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`
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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("") };
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user