pi-extensible-workflows 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +18 -11
  2. package/dist/src/agent-execution.d.ts +4 -94
  3. package/dist/src/agent-execution.js +34 -208
  4. package/dist/src/budget.d.ts +38 -0
  5. package/dist/src/budget.js +160 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +60 -8
  8. package/dist/src/doctor-cleanup.d.ts +41 -0
  9. package/dist/src/doctor-cleanup.js +597 -0
  10. package/dist/src/doctor.d.ts +7 -2
  11. package/dist/src/doctor.js +127 -22
  12. package/dist/src/execution.d.ts +17 -0
  13. package/dist/src/execution.js +630 -0
  14. package/dist/src/host.d.ts +101 -0
  15. package/dist/src/host.js +3084 -0
  16. package/dist/src/index.d.ts +13 -634
  17. package/dist/src/index.js +10 -4432
  18. package/dist/src/persistence.d.ts +9 -19
  19. package/dist/src/persistence.js +175 -61
  20. package/dist/src/registry.d.ts +43 -0
  21. package/dist/src/registry.js +279 -0
  22. package/dist/src/session-inspector.js +5 -3
  23. package/dist/src/types.d.ts +692 -0
  24. package/dist/src/types.js +27 -0
  25. package/dist/src/utils.d.ts +32 -0
  26. package/dist/src/utils.js +168 -0
  27. package/dist/src/validation.d.ts +28 -0
  28. package/dist/src/validation.js +832 -0
  29. package/package.json +3 -2
  30. package/skills/pi-extensible-workflows/SKILL.md +69 -34
  31. package/src/agent-execution.ts +37 -189
  32. package/src/budget.ts +75 -0
  33. package/src/cli.ts +47 -7
  34. package/src/doctor-cleanup.ts +337 -0
  35. package/src/doctor.ts +117 -24
  36. package/src/execution.ts +540 -0
  37. package/src/host.ts +2598 -0
  38. package/src/index.ts +14 -3856
  39. package/src/persistence.ts +122 -37
  40. package/src/registry.ts +240 -0
  41. package/src/session-inspector.ts +5 -3
  42. package/src/types.ts +158 -0
  43. package/src/utils.ts +142 -0
  44. package/src/validation.ts +688 -0
