pi-extensible-workflows 3.2.0 → 3.4.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 +2 -31
- package/dist/src/agent-execution.d.ts +67 -9
- package/dist/src/agent-execution.js +385 -141
- 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 +156 -22
- package/dist/src/doctor-cleanup.js +68 -31
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +12 -9
- package/dist/src/host.js +525 -195
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.js +3 -2
- package/dist/src/persistence.d.ts +43 -8
- package/dist/src/persistence.js +54 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +50 -24
- package/dist/src/types.d.ts +141 -60
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +28 -7
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -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/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +302 -107
- package/src/bundles.ts +471 -0
- package/src/cli.ts +130 -22
- package/src/doctor-cleanup.ts +38 -4
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +27 -15
- package/src/host.ts +454 -175
- package/src/index.ts +5 -4
- package/src/persistence.ts +58 -4
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +49 -26
- package/src/types.ts +55 -30
- package/src/validation.ts +19 -6
- package/src/workflow-artifacts.ts +1 -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,41 @@ 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;
|
|
389
|
+
#runtimeStartedAt = 0;
|
|
390
|
+
#runtimeBaseMs = 0;
|
|
363
391
|
#timer;
|
|
364
392
|
#interactive;
|
|
365
393
|
#styles;
|
|
366
|
-
constructor(stderr, tty) {
|
|
394
|
+
constructor(stderr, tty, onRunId) {
|
|
367
395
|
this.stderr = stderr;
|
|
396
|
+
this.onRunId = onRunId;
|
|
368
397
|
this.#interactive = tty && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
|
|
369
398
|
this.#styles = terminalProgressStyles(this.#interactive);
|
|
370
399
|
}
|
|
371
400
|
update(run) {
|
|
372
|
-
|
|
401
|
+
if (this.#runId !== run.id) {
|
|
402
|
+
this.#runId = run.id;
|
|
403
|
+
this.onRunId(run.id);
|
|
404
|
+
this.#runtimeStartedAt = Date.now();
|
|
405
|
+
this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
|
|
406
|
+
}
|
|
407
|
+
else if (this.#run && this.#run.state !== "running" && run.state === "running") {
|
|
408
|
+
this.#runtimeStartedAt = Date.now();
|
|
409
|
+
this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
|
|
410
|
+
}
|
|
411
|
+
this.#run = run;
|
|
373
412
|
if (!this.#interactive) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
413
|
+
this.#timer ??= setInterval(() => { this.render(); }, 1000);
|
|
414
|
+
this.#timer.unref();
|
|
415
|
+
this.render();
|
|
378
416
|
return;
|
|
379
417
|
}
|
|
380
|
-
this.#run = run;
|
|
381
418
|
this.#timer ??= setInterval(() => { this.render(); }, 80);
|
|
382
419
|
this.#timer.unref();
|
|
383
420
|
this.render();
|
|
@@ -385,9 +422,18 @@ class CliProgress {
|
|
|
385
422
|
render() {
|
|
386
423
|
if (!this.#run)
|
|
387
424
|
return;
|
|
425
|
+
const run = this.#run.state !== "running" ? this.#run : { ...this.#run, usage: { ...(this.#run.usage ?? { tokens: 0, costUsd: 0, durationMs: 0, agentLaunches: 0 }), durationMs: Math.max(this.#run.usage?.durationMs ?? 0, this.#runtimeBaseMs + Date.now() - this.#runtimeStartedAt) } };
|
|
426
|
+
if (!this.#interactive) {
|
|
427
|
+
const stable = formatWorkflowProgress(run, "◇", this.#styles);
|
|
428
|
+
if (stable !== this.#lastStable) {
|
|
429
|
+
this.#lastStable = stable;
|
|
430
|
+
this.stderr(`${stable}\n`);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
388
434
|
const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
|
|
389
435
|
const width = process.stderr.columns || 80;
|
|
390
|
-
const text = truncateWorkflowProgress(formatWorkflowProgress(
|
|
436
|
+
const text = truncateWorkflowProgress(formatWorkflowProgress(run, spinner, this.#styles), width).join("\n");
|
|
391
437
|
this.stderr(`${this.#lines ? `\x1b[${String(this.#lines)}A` : ""}${this.#lines ? "" : "\x1b[?25l"}\x1b[0J${text}\n`);
|
|
392
438
|
this.#lines = text.split("\n").length;
|
|
393
439
|
}
|
|
@@ -406,18 +452,24 @@ class CliProgress {
|
|
|
406
452
|
async function invokeWorkflow(fn, args, runtime, options, context) {
|
|
407
453
|
if (!Value.Check(fn.input, args))
|
|
408
454
|
throw new Error(`Invalid input for ${fn.name}`);
|
|
409
|
-
|
|
455
|
+
let announcedRunId;
|
|
456
|
+
const announceRunId = (runId) => { if (announcedRunId === runId)
|
|
457
|
+
return; announcedRunId = runId; options.stderr(`Run ID: ${runId}\n`); };
|
|
458
|
+
const progress = new CliProgress(options.stderr, options.isTTY ?? process.stderr.isTTY, announceRunId);
|
|
410
459
|
try {
|
|
411
460
|
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
461
|
progress.update(update.details.run); }, context);
|
|
413
462
|
const details = object(result.details) ? result.details : {};
|
|
463
|
+
const runId = typeof details.runId === "string" ? details.runId : undefined;
|
|
464
|
+
if (runId)
|
|
465
|
+
announceRunId(runId);
|
|
414
466
|
if (has(details, "value"))
|
|
415
|
-
return details.value;
|
|
467
|
+
return { value: details.value, ...(runId ? { runId } : {}) };
|
|
416
468
|
const first = result.content[0];
|
|
417
469
|
if (!first || first.type !== "text")
|
|
418
470
|
throw new Error("Workflow returned no result");
|
|
419
471
|
try {
|
|
420
|
-
return parseJsonInput(first.text);
|
|
472
|
+
return { value: parseJsonInput(first.text), ...(runId ? { runId } : {}) };
|
|
421
473
|
}
|
|
422
474
|
catch {
|
|
423
475
|
throw new Error("Workflow returned invalid JSON");
|
|
@@ -471,14 +523,16 @@ async function runWorkflowCli(rawArgs, options) {
|
|
|
471
523
|
return 0;
|
|
472
524
|
}
|
|
473
525
|
const input = parseWorkflowCliArgs(fn.input, args.slice(1));
|
|
474
|
-
const
|
|
475
|
-
options.write(`${JSON.stringify(value)}\n`);
|
|
526
|
+
const result = await invokeWorkflow(fn, input, runtime, options, context);
|
|
527
|
+
options.write(`${JSON.stringify(result.value)}\n`);
|
|
476
528
|
return 0;
|
|
477
529
|
});
|
|
478
530
|
}
|
|
479
531
|
async function exportWorkflowCli(rawArgs, options) {
|
|
480
532
|
const parsed = stripTrustOptions(rawArgs);
|
|
481
533
|
const args = parsed.args;
|
|
534
|
+
if (args.includes("--bundle"))
|
|
535
|
+
return bundleWorkflowCli(args.filter((arg) => arg !== "--bundle"), options);
|
|
482
536
|
if (!args.length || args[0] === "--help" || args[0] === "-h") {
|
|
483
537
|
options.write(exportUsage());
|
|
484
538
|
return args.length ? 0 : 1;
|
|
@@ -534,6 +588,80 @@ async function exportWorkflowCli(rawArgs, options) {
|
|
|
534
588
|
return 0;
|
|
535
589
|
});
|
|
536
590
|
}
|
|
591
|
+
async function bundleWorkflowCli(rawArgs, options) {
|
|
592
|
+
const parsed = stripTrustOptions(rawArgs);
|
|
593
|
+
const args = parsed.args;
|
|
594
|
+
if (!args.length || args[0] === "--help" || args[0] === "-h") {
|
|
595
|
+
options.write(bundleUsage());
|
|
596
|
+
return args.length ? 0 : 1;
|
|
597
|
+
}
|
|
598
|
+
const workflowName = args[0];
|
|
599
|
+
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
|
|
600
|
+
let name;
|
|
601
|
+
let output;
|
|
602
|
+
let force = false;
|
|
603
|
+
const requirements = { roles: [], aliases: [], tools: [], commands: [], environment: [] };
|
|
604
|
+
const resources = { extensions: [], skills: [], static: [], dependencies: [] };
|
|
605
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
606
|
+
const arg = args[index];
|
|
607
|
+
if (arg === "--force") {
|
|
608
|
+
force = true;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const equals = arg.indexOf("=");
|
|
612
|
+
const option = equals >= 0 ? arg.slice(0, equals) : arg;
|
|
613
|
+
const requirementOptions = { "--role": "roles", "--alias": "aliases", "--tool": "tools", "--command": "commands", "--environment": "environment" };
|
|
614
|
+
const resourceOptions = { "--extension": "extensions", "--skill": "skills", "--resource": "static", "--dependency": "dependencies" };
|
|
615
|
+
if (option === "--name" || option === "--output" || Object.prototype.hasOwnProperty.call(requirementOptions, option) || Object.prototype.hasOwnProperty.call(resourceOptions, option)) {
|
|
616
|
+
const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
|
|
617
|
+
if (!value)
|
|
618
|
+
throw new Error(`Missing value for ${option}`);
|
|
619
|
+
if (option === "--name")
|
|
620
|
+
name = value;
|
|
621
|
+
else if (option === "--output")
|
|
622
|
+
output = value;
|
|
623
|
+
else if (Object.prototype.hasOwnProperty.call(requirementOptions, option))
|
|
624
|
+
requirements[requirementOptions[option]].push(value);
|
|
625
|
+
else
|
|
626
|
+
resources[resourceOptions[option]].push(value);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (arg === "--help" || arg === "-h") {
|
|
630
|
+
options.write(bundleUsage());
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
634
|
+
}
|
|
635
|
+
const fn = runtime.catalog.functions.find((candidate) => candidate.name === workflowName);
|
|
636
|
+
if (!fn)
|
|
637
|
+
throw new Error(`Unknown workflow function: ${workflowName}`);
|
|
638
|
+
const registered = registeredWorkflowFunctions()[workflowName];
|
|
639
|
+
if (!registered)
|
|
640
|
+
throw new Error(`Workflow ${workflowName} is not exportable because its registered implementation is unavailable`);
|
|
641
|
+
const definitions = requirements.roles.length ? loadAgentDefinitions(options.cwd ?? process.cwd(), options.agentDir ?? getAgentDir(), runtime.services.settingsManager.isProjectTrusted()) : {};
|
|
642
|
+
const roles = Object.fromEntries(requirements.roles.map((role) => {
|
|
643
|
+
if (!role || role === "." || role === ".." || role.includes("/") || role.includes("\\"))
|
|
644
|
+
throw new Error(`Invalid role name for bundle: ${role}`);
|
|
645
|
+
const definition = definitions[role];
|
|
646
|
+
if (!definition)
|
|
647
|
+
throw new Error(`Unknown role for bundle: ${role}`);
|
|
648
|
+
return [role, definition];
|
|
649
|
+
}));
|
|
650
|
+
const command = commandName(name ?? kebabCase(workflowName));
|
|
651
|
+
if (!command)
|
|
652
|
+
throw new Error("Command name must be a non-empty name without path separators");
|
|
653
|
+
const destination = output ?? join(homedir(), ".local", "share", "pi-extensible-workflows", "bundles", command);
|
|
654
|
+
const aliasTargets = Object.fromEntries(requirements.aliases.flatMap((name) => {
|
|
655
|
+
const target = runtime.catalog.modelAliases?.[name];
|
|
656
|
+
return typeof target === "string" ? [[name, target]] : [];
|
|
657
|
+
}));
|
|
658
|
+
const selectedResources = Object.values(resources).some((entries) => entries.length) ? resources : undefined;
|
|
659
|
+
writePortableWorkflowBundle({ destination, command, workflow: fn, functionSource: registered.run.toString(), requirements, aliasTargets, roles, ...(selectedResources ? { resources: selectedResources } : {}), piVersion: portablePiVersion(), engineVersion: portableEngineVersion(), force });
|
|
660
|
+
options.write(`Bundled ${workflowName} at ${destination}\n`);
|
|
661
|
+
options.write(`Run ${join(destination, command)} setup before launching the workflow.\n`);
|
|
662
|
+
return 0;
|
|
663
|
+
});
|
|
664
|
+
}
|
|
537
665
|
export async function runCli(args, options = {}, write = (text) => { process.stdout.write(text); }) {
|
|
538
666
|
const stderr = options.stderr ?? ((text) => { process.stderr.write(text); });
|
|
539
667
|
if (args[0] === "doctor" && args.length === 1) {
|
|
@@ -558,9 +686,13 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
558
686
|
return 1;
|
|
559
687
|
}
|
|
560
688
|
}
|
|
561
|
-
if (args[0] === "inspect"
|
|
689
|
+
if (args[0] === "inspect") {
|
|
562
690
|
try {
|
|
563
|
-
|
|
691
|
+
const parsed = parseInspectArgs(args.slice(1));
|
|
692
|
+
if (options.inspect)
|
|
693
|
+
await options.inspect(parsed.sessionId, parsed.mode, parsed.failedOnly);
|
|
694
|
+
else
|
|
695
|
+
await runSessionInspector(parsed.sessionId, parsed.mode, options.cwd ?? process.cwd(), undefined, write, parsed.failedOnly);
|
|
564
696
|
return 0;
|
|
565
697
|
}
|
|
566
698
|
catch (error) {
|
|
@@ -581,9 +713,11 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
581
713
|
return 1;
|
|
582
714
|
}
|
|
583
715
|
}
|
|
584
|
-
if (args[0] === "run" || args[0] === "export") {
|
|
716
|
+
if (args[0] === "bundle" || args[0] === "run" || args[0] === "export") {
|
|
585
717
|
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 } : {}) };
|
|
718
|
+
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] } : {}) };
|
|
719
|
+
if (args[0] === "bundle")
|
|
720
|
+
return await bundleWorkflowCli(args.slice(1), workflowOptions);
|
|
587
721
|
return args[0] === "run" ? await runWorkflowCli(args.slice(1), workflowOptions) : await exportWorkflowCli(args.slice(1), workflowOptions);
|
|
588
722
|
}
|
|
589
723
|
catch (error) {
|
|
@@ -591,7 +725,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
591
725
|
return 1;
|
|
592
726
|
}
|
|
593
727
|
}
|
|
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");
|
|
728
|
+
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
729
|
return 1;
|
|
596
730
|
}
|
|
597
731
|
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"]);
|
|
@@ -63,35 +63,70 @@ function validateScheduledOptions(value, label) { if (!object(value) || typeof v
|
|
|
63
63
|
optionalString(value.agentIdentity.parentBreadcrumb, `${label}.agentIdentity.parentBreadcrumb`);
|
|
64
64
|
optionalString(value.agentIdentity.worktreeOwner, `${label}.agentIdentity.worktreeOwner`);
|
|
65
65
|
} }
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
66
|
+
function validateSessionReference(value, label) {
|
|
67
|
+
if (!object(value) || typeof value.transport !== "string" || !value.transport || typeof value.sessionId !== "string" || !value.sessionId || (value.locator !== undefined && !jsonValue(value.locator)))
|
|
68
|
+
throw new Error(`${label} is invalid`);
|
|
69
|
+
}
|
|
70
|
+
function validateAgentSetup(value, label) {
|
|
71
|
+
if (!object(value))
|
|
72
|
+
throw new Error(`${label} is invalid`);
|
|
73
|
+
stringList(value.hookNames, `${label}.hookNames`);
|
|
74
|
+
model(value.model, `${label}.model`);
|
|
75
|
+
stringList(value.tools, `${label}.tools`);
|
|
76
|
+
if (typeof value.cwd !== "string" || !value.cwd)
|
|
77
|
+
throw new Error(`${label}.cwd is invalid`);
|
|
78
|
+
resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
|
|
79
|
+
}
|
|
80
|
+
function validateAgent(value, label) {
|
|
81
|
+
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))
|
|
82
|
+
throw new Error(`${label} is invalid`);
|
|
83
|
+
optionalString(value.systemPrompt, `${label}.systemPrompt`);
|
|
84
|
+
optionalString(value.prompt, `${label}.prompt`);
|
|
85
|
+
optionalString(value.label, `${label}.label`);
|
|
86
|
+
optionalString(value.parentId, `${label}.parentId`);
|
|
87
|
+
if (value.structuralPath !== undefined)
|
|
88
|
+
stringList(value.structuralPath, `${label}.structuralPath`);
|
|
89
|
+
optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`);
|
|
90
|
+
optionalString(value.worktreeOwner, `${label}.worktreeOwner`);
|
|
91
|
+
optionalString(value.role, `${label}.role`);
|
|
92
|
+
optionalString(value.requestedModel, `${label}.requestedModel`);
|
|
93
|
+
model(value.model, `${label}.model`);
|
|
94
|
+
stringList(value.tools, `${label}.tools`);
|
|
95
|
+
nonNegativeInteger(value.attempts, `${label}.attempts`);
|
|
96
|
+
if (value.attemptDetails !== undefined) {
|
|
97
|
+
if (!Array.isArray(value.attemptDetails))
|
|
98
|
+
throw new Error(`${label}.attemptDetails is invalid`);
|
|
99
|
+
for (const [index, attempt] of value.attemptDetails.entries()) {
|
|
100
|
+
const at = `${label}.attemptDetails[${String(index)}]`;
|
|
101
|
+
if (!object(attempt) || !Number.isSafeInteger(attempt.attempt) || Number(attempt.attempt) < 1 || typeof attempt.transport !== "string" || !attempt.transport || Object.hasOwn(attempt, "sessionId") || Object.hasOwn(attempt, "sessionFile"))
|
|
102
|
+
throw new Error(`${at} is invalid`);
|
|
103
|
+
validateAgentSetup(attempt.setup, `${at}.setup`);
|
|
104
|
+
if (attempt.session !== undefined) {
|
|
105
|
+
validateSessionReference(attempt.session, `${at}.session`);
|
|
106
|
+
if (attempt.session.transport !== attempt.transport)
|
|
107
|
+
throw new Error(`${at}.session transport does not match attempt transport`);
|
|
108
|
+
}
|
|
109
|
+
accounting(attempt.accounting, `${at}.accounting`);
|
|
110
|
+
if (attempt.error !== undefined && (!object(attempt.error) || typeof attempt.error.code !== "string" || typeof attempt.error.message !== "string"))
|
|
111
|
+
throw new Error(`${at}.error is invalid`);
|
|
84
112
|
}
|
|
85
113
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
for (const call of value.toolCalls)
|
|
91
|
-
if (!object(call) || typeof call.id !== "string" || typeof call.name !== "string" || !["running", "completed", "failed"].includes(call.state))
|
|
114
|
+
if (value.accounting !== undefined)
|
|
115
|
+
accounting(value.accounting, `${label}.accounting`);
|
|
116
|
+
if (value.toolCalls !== undefined) {
|
|
117
|
+
if (!Array.isArray(value.toolCalls))
|
|
92
118
|
throw new Error(`${label}.toolCalls is invalid`);
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
for (const [index, call] of value.toolCalls.entries())
|
|
120
|
+
if (!object(call) || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || !["running", "completed", "failed"].includes(call.state))
|
|
121
|
+
throw new Error(`${label}.toolCalls[${String(index)}] is invalid`);
|
|
122
|
+
}
|
|
123
|
+
if (value.activity !== undefined) {
|
|
124
|
+
if (!object(value.activity) || !["reasoning", "tool", "text"].includes(value.activity.kind) || typeof value.activity.text !== "string")
|
|
125
|
+
throw new Error(`${label}.activity is invalid`);
|
|
126
|
+
}
|
|
127
|
+
if (value.lastEventAt !== undefined)
|
|
128
|
+
finiteNumber(value.lastEventAt, `${label}.lastEventAt`);
|
|
129
|
+
}
|
|
95
130
|
function validateUsage(value, label) { if (!object(value))
|
|
96
131
|
throw new Error(`${label} is invalid`); for (const key of ["tokens", "costUsd", "durationMs", "agentLaunches"])
|
|
97
132
|
finiteNumber(value[key], `${label}.${key}`); }
|
|
@@ -106,7 +141,7 @@ function validateBudgetEvents(value) { if (value === undefined)
|
|
|
106
141
|
} }
|
|
107
142
|
function validateRunRecord(run) {
|
|
108
143
|
const value = run;
|
|
109
|
-
if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !RUN_STATES.includes(value.state) || !Array.isArray(value.agents) || !Array.isArray(value.nativeSessions))
|
|
144
|
+
if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !RUN_STATES.includes(value.state) || !Array.isArray(value.agents) || !Array.isArray(value.agentSessions) || Object.hasOwn(value, "nativeSessions"))
|
|
110
145
|
throw new Error("Persisted run state is invalid");
|
|
111
146
|
const agents = value.agents;
|
|
112
147
|
agents.forEach((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); });
|
|
@@ -131,10 +166,11 @@ function validateRunRecord(run) {
|
|
|
131
166
|
parent = typeof parentAgent?.parentId === "string" ? parentAgent.parentId : undefined;
|
|
132
167
|
}
|
|
133
168
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
169
|
+
const sessions = value.agentSessions;
|
|
170
|
+
for (const [index, session] of sessions.entries())
|
|
171
|
+
validateSessionReference(session, `agentSessions[${String(index)}]`);
|
|
137
172
|
optionalString(value.parentRunId, "Persisted parent run");
|
|
173
|
+
optionalString(value.failedAt, "Persisted failed path");
|
|
138
174
|
if (value.retry !== undefined) {
|
|
139
175
|
if (!object(value.retry) || typeof value.retry.sourceRunId !== "string" || !value.retry.sourceRunId || typeof value.retry.lineageRootRunId !== "string" || !value.retry.lineageRootRunId)
|
|
140
176
|
throw new Error("Persisted retry provenance is invalid");
|
|
@@ -265,6 +301,7 @@ async function validateRunArtifacts(store, workflowScript, state) {
|
|
|
265
301
|
throw new Error(`${label} is invalid`);
|
|
266
302
|
ownershipIds.add(record.id);
|
|
267
303
|
optionalString(record.parentId, `${label}.parentId`);
|
|
304
|
+
optionalString(record.prompt, `${label}.prompt`);
|
|
268
305
|
validateScheduledOptions(record.options, `${label}.options`);
|
|
269
306
|
}
|
|
270
307
|
for (const record of ownership)
|
|
@@ -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.d.ts
CHANGED
|
@@ -12,6 +12,6 @@ export declare function agentWorktree(identity: AgentIdentity): {
|
|
|
12
12
|
};
|
|
13
13
|
export declare function executeShellCommand(command: string, options: ShellOptions, signal: AbortSignal, cwd?: string): Promise<ShellResult>;
|
|
14
14
|
export declare function runWorkflow(script: string, args?: JsonValue, bridge?: WorkflowBridge, signal?: AbortSignal): WorkflowExecution;
|
|
15
|
-
export declare function persistActiveAgentAttempt(store: RunStore, id: string, active:
|
|
15
|
+
export declare function persistActiveAgentAttempt(store: RunStore, id: string, active: AgentAttempt): Promise<void>;
|
|
16
16
|
export declare function persistAgentAttempts(store: RunStore, id: string, attempts: readonly AgentAttempt[]): Promise<void>;
|
|
17
17
|
export type { AgentIdentity, ShellIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";
|
package/dist/src/execution.js
CHANGED
|
@@ -9,6 +9,7 @@ import { asWorkflowError, errorText, fail, isWorkflowAuthored, jsonValue, markWo
|
|
|
9
9
|
import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validateShellOptions } from "./validation.js";
|
|
10
10
|
export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
11
11
|
export const HEARTBEAT_TIMEOUT_MS = 5000;
|
|
12
|
+
const HEARTBEAT_INTERVAL_MS = 1000;
|
|
12
13
|
const OUTCOME_ERRORS = new Set(["AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID"]);
|
|
13
14
|
const WORK_RESULT_BRAND = "__workResult";
|
|
14
15
|
const childSource = String.raw `
|
|
@@ -58,7 +59,7 @@ process.on("message", raw => {
|
|
|
58
59
|
if (message.ok) request.resolve(message.value);
|
|
59
60
|
else request.reject(workflowError(message.error));
|
|
60
61
|
});
|
|
61
|
-
const heartbeat = setInterval(() => send({ type: "heartbeat" }),
|
|
62
|
+
const heartbeat = setInterval(() => send({ type: "heartbeat" }), ${HEARTBEAT_INTERVAL_MS});
|
|
62
63
|
send({ type: "heartbeat" });
|
|
63
64
|
const BRAND = "${WORK_RESULT_BRAND}";
|
|
64
65
|
const workError = (code, message) => Object.assign(new Error(message), { code });
|
|
@@ -458,7 +459,20 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
458
459
|
const controller = new AbortController();
|
|
459
460
|
let settled = false;
|
|
460
461
|
let rejectResult = () => undefined;
|
|
461
|
-
let
|
|
462
|
+
let lastHeartbeatAt = performance.now();
|
|
463
|
+
let lastWatchdogCheckAt = lastHeartbeatAt;
|
|
464
|
+
const watchdog = setInterval(() => {
|
|
465
|
+
const now = performance.now();
|
|
466
|
+
// A check delayed enough to bridge the normal heartbeat margin makes elapsed time unreliable.
|
|
467
|
+
const watchdogCheckWasDelayed = now - lastWatchdogCheckAt >= HEARTBEAT_TIMEOUT_MS - HEARTBEAT_INTERVAL_MS;
|
|
468
|
+
lastWatchdogCheckAt = now;
|
|
469
|
+
if (watchdogCheckWasDelayed) {
|
|
470
|
+
lastHeartbeatAt = now;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (now - lastHeartbeatAt >= HEARTBEAT_TIMEOUT_MS)
|
|
474
|
+
stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat");
|
|
475
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
462
476
|
const result = new Promise((resolve, reject) => {
|
|
463
477
|
rejectResult = reject;
|
|
464
478
|
child.on("message", (raw) => {
|
|
@@ -469,8 +483,7 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
469
483
|
if (!jsonValue(message))
|
|
470
484
|
fail("RPC_LIMIT_EXCEEDED", "Worker RPC must contain JSON-compatible values");
|
|
471
485
|
if (message.type === "heartbeat") {
|
|
472
|
-
|
|
473
|
-
watchdog = setTimeout(() => { stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat"); }, HEARTBEAT_TIMEOUT_MS);
|
|
486
|
+
lastHeartbeatAt = performance.now();
|
|
474
487
|
return;
|
|
475
488
|
}
|
|
476
489
|
if (message.type === "result") {
|
|
@@ -502,7 +515,7 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
502
515
|
child.kill("SIGKILL"); }, 1000).unref();
|
|
503
516
|
}
|
|
504
517
|
}
|
|
505
|
-
function finish() { settled = true;
|
|
518
|
+
function finish() { settled = true; clearInterval(watchdog); signal?.removeEventListener("abort", cancel); killChild(); rmSync(childDir, { recursive: true, force: true }); }
|
|
506
519
|
function stop(code, message) { if (settled)
|
|
507
520
|
return; controller.abort(); finish(); rejectResult(new WorkflowError(code, message)); }
|
|
508
521
|
function branded(result) { return { ...result, [WORK_RESULT_BRAND]: true }; }
|
|
@@ -612,8 +625,8 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
612
625
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
613
626
|
return { result, cancel };
|
|
614
627
|
}
|
|
615
|
-
function
|
|
616
|
-
return {
|
|
628
|
+
function attemptSummary(attempt, accounting = attempt.accounting) {
|
|
629
|
+
return { attempt: attempt.attempt, transport: attempt.transport, ...(attempt.session ? { session: attempt.session } : {}), ...(attempt.error ? { error: attempt.error } : {}), accounting, setup: attempt.setup };
|
|
617
630
|
}
|
|
618
631
|
export async function persistActiveAgentAttempt(store, id, active) {
|
|
619
632
|
await store.updateState((run) => {
|
|
@@ -621,9 +634,11 @@ export async function persistActiveAgentAttempt(store, id, active) {
|
|
|
621
634
|
if (!agent)
|
|
622
635
|
throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
|
|
623
636
|
const accounting = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
624
|
-
const
|
|
625
|
-
const
|
|
626
|
-
|
|
637
|
+
const detail = attemptSummary(active, accounting);
|
|
638
|
+
const details = [...(agent.attemptDetails ?? []).filter((candidate) => candidate.attempt !== active.attempt), detail];
|
|
639
|
+
const session = active.session;
|
|
640
|
+
const agentSessions = session && !run.agentSessions.some(({ transport, sessionId }) => transport === session.transport && sessionId === session.sessionId) ? [...run.agentSessions, session] : run.agentSessions;
|
|
641
|
+
return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: Math.max(candidate.attempts, active.attempt), attemptDetails: details } : candidate), agentSessions };
|
|
627
642
|
});
|
|
628
643
|
}
|
|
629
644
|
export async function persistAgentAttempts(store, id, attempts) {
|
|
@@ -632,8 +647,9 @@ export async function persistAgentAttempts(store, id, attempts) {
|
|
|
632
647
|
if (!agent)
|
|
633
648
|
throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
|
|
634
649
|
const total = attempts.reduce((sum, attempt) => ({ input: sum.input + attempt.accounting.input, output: sum.output + attempt.accounting.output, cacheRead: sum.cacheRead + attempt.accounting.cacheRead, cacheWrite: sum.cacheWrite + attempt.accounting.cacheWrite, cost: sum.cost + attempt.accounting.cost }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
635
|
-
const attemptDetails = attempts.map((
|
|
636
|
-
const
|
|
637
|
-
|
|
650
|
+
const attemptDetails = attempts.map((attempt) => attemptSummary(attempt));
|
|
651
|
+
const sessions = attempts.map((attempt) => attempt.session).filter((session) => session !== undefined);
|
|
652
|
+
const sessionKeys = new Set(sessions.map(({ transport, sessionId }) => `${transport}:${sessionId}`));
|
|
653
|
+
return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: attempts.length, attemptDetails, accounting: total } : candidate), agentSessions: [...run.agentSessions.filter(({ transport, sessionId }) => !sessionKeys.has(`${transport}:${sessionId}`)), ...sessions] };
|
|
638
654
|
});
|
|
639
655
|
}
|