pi-extensible-workflows 3.1.0 → 3.3.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.
- package/README.md +11 -47
- package/dist/src/agent-execution.d.ts +4 -0
- package/dist/src/agent-execution.js +146 -60
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +146 -21
- package/dist/src/doctor-cleanup.js +4 -2
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.js +13 -4
- package/dist/src/host.d.ts +51 -8
- package/dist/src/host.js +1181 -487
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +3 -1
- package/dist/src/persistence.d.ts +40 -1
- package/dist/src/persistence.js +51 -0
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +36 -2
- package/dist/src/types.d.ts +19 -0
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +42 -2
- package/dist/src/workflow-artifacts.d.ts +13 -0
- package/dist/src/workflow-artifacts.js +39 -0
- package/dist/src/workflow-evals.d.ts +7 -1
- package/dist/src/workflow-evals.js +23 -3
- package/evals/cases/recovery-completed-worktree.yaml +17 -0
- package/evals/cases/recovery-failed-run.yaml +14 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +21 -28
- package/src/agent-execution.ts +102 -30
- package/src/bundles.ts +471 -0
- package/src/cli.ts +118 -21
- package/src/doctor-cleanup.ts +3 -2
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +13 -4
- package/src/host.ts +992 -447
- package/src/index.ts +4 -2
- package/src/persistence.ts +53 -1
- package/src/session-inspector.ts +36 -4
- package/src/types.ts +12 -5
- package/src/validation.ts +33 -2
- package/src/workflow-artifacts.ts +34 -0
- package/src/workflow-evals.ts +24 -3
package/dist/src/cli.js
CHANGED
|
@@ -8,7 +8,8 @@ import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionF
|
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
import { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
|
|
10
10
|
import { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
|
|
11
|
-
import workflowExtension, { formatWorkflowProgress, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "./index.js";
|
|
11
|
+
import workflowExtension, { formatWorkflowProgress, loadAgentDefinitions, registeredWorkflowFunctions, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "./index.js";
|
|
12
|
+
import { portableEngineVersion, portablePiVersion, writePortableWorkflowBundle } from "./bundles.js";
|
|
12
13
|
import { runSessionInspector, transcriptFileLines } from "./session-inspector.js";
|
|
13
14
|
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
14
15
|
function has(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
|
|
@@ -193,7 +194,30 @@ function launcherHelpLines() {
|
|
|
193
194
|
];
|
|
194
195
|
}
|
|
195
196
|
function workflowUsage() { return [`Usage: pi-extensible-workflows run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
196
|
-
function exportUsage() { return [`Usage: pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
197
|
+
function exportUsage() { return [`Usage: pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
198
|
+
function bundleUsage() { return [`Usage: pi-extensible-workflows bundle <workflow-name> [--name <command>] [--output <directory>] [--force]`, "", "The bundle contains a launcher, manifest, workflow payload, and external-runtime setup instructions.", "Repeat --role, --alias, --tool, --command, or --environment to declare recipient requirements.", "Use --extension, --skill, --resource, and --dependency to copy selected payload resources."].join("\n") + "\n"; }
|
|
199
|
+
function parseInspectArgs(rawArgs) {
|
|
200
|
+
let sessionId;
|
|
201
|
+
let mode = "tui";
|
|
202
|
+
let failedOnly = false;
|
|
203
|
+
for (const arg of rawArgs) {
|
|
204
|
+
if (arg === "--json" || arg === "--summary") {
|
|
205
|
+
const next = arg === "--json" ? "json" : "summary";
|
|
206
|
+
if (mode !== "tui" && mode !== next)
|
|
207
|
+
throw new Error("inspect accepts only one output mode");
|
|
208
|
+
mode = next;
|
|
209
|
+
}
|
|
210
|
+
else if (arg === "--failed")
|
|
211
|
+
failedOnly = true;
|
|
212
|
+
else if (arg.startsWith("--"))
|
|
213
|
+
throw new Error(`Unknown inspect option: ${arg}`);
|
|
214
|
+
else if (sessionId !== undefined)
|
|
215
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
216
|
+
else
|
|
217
|
+
sessionId = arg;
|
|
218
|
+
}
|
|
219
|
+
return { ...(sessionId ? { sessionId } : {}), mode: failedOnly && mode === "tui" ? "summary" : mode, failedOnly };
|
|
220
|
+
}
|
|
197
221
|
export function parseDoctorCleanupArgs(rawArgs) {
|
|
198
222
|
let olderThanDays = 90;
|
|
199
223
|
let yes = false;
|
|
@@ -286,7 +310,7 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
|
286
310
|
cwd,
|
|
287
311
|
agentDir,
|
|
288
312
|
settingsManager,
|
|
289
|
-
resourceLoaderOptions: {},
|
|
313
|
+
resourceLoaderOptions: { ...(options.skillPaths?.length ? { additionalSkillPaths: [...options.skillPaths] } : {}) },
|
|
290
314
|
resourceLoaderReloadOptions: { resolveProjectTrust },
|
|
291
315
|
});
|
|
292
316
|
const extensions = services.resourceLoader.getExtensions();
|
|
@@ -303,7 +327,7 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
|
303
327
|
sendMessage() { },
|
|
304
328
|
events: { emit() { } },
|
|
305
329
|
};
|
|
306
|
-
workflowExtension(headlessPi, homedir(), undefined, undefined, agentDir);
|
|
330
|
+
workflowExtension(headlessPi, homedir(), undefined, undefined, agentDir, options.skillPaths);
|
|
307
331
|
const workflowTool = tools.find((tool) => object(tool) && tool.name === "workflow");
|
|
308
332
|
if (!workflowTool)
|
|
309
333
|
throw new Error("The workflow runtime could not be initialized");
|
|
@@ -356,28 +380,33 @@ function terminalProgressStyles(enabled) {
|
|
|
356
380
|
}
|
|
357
381
|
class CliProgress {
|
|
358
382
|
stderr;
|
|
383
|
+
onRunId;
|
|
359
384
|
#lastStable = "";
|
|
360
385
|
#lines = 0;
|
|
361
386
|
#frame = 0;
|
|
362
387
|
#run;
|
|
388
|
+
#runId;
|
|
363
389
|
#timer;
|
|
364
390
|
#interactive;
|
|
365
391
|
#styles;
|
|
366
|
-
constructor(stderr, tty) {
|
|
392
|
+
constructor(stderr, tty, onRunId) {
|
|
367
393
|
this.stderr = stderr;
|
|
394
|
+
this.onRunId = onRunId;
|
|
368
395
|
this.#interactive = tty && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
|
|
369
396
|
this.#styles = terminalProgressStyles(this.#interactive);
|
|
370
397
|
}
|
|
371
398
|
update(run) {
|
|
372
|
-
|
|
399
|
+
if (this.#runId !== run.id) {
|
|
400
|
+
this.#runId = run.id;
|
|
401
|
+
this.onRunId(run.id);
|
|
402
|
+
}
|
|
403
|
+
this.#run = run;
|
|
373
404
|
if (!this.#interactive) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
405
|
+
this.#timer ??= setInterval(() => { this.render(); }, 1000);
|
|
406
|
+
this.#timer.unref();
|
|
407
|
+
this.render();
|
|
378
408
|
return;
|
|
379
409
|
}
|
|
380
|
-
this.#run = run;
|
|
381
410
|
this.#timer ??= setInterval(() => { this.render(); }, 80);
|
|
382
411
|
this.#timer.unref();
|
|
383
412
|
this.render();
|
|
@@ -385,6 +414,14 @@ class CliProgress {
|
|
|
385
414
|
render() {
|
|
386
415
|
if (!this.#run)
|
|
387
416
|
return;
|
|
417
|
+
if (!this.#interactive) {
|
|
418
|
+
const stable = formatWorkflowProgress(this.#run, "◇", this.#styles);
|
|
419
|
+
if (stable !== this.#lastStable) {
|
|
420
|
+
this.#lastStable = stable;
|
|
421
|
+
this.stderr(`${stable}\n`);
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
388
425
|
const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
|
|
389
426
|
const width = process.stderr.columns || 80;
|
|
390
427
|
const text = truncateWorkflowProgress(formatWorkflowProgress(this.#run, spinner, this.#styles), width).join("\n");
|
|
@@ -406,18 +443,24 @@ class CliProgress {
|
|
|
406
443
|
async function invokeWorkflow(fn, args, runtime, options, context) {
|
|
407
444
|
if (!Value.Check(fn.input, args))
|
|
408
445
|
throw new Error(`Invalid input for ${fn.name}`);
|
|
409
|
-
|
|
446
|
+
let announcedRunId;
|
|
447
|
+
const announceRunId = (runId) => { if (announcedRunId === runId)
|
|
448
|
+
return; announcedRunId = runId; options.stderr(`Run ID: ${runId}\n`); };
|
|
449
|
+
const progress = new CliProgress(options.stderr, options.isTTY ?? process.stderr.isTTY, announceRunId);
|
|
410
450
|
try {
|
|
411
451
|
const result = await runtime.workflowTool.execute(randomUUID(), { workflow: fn.name, args, foreground: true }, options.signal, (update) => { if (object(update) && object(update.details) && object(update.details.run))
|
|
412
452
|
progress.update(update.details.run); }, context);
|
|
413
453
|
const details = object(result.details) ? result.details : {};
|
|
454
|
+
const runId = typeof details.runId === "string" ? details.runId : undefined;
|
|
455
|
+
if (runId)
|
|
456
|
+
announceRunId(runId);
|
|
414
457
|
if (has(details, "value"))
|
|
415
|
-
return details.value;
|
|
458
|
+
return { value: details.value, ...(runId ? { runId } : {}) };
|
|
416
459
|
const first = result.content[0];
|
|
417
460
|
if (!first || first.type !== "text")
|
|
418
461
|
throw new Error("Workflow returned no result");
|
|
419
462
|
try {
|
|
420
|
-
return parseJsonInput(first.text);
|
|
463
|
+
return { value: parseJsonInput(first.text), ...(runId ? { runId } : {}) };
|
|
421
464
|
}
|
|
422
465
|
catch {
|
|
423
466
|
throw new Error("Workflow returned invalid JSON");
|
|
@@ -471,14 +514,16 @@ async function runWorkflowCli(rawArgs, options) {
|
|
|
471
514
|
return 0;
|
|
472
515
|
}
|
|
473
516
|
const input = parseWorkflowCliArgs(fn.input, args.slice(1));
|
|
474
|
-
const
|
|
475
|
-
options.write(`${JSON.stringify(value)}\n`);
|
|
517
|
+
const result = await invokeWorkflow(fn, input, runtime, options, context);
|
|
518
|
+
options.write(`${JSON.stringify(result.value)}\n`);
|
|
476
519
|
return 0;
|
|
477
520
|
});
|
|
478
521
|
}
|
|
479
522
|
async function exportWorkflowCli(rawArgs, options) {
|
|
480
523
|
const parsed = stripTrustOptions(rawArgs);
|
|
481
524
|
const args = parsed.args;
|
|
525
|
+
if (args.includes("--bundle"))
|
|
526
|
+
return bundleWorkflowCli(args.filter((arg) => arg !== "--bundle"), options);
|
|
482
527
|
if (!args.length || args[0] === "--help" || args[0] === "-h") {
|
|
483
528
|
options.write(exportUsage());
|
|
484
529
|
return args.length ? 0 : 1;
|
|
@@ -534,6 +579,80 @@ async function exportWorkflowCli(rawArgs, options) {
|
|
|
534
579
|
return 0;
|
|
535
580
|
});
|
|
536
581
|
}
|
|
582
|
+
async function bundleWorkflowCli(rawArgs, options) {
|
|
583
|
+
const parsed = stripTrustOptions(rawArgs);
|
|
584
|
+
const args = parsed.args;
|
|
585
|
+
if (!args.length || args[0] === "--help" || args[0] === "-h") {
|
|
586
|
+
options.write(bundleUsage());
|
|
587
|
+
return args.length ? 0 : 1;
|
|
588
|
+
}
|
|
589
|
+
const workflowName = args[0];
|
|
590
|
+
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
|
|
591
|
+
let name;
|
|
592
|
+
let output;
|
|
593
|
+
let force = false;
|
|
594
|
+
const requirements = { roles: [], aliases: [], tools: [], commands: [], environment: [] };
|
|
595
|
+
const resources = { extensions: [], skills: [], static: [], dependencies: [] };
|
|
596
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
597
|
+
const arg = args[index];
|
|
598
|
+
if (arg === "--force") {
|
|
599
|
+
force = true;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const equals = arg.indexOf("=");
|
|
603
|
+
const option = equals >= 0 ? arg.slice(0, equals) : arg;
|
|
604
|
+
const requirementOptions = { "--role": "roles", "--alias": "aliases", "--tool": "tools", "--command": "commands", "--environment": "environment" };
|
|
605
|
+
const resourceOptions = { "--extension": "extensions", "--skill": "skills", "--resource": "static", "--dependency": "dependencies" };
|
|
606
|
+
if (option === "--name" || option === "--output" || Object.prototype.hasOwnProperty.call(requirementOptions, option) || Object.prototype.hasOwnProperty.call(resourceOptions, option)) {
|
|
607
|
+
const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
|
|
608
|
+
if (!value)
|
|
609
|
+
throw new Error(`Missing value for ${option}`);
|
|
610
|
+
if (option === "--name")
|
|
611
|
+
name = value;
|
|
612
|
+
else if (option === "--output")
|
|
613
|
+
output = value;
|
|
614
|
+
else if (Object.prototype.hasOwnProperty.call(requirementOptions, option))
|
|
615
|
+
requirements[requirementOptions[option]].push(value);
|
|
616
|
+
else
|
|
617
|
+
resources[resourceOptions[option]].push(value);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (arg === "--help" || arg === "-h") {
|
|
621
|
+
options.write(bundleUsage());
|
|
622
|
+
return 0;
|
|
623
|
+
}
|
|
624
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
625
|
+
}
|
|
626
|
+
const fn = runtime.catalog.functions.find((candidate) => candidate.name === workflowName);
|
|
627
|
+
if (!fn)
|
|
628
|
+
throw new Error(`Unknown workflow function: ${workflowName}`);
|
|
629
|
+
const registered = registeredWorkflowFunctions()[workflowName];
|
|
630
|
+
if (!registered)
|
|
631
|
+
throw new Error(`Workflow ${workflowName} is not exportable because its registered implementation is unavailable`);
|
|
632
|
+
const definitions = requirements.roles.length ? loadAgentDefinitions(options.cwd ?? process.cwd(), options.agentDir ?? getAgentDir(), runtime.services.settingsManager.isProjectTrusted()) : {};
|
|
633
|
+
const roles = Object.fromEntries(requirements.roles.map((role) => {
|
|
634
|
+
if (!role || role === "." || role === ".." || role.includes("/") || role.includes("\\"))
|
|
635
|
+
throw new Error(`Invalid role name for bundle: ${role}`);
|
|
636
|
+
const definition = definitions[role];
|
|
637
|
+
if (!definition)
|
|
638
|
+
throw new Error(`Unknown role for bundle: ${role}`);
|
|
639
|
+
return [role, definition];
|
|
640
|
+
}));
|
|
641
|
+
const command = commandName(name ?? kebabCase(workflowName));
|
|
642
|
+
if (!command)
|
|
643
|
+
throw new Error("Command name must be a non-empty name without path separators");
|
|
644
|
+
const destination = output ?? join(homedir(), ".local", "share", "pi-extensible-workflows", "bundles", command);
|
|
645
|
+
const aliasTargets = Object.fromEntries(requirements.aliases.flatMap((name) => {
|
|
646
|
+
const target = runtime.catalog.modelAliases?.[name];
|
|
647
|
+
return typeof target === "string" ? [[name, target]] : [];
|
|
648
|
+
}));
|
|
649
|
+
const selectedResources = Object.values(resources).some((entries) => entries.length) ? resources : undefined;
|
|
650
|
+
writePortableWorkflowBundle({ destination, command, workflow: fn, functionSource: registered.run.toString(), requirements, aliasTargets, roles, ...(selectedResources ? { resources: selectedResources } : {}), piVersion: portablePiVersion(), engineVersion: portableEngineVersion(), force });
|
|
651
|
+
options.write(`Bundled ${workflowName} at ${destination}\n`);
|
|
652
|
+
options.write(`Run ${join(destination, command)} setup before launching the workflow.\n`);
|
|
653
|
+
return 0;
|
|
654
|
+
});
|
|
655
|
+
}
|
|
537
656
|
export async function runCli(args, options = {}, write = (text) => { process.stdout.write(text); }) {
|
|
538
657
|
const stderr = options.stderr ?? ((text) => { process.stderr.write(text); });
|
|
539
658
|
if (args[0] === "doctor" && args.length === 1) {
|
|
@@ -558,9 +677,13 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
558
677
|
return 1;
|
|
559
678
|
}
|
|
560
679
|
}
|
|
561
|
-
if (args[0] === "inspect"
|
|
680
|
+
if (args[0] === "inspect") {
|
|
562
681
|
try {
|
|
563
|
-
|
|
682
|
+
const parsed = parseInspectArgs(args.slice(1));
|
|
683
|
+
if (options.inspect)
|
|
684
|
+
await options.inspect(parsed.sessionId, parsed.mode, parsed.failedOnly);
|
|
685
|
+
else
|
|
686
|
+
await runSessionInspector(parsed.sessionId, parsed.mode, options.cwd ?? process.cwd(), undefined, write, parsed.failedOnly);
|
|
564
687
|
return 0;
|
|
565
688
|
}
|
|
566
689
|
catch (error) {
|
|
@@ -581,9 +704,11 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
581
704
|
return 1;
|
|
582
705
|
}
|
|
583
706
|
}
|
|
584
|
-
if (args[0] === "run" || args[0] === "export") {
|
|
707
|
+
if (args[0] === "bundle" || args[0] === "run" || args[0] === "export") {
|
|
585
708
|
try {
|
|
586
|
-
const workflowOptions = { write, stderr, ...(options.cwd !== undefined ? { cwd: options.cwd } : {}), ...(options.agentDir !== undefined ? { agentDir: options.agentDir } : {}), ...(options.signal ? { signal: options.signal } : {}), ...(options.trustOverride !== undefined ? { trustOverride: options.trustOverride } : {}), ...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}) };
|
|
709
|
+
const workflowOptions = { write, stderr, ...(options.cwd !== undefined ? { cwd: options.cwd } : {}), ...(options.agentDir !== undefined ? { agentDir: options.agentDir } : {}), ...(options.signal ? { signal: options.signal } : {}), ...(options.trustOverride !== undefined ? { trustOverride: options.trustOverride } : {}), ...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}), ...(options.skillPaths?.length ? { skillPaths: [...options.skillPaths] } : {}) };
|
|
710
|
+
if (args[0] === "bundle")
|
|
711
|
+
return await bundleWorkflowCli(args.slice(1), workflowOptions);
|
|
587
712
|
return args[0] === "run" ? await runWorkflowCli(args.slice(1), workflowOptions) : await exportWorkflowCli(args.slice(1), workflowOptions);
|
|
588
713
|
}
|
|
589
714
|
catch (error) {
|
|
@@ -591,7 +716,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
591
716
|
return 1;
|
|
592
717
|
}
|
|
593
718
|
}
|
|
594
|
-
write("Usage: pi-extensible-workflows doctor | inspect [session-id] | transcript <session-file> | run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force]\n");
|
|
719
|
+
write("Usage: pi-extensible-workflows doctor | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
|
|
595
720
|
return 1;
|
|
596
721
|
}
|
|
597
722
|
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
@@ -10,7 +10,7 @@ import { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, Run
|
|
|
10
10
|
const TERMINAL_STATES = new Set(["completed", "failed", "stopped"]);
|
|
11
11
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
12
12
|
const REQUIRED_RUN_FILES = ["workflow.js", "state.json", "snapshot.json", "journal.json", "ownership.json", "worktrees.json", "borrowed-worktrees.json", "system-prompts.json"];
|
|
13
|
-
const OPTIONAL_RUN_FILES = new Set(["result.json"]);
|
|
13
|
+
const OPTIONAL_RUN_FILES = new Set(["result.json", "summary.json"]);
|
|
14
14
|
const RUN_DIRECTORIES = new Set(["worktrees"]);
|
|
15
15
|
const RUN_FILES = new Set([...REQUIRED_RUN_FILES, ...OPTIONAL_RUN_FILES]);
|
|
16
16
|
const AGENT_STATES = new Set(["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"]);
|
|
@@ -65,7 +65,8 @@ function validateScheduledOptions(value, label) { if (!object(value) || typeof v
|
|
|
65
65
|
} }
|
|
66
66
|
function validateAgent(value, label) { if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.name !== "string" || !value.name || typeof value.path !== "string" || !value.path || typeof value.state !== "string" || !AGENT_STATES.has(value.state))
|
|
67
67
|
throw new Error(`${label} is invalid`); optionalString(value.label, `${label}.label`); optionalString(value.parentId, `${label}.parentId`); if (value.structuralPath !== undefined)
|
|
68
|
-
stringList(value.structuralPath, `${label}.structuralPath`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.role, `${label}.role`); optionalString(value.requestedModel, `${label}.requestedModel`); model(value.model, `${label}.model`); stringList(value.tools, `${label}.tools`); nonNegativeInteger(value.attempts, `${label}.attempts`); if (value.
|
|
68
|
+
stringList(value.structuralPath, `${label}.structuralPath`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.role, `${label}.role`); optionalString(value.requestedModel, `${label}.requestedModel`); model(value.model, `${label}.model`); stringList(value.tools, `${label}.tools`); nonNegativeInteger(value.attempts, `${label}.attempts`); if (value.lastEventAt !== undefined)
|
|
69
|
+
finiteNumber(value.lastEventAt, `${label}.lastEventAt`); if (value.attemptDetails !== undefined) {
|
|
69
70
|
if (!Array.isArray(value.attemptDetails))
|
|
70
71
|
throw new Error(`${label}.attemptDetails is invalid`);
|
|
71
72
|
for (const [index, attempt] of value.attemptDetails.entries()) {
|
|
@@ -135,6 +136,7 @@ function validateRunRecord(run) {
|
|
|
135
136
|
if (!object(session) || typeof session.sessionId !== "string" || !session.sessionId || typeof session.sessionFile !== "string" || !session.sessionFile)
|
|
136
137
|
throw new Error(`nativeSessions[${String(index)}] is invalid`);
|
|
137
138
|
optionalString(value.parentRunId, "Persisted parent run");
|
|
139
|
+
optionalString(value.failedAt, "Persisted failed path");
|
|
138
140
|
if (value.retry !== undefined) {
|
|
139
141
|
if (!object(value.retry) || typeof value.retry.sourceRunId !== "string" || !value.retry.sourceRunId || typeof value.retry.lineageRootRunId !== "string" || !value.retry.lineageRootRunId)
|
|
140
142
|
throw new Error("Persisted retry provenance is invalid");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { validateWorkflowLaunch, WorkflowError, WORKFLOW_TOOL_DESCRIPTION, WORKFLOW_TOOL_LABEL, WORKFLOW_TOOL_PARAMETERS, WORKFLOW_TOOL_PROMPT_SNIPPET } from "./index.js";
|
|
4
|
+
import { validateWorkflowLaunch, WorkflowError, WORKFLOW_RETRY_PARAMETERS, WORKFLOW_TOOL_DESCRIPTION, WORKFLOW_TOOL_LABEL, WORKFLOW_TOOL_PARAMETERS, WORKFLOW_TOOL_PROMPT_SNIPPET } from "./index.js";
|
|
5
5
|
export const CAPTURE_IDENTITY = "pi-extensible-workflows-eval-capture-v1";
|
|
6
6
|
export const CAPTURE_ERROR_PREFIX = `${CAPTURE_IDENTITY}:`;
|
|
7
7
|
export function resolveWorkflowSkillPath() {
|
|
@@ -45,4 +45,19 @@ export default function evalCaptureExtension(pi) {
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
});
|
|
48
|
+
pi.registerTool({
|
|
49
|
+
name: "workflow_retry",
|
|
50
|
+
label: "Workflow Retry",
|
|
51
|
+
description: "Capture a recovery selection without executing it",
|
|
52
|
+
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
53
|
+
async execute(_id, params) {
|
|
54
|
+
if (!params.runId.trim())
|
|
55
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `${CAPTURE_ERROR_PREFIX}RESUME_INCOMPATIBLE: workflow_retry requires an explicit run ID`);
|
|
56
|
+
const selection = { tool: "workflow_retry", arguments: { runId: params.runId } };
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: JSON.stringify({ captured: true, ...selection }) }],
|
|
59
|
+
details: { captured: true, captureIdentity: CAPTURE_IDENTITY, realWorkflowAgentsLaunched: 0, selection },
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
48
63
|
}
|
package/dist/src/execution.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fork, spawn } from "node:child_process";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -72,6 +72,7 @@ const named = (value, kind) => { if (typeof value !== "string" || !value.trim())
|
|
|
72
72
|
const path = (...names) => names.map(encodeURIComponent).join("/");
|
|
73
73
|
const inheritedAgentPath = new AsyncLocalStorage();
|
|
74
74
|
const agentOccurrences = new Map();
|
|
75
|
+
const agentInflight = new Set();
|
|
75
76
|
const shellOccurrences = new Map();
|
|
76
77
|
const worktreeOwners = new AsyncLocalStorage();
|
|
77
78
|
const rejectAgent = () => { throw workError("INVALID_METADATA", "Workflow agent calls must use a direct agent(...) call; aliases and indirect calls are unsupported"); };
|
|
@@ -92,14 +93,22 @@ const internalAgent = (...values) => {
|
|
|
92
93
|
const callSite = values.pop();
|
|
93
94
|
if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow agent call-site identity");
|
|
94
95
|
const inherited = inheritedAgentPath.getStore() || [];
|
|
95
|
-
// ponytail: same-callsite races outside parallel/pipeline lack a stable structural scope and are unsupported.
|
|
96
96
|
const occurrenceKey = JSON.stringify([inherited, callSite]);
|
|
97
|
+
if (agentInflight.has(occurrenceKey)) throw workError("INVALID_METADATA", "Concurrent agent calls from the same source call site are unsupported; use parallel(...) or pipeline(...)");
|
|
98
|
+
agentInflight.add(occurrenceKey);
|
|
97
99
|
const occurrence = (agentOccurrences.get(occurrenceKey) || 0) + 1;
|
|
98
100
|
agentOccurrences.set(occurrenceKey, occurrence);
|
|
99
101
|
const options = values.length < 2 || values[1] === undefined ? {} : values[1];
|
|
100
102
|
const worktreeOwner = worktreeOwners.getStore();
|
|
101
103
|
const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
|
|
102
|
-
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = rpc("agent", [values[0], options, identity]).then(unwrap);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
agentInflight.delete(occurrenceKey);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
void result.then(() => agentInflight.delete(occurrenceKey), () => agentInflight.delete(occurrenceKey));
|
|
103
112
|
Object.defineProperties(result, {
|
|
104
113
|
toJSON: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before serialization"); } },
|
|
105
114
|
toString: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before interpolation"); } },
|
|
@@ -421,7 +430,7 @@ function workflowErrorFromWorker(error) {
|
|
|
421
430
|
export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
422
431
|
encoded(args);
|
|
423
432
|
const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
|
|
424
|
-
const childDir = mkdtempSync(join(tmpdir(), "pi-wf-"));
|
|
433
|
+
const childDir = realpathSync(mkdtempSync(join(tmpdir(), "pi-wf-")));
|
|
425
434
|
const childFile = join(childDir, "child.cjs");
|
|
426
435
|
writeFileSync(childFile, childSource);
|
|
427
436
|
const child = fork(childFile, [String(RPC_LIMIT_BYTES), config], {
|
package/dist/src/host.d.ts
CHANGED
|
@@ -34,8 +34,8 @@ export declare function formatWorkflowPreview(args: {
|
|
|
34
34
|
description?: unknown;
|
|
35
35
|
}): string;
|
|
36
36
|
export declare const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
37
|
-
export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
|
|
38
|
-
export declare const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that
|
|
37
|
+
export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default";
|
|
38
|
+
export declare const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
|
|
39
39
|
export declare const WORKFLOW_TOOL_PARAMETERS: Type.TObject<{
|
|
40
40
|
name: Type.TOptional<Type.TString>;
|
|
41
41
|
description: Type.TOptional<Type.TString>;
|
|
@@ -49,6 +49,7 @@ export declare const WORKFLOW_TOOL_PARAMETERS: Type.TObject<{
|
|
|
49
49
|
}>;
|
|
50
50
|
export declare const WORKFLOW_RETRY_PARAMETERS: Type.TObject<{
|
|
51
51
|
runId: Type.TString;
|
|
52
|
+
foreground: Type.TOptional<Type.TBoolean>;
|
|
52
53
|
}>;
|
|
53
54
|
export type WorkflowPhaseState = "not started" | "running" | "completed" | "failed" | "cancelled" | "interrupted" | "budget_exhausted";
|
|
54
55
|
export interface WorkflowPhaseAgentCounts {
|
|
@@ -64,7 +65,6 @@ export interface WorkflowPhaseView {
|
|
|
64
65
|
name: string;
|
|
65
66
|
occurrence: number;
|
|
66
67
|
state: WorkflowPhaseState;
|
|
67
|
-
status: WorkflowPhaseState;
|
|
68
68
|
observed: boolean;
|
|
69
69
|
afterAgent?: number;
|
|
70
70
|
agents: readonly AgentRecord[];
|
|
@@ -83,19 +83,62 @@ export declare function buildWorkflowPhaseModel(run: Pick<PersistedRun, "state"
|
|
|
83
83
|
export interface WorkflowPhaseSelection {
|
|
84
84
|
phaseId?: string | undefined;
|
|
85
85
|
agentId?: string | undefined;
|
|
86
|
+
nodeId?: string | undefined;
|
|
87
|
+
expandedNodeIds?: readonly string[] | undefined;
|
|
88
|
+
treeOnly?: boolean | undefined;
|
|
89
|
+
detailsOnly?: boolean | undefined;
|
|
90
|
+
actions?: {
|
|
91
|
+
title: string;
|
|
92
|
+
options: readonly string[];
|
|
93
|
+
index: number;
|
|
94
|
+
} | undefined;
|
|
95
|
+
}
|
|
96
|
+
export type WorkflowPhaseTreeNodeKind = "phase" | "operation" | "agent";
|
|
97
|
+
export interface WorkflowPhaseTreeNode {
|
|
98
|
+
id: string;
|
|
99
|
+
kind: WorkflowPhaseTreeNodeKind;
|
|
100
|
+
label: string;
|
|
101
|
+
depth: number;
|
|
102
|
+
phaseId: string;
|
|
103
|
+
operationPath: readonly string[];
|
|
104
|
+
parentId?: string;
|
|
105
|
+
children: readonly string[];
|
|
106
|
+
state: WorkflowPhaseState | AgentRecord["state"];
|
|
107
|
+
agentId?: string;
|
|
108
|
+
agent?: AgentRecord;
|
|
109
|
+
phase?: WorkflowPhaseView;
|
|
110
|
+
}
|
|
111
|
+
export interface WorkflowPhaseTree {
|
|
112
|
+
roots: readonly string[];
|
|
113
|
+
nodes: readonly WorkflowPhaseTreeNode[];
|
|
114
|
+
byId: ReadonlyMap<string, WorkflowPhaseTreeNode>;
|
|
115
|
+
}
|
|
116
|
+
export interface WorkflowPhaseTreeSelection {
|
|
117
|
+
nodeId?: string | undefined;
|
|
86
118
|
}
|
|
119
|
+
export type WorkflowPhaseTreeDirection = "up" | "down" | "left" | "right";
|
|
120
|
+
export declare function buildWorkflowPhaseTree(model: WorkflowPhaseModel): WorkflowPhaseTree;
|
|
121
|
+
export declare function workflowPhaseTreeVisibleNodes(tree: WorkflowPhaseTree, expanded?: ReadonlySet<string>): readonly WorkflowPhaseTreeNode[];
|
|
122
|
+
export declare function workflowPhaseTreeInitialExpanded(tree: WorkflowPhaseTree): ReadonlySet<string>;
|
|
123
|
+
export declare function preserveWorkflowPhaseTreeSelection(tree: WorkflowPhaseTree, selection: WorkflowPhaseTreeSelection): WorkflowPhaseTreeSelection;
|
|
124
|
+
export declare function navigateWorkflowPhaseTree(tree: WorkflowPhaseTree, selectedNodeId: string | undefined, expandedNodeIds: ReadonlySet<string>, direction: WorkflowPhaseTreeDirection): {
|
|
125
|
+
nodeId?: string;
|
|
126
|
+
expandedNodeIds: ReadonlySet<string>;
|
|
127
|
+
};
|
|
87
128
|
export declare function preserveWorkflowPhaseSelection(model: WorkflowPhaseModel, selection: WorkflowPhaseSelection): WorkflowPhaseSelection;
|
|
88
|
-
export declare function formatWorkflowProgress(run: PersistedRun, spinner?: string, styles?: WorkflowProgressStyles): string;
|
|
129
|
+
export declare function formatWorkflowProgress(run: PersistedRun, spinner?: string, styles?: WorkflowProgressStyles, now?: number): string;
|
|
89
130
|
export declare function truncateWorkflowProgress(text: string, width: number): string[];
|
|
90
131
|
export declare function formatBudgetStatus(run: Pick<PersistedRun, "budget" | "budgetVersion" | "usage" | "budgetEvents">): string[];
|
|
91
132
|
export declare function agentBreadcrumbParts(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string[];
|
|
92
133
|
export declare function agentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string;
|
|
93
|
-
export declare function
|
|
134
|
+
export declare function formatStalledDuration(durationMs: number): string;
|
|
135
|
+
export declare function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[], now?: number): string;
|
|
94
136
|
export declare function formatNavigatorRun(loaded: {
|
|
95
137
|
run: PersistedRun;
|
|
96
138
|
snapshot: Readonly<LaunchSnapshot>;
|
|
97
|
-
}, checkpoints: readonly AwaitingCheckpoint[],
|
|
98
|
-
export declare function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection?: WorkflowPhaseSelection, styles?: WorkflowProgressStyles): string[];
|
|
139
|
+
}, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[], now?: number): string;
|
|
140
|
+
export declare function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection?: WorkflowPhaseSelection, styles?: WorkflowProgressStyles, now?: number): string[];
|
|
99
141
|
export declare function formatWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string;
|
|
100
|
-
export
|
|
142
|
+
export declare function formatWorkflowFailureDelivery(diagnostic: WorkflowFailureDiagnostics): string;
|
|
143
|
+
export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard?: typeof copyToClipboard, createSession?: SessionFactory, agentDir?: string, additionalSkillPaths?: readonly string[]): void;
|
|
101
144
|
export {};
|