@@ -0,0 +1,279 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { Value } from "typebox/value";
4
+ import { deepFreeze, fail, jsonValue, object } from "./utils.js";
5
+ import { loadSettings, resolveWorkflowSettings, validateSchema } from "./validation.js";
6
+ const RESERVED_GLOBALS = new Set(["agent", "shell", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
7
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
8
+ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
9
+ function normalizeRoleDirectory(value) {
10
+ try {
11
+ if (value instanceof URL) {
12
+ if (value.protocol !== "file:")
13
+ fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
14
+ return fileURLToPath(value);
15
+ }
16
+ if (typeof value === "string") {
17
+ if (value.startsWith("file:"))
18
+ return fileURLToPath(value);
19
+ if (isAbsolute(value))
20
+ return value;
21
+ }
22
+ }
23
+ catch (error) {
24
+ fail("INVALID_METADATA", `Invalid workflow role directory: ${error instanceof Error ? error.message : String(error)}`);
25
+ }
26
+ fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
27
+ }
28
+ export class WorkflowRegistry {
29
+ #extensions = new Set();
30
+ #globals = new Map();
31
+ #hooks = new Map();
32
+ #roleDirectories = new Map();
33
+ #modelAliases = new Map();
34
+ #frozen = false;
35
+ get frozen() { return this.#frozen; }
36
+ freeze() { this.#frozen = true; }
37
+ register(extension) {
38
+ if (this.#frozen)
39
+ fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
40
+ if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows"))
41
+ fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
42
+ if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "modelAliases", "agentSetupHooks", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
43
+ fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
44
+ const functions = extension.functions ?? {};
45
+ const variables = extension.variables ?? {};
46
+ const modelAliases = extension.modelAliases ?? {};
47
+ const agentSetupHooks = extension.agentSetupHooks ?? {};
48
+ const roleDirectoryValues = extension.roleDirectories === undefined ? [] : extension.roleDirectories;
49
+ if (!Array.isArray(roleDirectoryValues))
50
+ fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
51
+ const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
52
+ if (!object(functions) || !object(variables) || !object(modelAliases) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && roleDirectories.length === 0))
53
+ fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, or role directories");
54
+ const names = [...Object.keys(functions), ...Object.keys(variables)];
55
+ if (new Set(names).size !== names.length)
56
+ fail("GLOBAL_COLLISION", "Global name collision inside extension");
57
+ for (const name of names) {
58
+ if (!IDENTIFIER.test(name) || name.startsWith("__pi_extensible_workflows_"))
59
+ fail("INVALID_METADATA", `Invalid global name: ${name}`);
60
+ if (RESERVED_GLOBALS.has(name))
61
+ fail("GLOBAL_COLLISION", `Global name is reserved: ${name}`);
62
+ if (this.#globals.has(name))
63
+ fail("GLOBAL_COLLISION", `Global name is already registered: ${name}`);
64
+ }
65
+ for (const [name, fn] of Object.entries(functions)) {
66
+ if (!object(fn) || Object.keys(fn).some((key) => !["description", "input", "output", "run"].includes(key)) || typeof fn.description !== "string" || !fn.description.trim() || typeof fn.run !== "function")
67
+ fail("INVALID_METADATA", `Invalid workflow function: ${name}`);
68
+ validateSchema(fn.input, `${name} input`);
69
+ validateSchema(fn.output, `${name} output`);
70
+ if (fn.input.type !== "object")
71
+ fail("INVALID_SCHEMA", `${name} input must describe one object`);
72
+ }
73
+ for (const [name, variable] of Object.entries(variables)) {
74
+ if (!object(variable) || Object.keys(variable).some((key) => !["description", "schema", "resolve"].includes(key)) || typeof variable.description !== "string" || !variable.description.trim() || typeof variable.resolve !== "function")
75
+ fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
76
+ validateSchema(variable.schema, `${name} schema`);
77
+ }
78
+ for (const [name, alias] of Object.entries(modelAliases)) {
79
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name))
80
+ fail("INVALID_METADATA", `Invalid model alias name: ${name}`);
81
+ if (!object(alias) || Object.keys(alias).some((key) => key !== "resolve") || typeof alias.resolve !== "function")
82
+ fail("INVALID_METADATA", `Invalid model alias resolver: ${name}`);
83
+ if (this.#modelAliases.has(name))
84
+ fail("DUPLICATE_NAME", `Model alias already registered: ${name}`);
85
+ }
86
+ for (const [name, hook] of Object.entries(agentSetupHooks)) {
87
+ if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority)))
88
+ fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
89
+ if (this.#hooks.has(name))
90
+ fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
91
+ }
92
+ const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, ...(roleDirectories.length ? { roleDirectories } : {}) });
93
+ this.#extensions.add(stored);
94
+ for (const directory of roleDirectories)
95
+ if (!this.#roleDirectories.has(directory))
96
+ this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension.version, headline: extension.headline, description: extension.description } }));
97
+ for (const name of names)
98
+ this.#globals.set(name, name);
99
+ for (const [name, alias] of Object.entries(modelAliases))
100
+ this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, resolve: alias.resolve });
101
+ for (const [name, hook] of Object.entries(agentSetupHooks))
102
+ this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
103
+ }
104
+ function(name) {
105
+ if (!IDENTIFIER.test(name))
106
+ fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
107
+ const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
108
+ if (!fn)
109
+ fail("MISSING_WORKFLOW", `Registered function is unavailable: ${name}; the separate registered-workflow format was removed`);
110
+ return fn;
111
+ }
112
+ functions() {
113
+ return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
114
+ }
115
+ catalog(context) {
116
+ const functions = [];
117
+ const variables = [];
118
+ for (const extension of this.#extensions) {
119
+ for (const [name, fn] of Object.entries(extension.functions ?? {}))
120
+ functions.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: fn.description, input: structuredClone(fn.input), output: structuredClone(fn.output) });
121
+ for (const [name, variable] of Object.entries(extension.variables ?? {}))
122
+ variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
123
+ }
124
+ let aliases;
125
+ let settings;
126
+ let source = "global settings";
127
+ try {
128
+ const resolved = context ? resolveWorkflowSettings(context.cwd, context.projectTrusted, context.globalSettingsPath) : undefined;
129
+ aliases = resolved?.effective.modelAliases ?? loadSettings().modelAliases;
130
+ if (resolved) {
131
+ source = resolved.projectTrusted && resolved.sources.modelAliases === resolved.projectSettingsPath ? "trusted project settings" : "global settings";
132
+ settings = { concurrency: resolved.effective.concurrency, modelAliases: resolved.effective.modelAliases ?? {}, disabledAgentResources: resolved.effective.disabledAgentResources ?? { skills: [], extensions: [] }, globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted: resolved.projectTrusted, sources: resolved.sources };
133
+ }
134
+ }
135
+ catch {
136
+ aliases = undefined;
137
+ }
138
+ const staticEntries = Object.keys(aliases ?? {}).map((name) => ({ name, kind: "static", provenance: source }));
139
+ const dynamicEntries = [...this.#modelAliases.values()].map(({ name, version, headline, extensionDescription }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline, extensionDescription }));
140
+ const modelAliasEntries = [...staticEntries, ...dynamicEntries].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind) || left.provenance.localeCompare(right.provenance));
141
+ const sort = (left, right) => left.name.localeCompare(right.name);
142
+ const catalog = { functions: functions.sort(sort), variables: variables.sort(sort), ...(modelAliasEntries.length ? { modelAliasEntries } : {}) };
143
+ if (aliases && Object.keys(aliases).length)
144
+ Object.defineProperty(catalog, "modelAliases", { value: Object.freeze(structuredClone(aliases)), enumerable: false });
145
+ if (settings) {
146
+ const publicSettings = { ...settings, modelAliases: Object.keys(aliases ?? {}).length ? {} : settings.modelAliases };
147
+ Object.defineProperty(catalog, "settings", { value: deepFreeze(publicSettings), enumerable: true });
148
+ }
149
+ return deepFreeze(catalog);
150
+ }
151
+ catalogIndex(context) {
152
+ const catalog = this.catalog(context);
153
+ const index = {
154
+ functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
155
+ variables: catalog.variables.map(({ name, description, schema }) => ({ name, description, schema: structuredClone(schema) })),
156
+ ...(catalog.modelAliasEntries ? { modelAliasEntries: structuredClone(catalog.modelAliasEntries) } : {}),
157
+ };
158
+ if (catalog.modelAliases)
159
+ Object.defineProperty(index, "modelAliases", { value: Object.freeze(structuredClone(catalog.modelAliases)), enumerable: false });
160
+ if (catalog.settings)
161
+ Object.defineProperty(index, "settings", { value: deepFreeze(structuredClone(catalog.settings)), enumerable: true });
162
+ return deepFreeze(index);
163
+ }
164
+ catalogDetail(name, context) {
165
+ const catalog = this.catalog(context);
166
+ const entry = catalog.functions.find((candidate) => candidate.name === name) ?? catalog.variables.find((candidate) => candidate.name === name);
167
+ if (entry)
168
+ return entry;
169
+ const alias = catalog.modelAliasEntries?.find((candidate) => candidate.name === name);
170
+ if (alias)
171
+ return alias;
172
+ return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function or variable is available: ${name}` } });
173
+ }
174
+ globals() {
175
+ return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.keys(extension.functions ?? {}).map((name) => [name, { name }]))));
176
+ }
177
+ async invokeFunction(name, input, context, path, journal) {
178
+ const fn = this.function(name);
179
+ if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input))
180
+ fail("RESULT_INVALID", `Invalid input for ${name}`);
181
+ const replayed = journal.get(path);
182
+ if (replayed !== undefined) {
183
+ if (!jsonValue(replayed) || !Value.Check(fn.output, replayed))
184
+ fail("RESULT_INVALID", `Invalid replay for ${name}`);
185
+ return structuredClone(replayed);
186
+ }
187
+ const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, invoke: context.invoke, agent: context.agent, shell: context.shell, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
188
+ if (!jsonValue(result) || !Value.Check(fn.output, result))
189
+ fail("RESULT_INVALID", `Invalid output from ${name}`);
190
+ const stored = structuredClone(result);
191
+ journal.put(path, stored);
192
+ return structuredClone(stored);
193
+ }
194
+ variables() {
195
+ return [...this.#extensions].flatMap((extension) => Object.entries(extension.variables ?? {}).map(([name, variable]) => ({ name, variable })));
196
+ }
197
+ agentSetupHooks() {
198
+ return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
199
+ }
200
+ roleDirectories() {
201
+ return [...this.#roleDirectories.keys()];
202
+ }
203
+ roleDirectoryRegistrations() {
204
+ return [...this.#roleDirectories.values()];
205
+ }
206
+ modelAliases() { return [...this.#modelAliases.values()].sort((left, right) => left.name.localeCompare(right.name)); }
207
+ async resolveModelAliases(context, shadowed = new Set()) {
208
+ const resolved = {};
209
+ const isAborted = () => context.signal.aborted;
210
+ for (const alias of this.modelAliases()) {
211
+ if (shadowed.has(alias.name))
212
+ continue;
213
+ if (isAborted())
214
+ fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
215
+ let target;
216
+ try {
217
+ target = await alias.resolve(Object.freeze({ cwd: context.cwd, projectTrusted: context.projectTrusted, rootModel: Object.freeze({ ...context.rootModel }), knownModels: new Set(context.knownModels), availableModels: new Set(context.availableModels), signal: context.signal }));
218
+ }
219
+ catch (error) {
220
+ if (isAborted() || error instanceof Error && error.name === "AbortError" || typeof error === "object" && error !== null && !Array.isArray(error) && error.code === "CANCELLED")
221
+ fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
222
+ fail("CONFIG_ERROR", `Model alias resolver failed for ${alias.name} (${alias.headline}): ${error instanceof Error ? error.message : String(error)}`);
223
+ }
224
+ if (isAborted())
225
+ fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
226
+ if (typeof target !== "string" || !target.trim())
227
+ fail("CONFIG_ERROR", `Model alias resolver returned an invalid target for ${alias.name} (${alias.headline})`);
228
+ resolved[alias.name] = target.trim();
229
+ }
230
+ return Object.freeze(resolved);
231
+ }
232
+ }
233
+ const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
234
+ const globalRegistry = globalThis;
235
+ function createWorkflowRegistryApi(registry) {
236
+ return {
237
+ get frozen() { return registry.frozen; },
238
+ freeze: () => { registry.freeze(); },
239
+ register: (extension) => { registry.register(extension); },
240
+ function: (name) => registry.function(name),
241
+ functions: () => registry.functions(),
242
+ catalog: (context) => registry.catalog(context),
243
+ catalogIndex: (context) => registry.catalogIndex(context),
244
+ catalogDetail: (name, context) => registry.catalogDetail(name, context),
245
+ globals: () => registry.globals(),
246
+ invokeFunction: (...args) => registry.invokeFunction(...args),
247
+ variables: () => registry.variables(),
248
+ modelAliases: () => registry.modelAliases(),
249
+ resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
250
+ roleDirectories: () => registry.roleDirectories(),
251
+ roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
252
+ agentSetupHooks: () => registry.agentSetupHooks(),
253
+ };
254
+ }
255
+ function workflowRegistryHost() {
256
+ return globalRegistry[WORKFLOW_REGISTRY_KEY] ??= { api: createWorkflowRegistryApi(new WorkflowRegistry()) };
257
+ }
258
+ export function resetWorkflowRegistry() {
259
+ workflowRegistryHost().api = createWorkflowRegistryApi(new WorkflowRegistry());
260
+ }
261
+ export function beginWorkflowExtensionLoading() {
262
+ if (workflowRegistryHost().api.frozen)
263
+ resetWorkflowRegistry();
264
+ }
265
+ export function loadingRegistry() { return workflowRegistryHost().api; }
266
+ beginWorkflowExtensionLoading();
267
+ export function registerWorkflowExtension(extension) { loadingRegistry().register(extension); }
268
+ export function workflowCatalog(context) { return loadingRegistry().catalog(context); }
269
+ export function workflowCatalogIndex(context) { return loadingRegistry().catalogIndex(context); }
270
+ export function workflowCatalogDetail(name, context) { return loadingRegistry().catalogDetail(name, context); }
271
+ export function registeredWorkflowFunctions() { return loadingRegistry().functions(); }
272
+ export function registeredWorkflowRoleDirectories() {
273
+ const directories = loadingRegistry().roleDirectories;
274
+ return typeof directories === "function" ? directories() : [];
275
+ }
276
+ export function registeredWorkflowRoleDirectoryRegistrations() {
277
+ const registrations = loadingRegistry().roleDirectoryRegistrations;
278
+ return typeof registrations === "function" ? registrations() : [];
279
+ }
@@ -223,7 +223,7 @@ export async function loadSessionReport(path, home = homedir()) {
223
223
  const args = call.arguments;
224
224
  const agents = loaded ? await Promise.all(loaded.run.agents.map(agentReport)) : [];
225
225
  const models = mergedModels(agents.flatMap(({ attempts }) => attempts.map(({ models: attemptModels }) => attemptModels)));
226
- const name = typeof args.name === "string" ? args.name : typeof args.workflow === "string" ? args.workflow : loaded?.run.workflowName ?? "workflow";
226
+ const name = loaded?.run.workflowName ?? (typeof args.workflow === "string" ? args.workflow : typeof args.name === "string" ? args.name : "workflow");
227
227
  const description = typeof args.description === "string" ? args.description : loaded?.snapshot.metadata.description;
228
228
  const script = typeof args.script === "string" && args.script.trim() ? args.script : loaded?.snapshot.script;
229
229
  let staticCalls = [];
@@ -296,8 +296,10 @@ function detailLines(workflow) {
296
296
  `Hooks: ${attempt.setup.hookNames.join(", ") || "(none)"}`,
297
297
  `Effective: model=${attempt.setup.model.provider}/${attempt.setup.model.model}${attempt.setup.model.thinking ? `:${attempt.setup.model.thinking}` : ""} tools=${attempt.setup.tools.join(",") || "(none)"} cwd=${attempt.setup.cwd}`,
298
298
  ...(attempt.setup.disabledAgentResources ? [
299
- `Disabled skills: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
300
- `Disabled extensions: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
299
+ `Configured skill patterns: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
300
+ `Excluded skills: ${attempt.setup.disabledAgentResources.excludedSkills?.join(", ") || "(none)"}`,
301
+ `Configured extension patterns: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
302
+ `Excluded extensions: ${attempt.setup.disabledAgentResources.excludedExtensions?.join(", ") || "(none)"}`,
301
303
  `Unmatched skills: ${attempt.setup.disabledAgentResources.unmatchedSkills.join(", ") || "(none)"}`,
302
304
  `Unmatched extensions: ${attempt.setup.disabledAgentResources.unmatchedExtensions.join(", ") || "(none)"}`,
303
305
  ] : []),