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/src/cli.ts
CHANGED
|
@@ -8,12 +8,13 @@ import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionF
|
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
import { doctor, doctorExitCode, formatDoctorReport, type DoctorOptions } from "./doctor.js";
|
|
10
10
|
import { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport, type DoctorCleanupOptions } from "./doctor-cleanup.js";
|
|
11
|
-
import workflowExtension, { formatWorkflowProgress, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath, type JsonSchema, type JsonValue, type WorkflowProgressStyles } from "./index.js";
|
|
12
|
-
import {
|
|
11
|
+
import workflowExtension, { formatWorkflowProgress, loadAgentDefinitions, registeredWorkflowFunctions, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath, type JsonSchema, type JsonValue, type WorkflowProgressStyles } from "./index.js";
|
|
12
|
+
import { portableEngineVersion, portablePiVersion, writePortableWorkflowBundle } from "./bundles.js";
|
|
13
|
+
import { runSessionInspector, transcriptFileLines, type InspectMode } from "./session-inspector.js";
|
|
13
14
|
import type { PersistedRun } from "./persistence.js";
|
|
14
15
|
import type { WorkflowCatalogFunction } from "./index.js";
|
|
15
16
|
|
|
16
|
-
export interface CliOptions extends DoctorOptions { inspect?: (sessionId?: string) => Promise<void>; transcript?: (sessionFile: string) => Promise<void>; stderr?: (text: string) => void; signal?: AbortSignal; trustOverride?: boolean; isTTY?: boolean }
|
|
17
|
+
export interface CliOptions extends DoctorOptions { inspect?: (sessionId?: string, mode?: InspectMode, failedOnly?: boolean) => Promise<void>; transcript?: (sessionFile: string) => Promise<void>; stderr?: (text: string) => void; signal?: AbortSignal; trustOverride?: boolean; isTTY?: boolean; skillPaths?: readonly string[] }
|
|
17
18
|
|
|
18
19
|
type CliScalar = "string" | "integer" | "number" | "boolean";
|
|
19
20
|
type CliField = { name: string; option: string; schema: Record<string, unknown>; type: CliScalar | "array"; itemType?: CliScalar; required: boolean };
|
|
@@ -158,7 +159,24 @@ function launcherHelpLines(): string[] {
|
|
|
158
159
|
];
|
|
159
160
|
}
|
|
160
161
|
function workflowUsage(): string { return [`Usage: pi-extensible-workflows run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
161
|
-
function exportUsage(): string { return [`Usage: pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
162
|
+
function exportUsage(): string { return [`Usage: pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
163
|
+
function bundleUsage(): string { 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"; }
|
|
164
|
+
function parseInspectArgs(rawArgs: readonly string[]): { sessionId?: string; mode: InspectMode; failedOnly: boolean } {
|
|
165
|
+
let sessionId: string | undefined;
|
|
166
|
+
let mode: InspectMode = "tui";
|
|
167
|
+
let failedOnly = false;
|
|
168
|
+
for (const arg of rawArgs) {
|
|
169
|
+
if (arg === "--json" || arg === "--summary") {
|
|
170
|
+
const next: InspectMode = arg === "--json" ? "json" : "summary";
|
|
171
|
+
if (mode !== "tui" && mode !== next) throw new Error("inspect accepts only one output mode");
|
|
172
|
+
mode = next;
|
|
173
|
+
} else if (arg === "--failed") failedOnly = true;
|
|
174
|
+
else if (arg.startsWith("--")) throw new Error(`Unknown inspect option: ${arg}`);
|
|
175
|
+
else if (sessionId !== undefined) throw new Error(`Unexpected argument: ${arg}`);
|
|
176
|
+
else sessionId = arg;
|
|
177
|
+
}
|
|
178
|
+
return { ...(sessionId ? { sessionId } : {}), mode: failedOnly && mode === "tui" ? "summary" : mode, failedOnly };
|
|
179
|
+
}
|
|
162
180
|
export function parseDoctorCleanupArgs(rawArgs: readonly string[]): Required<Pick<DoctorCleanupOptions, "olderThanDays" | "yes">> {
|
|
163
181
|
let olderThanDays = 90;
|
|
164
182
|
let yes = false;
|
|
@@ -193,7 +211,7 @@ function stripTrustOptions(rawArgs: readonly string[]): { args: string[]; trustO
|
|
|
193
211
|
}
|
|
194
212
|
return { args, ...(trustOverride !== undefined ? { trustOverride } : {}) };
|
|
195
213
|
}
|
|
196
|
-
type WorkflowIo = { write: (text: string) => void; stderr: (text: string) => void; cwd?: string; agentDir?: string; trustOverride?: boolean; isTTY?: boolean; signal?: AbortSignal };
|
|
214
|
+
type WorkflowIo = { write: (text: string) => void; stderr: (text: string) => void; cwd?: string; agentDir?: string; trustOverride?: boolean; isTTY?: boolean; signal?: AbortSignal; skillPaths?: readonly string[] };
|
|
197
215
|
|
|
198
216
|
type HeadlessWorkflowTool = { execute: (toolCallId: string, params: Record<string, JsonValue>, signal: AbortSignal | undefined, onUpdate: ((update: unknown) => void) | undefined, context: unknown) => Promise<{ content: Array<{ type: string; text: string }>; details?: unknown }> };
|
|
199
217
|
type ShutdownHandler = (event: unknown, context: unknown) => Promise<void> | void;
|
|
@@ -235,7 +253,7 @@ async function createWorkflowRuntime(options: WorkflowIo, shutdownHandlers: Shut
|
|
|
235
253
|
cwd,
|
|
236
254
|
agentDir,
|
|
237
255
|
settingsManager,
|
|
238
|
-
resourceLoaderOptions: {},
|
|
256
|
+
resourceLoaderOptions: { ...(options.skillPaths?.length ? { additionalSkillPaths: [...options.skillPaths] } : {}) },
|
|
239
257
|
resourceLoaderReloadOptions: { resolveProjectTrust },
|
|
240
258
|
});
|
|
241
259
|
const extensions = services.resourceLoader.getExtensions();
|
|
@@ -251,7 +269,7 @@ async function createWorkflowRuntime(options: WorkflowIo, shutdownHandlers: Shut
|
|
|
251
269
|
sendMessage() {},
|
|
252
270
|
events: { emit() {} },
|
|
253
271
|
};
|
|
254
|
-
workflowExtension(headlessPi as never, homedir(), undefined, undefined, agentDir);
|
|
272
|
+
workflowExtension(headlessPi as never, homedir(), undefined, undefined, agentDir, options.skillPaths);
|
|
255
273
|
const workflowTool = tools.find((tool) => object(tool) && tool.name === "workflow") as HeadlessWorkflowTool | undefined;
|
|
256
274
|
if (!workflowTool) throw new Error("The workflow runtime could not be initialized");
|
|
257
275
|
return { catalog: workflowCatalog({ cwd, projectTrusted: settingsManager.isProjectTrusted(), globalSettingsPath: workflowSettingsPath(agentDir) }), services, workflowTool, shutdownHandlers };
|
|
@@ -304,26 +322,48 @@ class CliProgress {
|
|
|
304
322
|
#lines = 0;
|
|
305
323
|
#frame = 0;
|
|
306
324
|
#run: PersistedRun | undefined;
|
|
325
|
+
#runId: string | undefined;
|
|
326
|
+
#runtimeStartedAt = 0;
|
|
327
|
+
#runtimeBaseMs = 0;
|
|
307
328
|
#timer: ReturnType<typeof setInterval> | undefined;
|
|
308
329
|
#interactive: boolean;
|
|
309
330
|
#styles: WorkflowProgressStyles;
|
|
310
|
-
constructor(private readonly stderr: (text: string) => void, tty: boolean) {
|
|
331
|
+
constructor(private readonly stderr: (text: string) => void, tty: boolean, private readonly onRunId: (runId: string) => void) {
|
|
311
332
|
this.#interactive = tty && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
|
|
312
333
|
this.#styles = terminalProgressStyles(this.#interactive);
|
|
313
334
|
}
|
|
314
335
|
update(run: PersistedRun): void {
|
|
315
|
-
|
|
316
|
-
|
|
336
|
+
if (this.#runId !== run.id) {
|
|
337
|
+
this.#runId = run.id;
|
|
338
|
+
this.onRunId(run.id);
|
|
339
|
+
this.#runtimeStartedAt = Date.now();
|
|
340
|
+
this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
|
|
341
|
+
} else if (this.#run && this.#run.state !== "running" && run.state === "running") {
|
|
342
|
+
this.#runtimeStartedAt = Date.now();
|
|
343
|
+
this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
|
|
344
|
+
}
|
|
317
345
|
this.#run = run;
|
|
346
|
+
if (!this.#interactive) {
|
|
347
|
+
this.#timer ??= setInterval(() => { this.render(); }, 1000);
|
|
348
|
+
this.#timer.unref();
|
|
349
|
+
this.render();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
318
352
|
this.#timer ??= setInterval(() => { this.render(); }, 80);
|
|
319
353
|
this.#timer.unref();
|
|
320
354
|
this.render();
|
|
321
355
|
}
|
|
322
356
|
render(): void {
|
|
323
357
|
if (!this.#run) return;
|
|
358
|
+
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) } };
|
|
359
|
+
if (!this.#interactive) {
|
|
360
|
+
const stable = formatWorkflowProgress(run, "◇", this.#styles);
|
|
361
|
+
if (stable !== this.#lastStable) { this.#lastStable = stable; this.stderr(`${stable}\n`); }
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
324
364
|
const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
|
|
325
365
|
const width = process.stderr.columns || 80;
|
|
326
|
-
const text = truncateWorkflowProgress(formatWorkflowProgress(
|
|
366
|
+
const text = truncateWorkflowProgress(formatWorkflowProgress(run, spinner, this.#styles), width).join("\n");
|
|
327
367
|
this.stderr(`${this.#lines ? `\x1b[${String(this.#lines)}A` : ""}${this.#lines ? "" : "\x1b[?25l"}\x1b[0J${text}\n`);
|
|
328
368
|
this.#lines = text.split("\n").length;
|
|
329
369
|
}
|
|
@@ -334,16 +374,21 @@ class CliProgress {
|
|
|
334
374
|
}
|
|
335
375
|
}
|
|
336
376
|
|
|
337
|
-
|
|
377
|
+
type CliWorkflowResult = { value: JsonValue; runId?: string };
|
|
378
|
+
async function invokeWorkflow(fn: WorkflowCatalogFunction, args: Record<string, JsonValue>, runtime: WorkflowRuntime, options: WorkflowIo, context: unknown): Promise<CliWorkflowResult> {
|
|
338
379
|
if (!Value.Check(fn.input, args)) throw new Error(`Invalid input for ${fn.name}`);
|
|
339
|
-
|
|
380
|
+
let announcedRunId: string | undefined;
|
|
381
|
+
const announceRunId = (runId: string) => { if (announcedRunId === runId) return; announcedRunId = runId; options.stderr(`Run ID: ${runId}\n`); };
|
|
382
|
+
const progress = new CliProgress(options.stderr, options.isTTY ?? process.stderr.isTTY, announceRunId);
|
|
340
383
|
try {
|
|
341
384
|
const result = await runtime.workflowTool.execute(randomUUID(), { workflow: fn.name, args, foreground: true }, options.signal, (update: unknown) => { if (object(update) && object(update.details) && object(update.details.run)) progress.update(update.details.run as unknown as PersistedRun); }, context);
|
|
342
385
|
const details = object(result.details) ? result.details : {};
|
|
343
|
-
|
|
386
|
+
const runId = typeof details.runId === "string" ? details.runId : undefined;
|
|
387
|
+
if (runId) announceRunId(runId);
|
|
388
|
+
if (has(details, "value")) return { value: details.value as JsonValue, ...(runId ? { runId } : {}) };
|
|
344
389
|
const first = result.content[0];
|
|
345
390
|
if (!first || first.type !== "text") throw new Error("Workflow returned no result");
|
|
346
|
-
try { return parseJsonInput(first.text); } catch { throw new Error("Workflow returned invalid JSON"); }
|
|
391
|
+
try { return { value: parseJsonInput(first.text), ...(runId ? { runId } : {}) }; } catch { throw new Error("Workflow returned invalid JSON"); }
|
|
347
392
|
} finally {
|
|
348
393
|
progress.finish();
|
|
349
394
|
}
|
|
@@ -385,8 +430,8 @@ async function runWorkflowCli(rawArgs: readonly string[], options: WorkflowIo):
|
|
|
385
430
|
if (!fn) throw new Error(`Unknown workflow function: ${name}`);
|
|
386
431
|
if (help) { options.write(formatWorkflowCliHelp(fn)); return 0; }
|
|
387
432
|
const input = parseWorkflowCliArgs(fn.input, args.slice(1));
|
|
388
|
-
const
|
|
389
|
-
options.write(`${JSON.stringify(value)}\n`);
|
|
433
|
+
const result = await invokeWorkflow(fn, input, runtime, options, context);
|
|
434
|
+
options.write(`${JSON.stringify(result.value)}\n`);
|
|
390
435
|
return 0;
|
|
391
436
|
});
|
|
392
437
|
}
|
|
@@ -394,6 +439,7 @@ async function runWorkflowCli(rawArgs: readonly string[], options: WorkflowIo):
|
|
|
394
439
|
async function exportWorkflowCli(rawArgs: readonly string[], options: WorkflowIo): Promise<number> {
|
|
395
440
|
const parsed = stripTrustOptions(rawArgs);
|
|
396
441
|
const args = parsed.args;
|
|
442
|
+
if (args.includes("--bundle")) return bundleWorkflowCli(args.filter((arg) => arg !== "--bundle"), options);
|
|
397
443
|
if (!args.length || args[0] === "--help" || args[0] === "-h") { options.write(exportUsage()); return args.length ? 0 : 1; }
|
|
398
444
|
const workflowName = args[0] as string;
|
|
399
445
|
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
|
|
@@ -429,6 +475,62 @@ async function exportWorkflowCli(rawArgs: readonly string[], options: WorkflowIo
|
|
|
429
475
|
});
|
|
430
476
|
}
|
|
431
477
|
|
|
478
|
+
async function bundleWorkflowCli(rawArgs: readonly string[], options: WorkflowIo): Promise<number> {
|
|
479
|
+
const parsed = stripTrustOptions(rawArgs);
|
|
480
|
+
const args = parsed.args;
|
|
481
|
+
if (!args.length || args[0] === "--help" || args[0] === "-h") { options.write(bundleUsage()); return args.length ? 0 : 1; }
|
|
482
|
+
const workflowName = args[0] as string;
|
|
483
|
+
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
|
|
484
|
+
let name: string | undefined;
|
|
485
|
+
let output: string | undefined;
|
|
486
|
+
let force = false;
|
|
487
|
+
const requirements = { roles: [] as string[], aliases: [] as string[], tools: [] as string[], commands: [] as string[], environment: [] as string[] };
|
|
488
|
+
const resources = { extensions: [] as string[], skills: [] as string[], static: [] as string[], dependencies: [] as string[] };
|
|
489
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
490
|
+
const arg = args[index] as string;
|
|
491
|
+
if (arg === "--force") { force = true; continue; }
|
|
492
|
+
const equals = arg.indexOf("=");
|
|
493
|
+
const option = equals >= 0 ? arg.slice(0, equals) : arg;
|
|
494
|
+
const requirementOptions = { "--role": "roles", "--alias": "aliases", "--tool": "tools", "--command": "commands", "--environment": "environment" } as const;
|
|
495
|
+
const resourceOptions = { "--extension": "extensions", "--skill": "skills", "--resource": "static", "--dependency": "dependencies" } as const;
|
|
496
|
+
if (option === "--name" || option === "--output" || Object.prototype.hasOwnProperty.call(requirementOptions, option) || Object.prototype.hasOwnProperty.call(resourceOptions, option)) {
|
|
497
|
+
const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
|
|
498
|
+
if (!value) throw new Error(`Missing value for ${option}`);
|
|
499
|
+
if (option === "--name") name = value;
|
|
500
|
+
else if (option === "--output") output = value;
|
|
501
|
+
else if (Object.prototype.hasOwnProperty.call(requirementOptions, option)) requirements[requirementOptions[option as keyof typeof requirementOptions]].push(value);
|
|
502
|
+
else resources[resourceOptions[option as keyof typeof resourceOptions]].push(value);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (arg === "--help" || arg === "-h") { options.write(bundleUsage()); return 0; }
|
|
506
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
507
|
+
}
|
|
508
|
+
const fn = runtime.catalog.functions.find((candidate) => candidate.name === workflowName);
|
|
509
|
+
if (!fn) throw new Error(`Unknown workflow function: ${workflowName}`);
|
|
510
|
+
const registered = registeredWorkflowFunctions()[workflowName];
|
|
511
|
+
if (!registered) throw new Error(`Workflow ${workflowName} is not exportable because its registered implementation is unavailable`);
|
|
512
|
+
const definitions = requirements.roles.length ? loadAgentDefinitions(options.cwd ?? process.cwd(), options.agentDir ?? getAgentDir(), runtime.services.settingsManager.isProjectTrusted()) : {};
|
|
513
|
+
const roles = Object.fromEntries(requirements.roles.map((role) => {
|
|
514
|
+
if (!role || role === "." || role === ".." || role.includes("/") || role.includes("\\")) throw new Error(`Invalid role name for bundle: ${role}`);
|
|
515
|
+
const definition = definitions[role];
|
|
516
|
+
if (!definition) throw new Error(`Unknown role for bundle: ${role}`);
|
|
517
|
+
return [role, definition];
|
|
518
|
+
}));
|
|
519
|
+
const command = commandName(name ?? kebabCase(workflowName));
|
|
520
|
+
if (!command) throw new Error("Command name must be a non-empty name without path separators");
|
|
521
|
+
const destination = output ?? join(homedir(), ".local", "share", "pi-extensible-workflows", "bundles", command);
|
|
522
|
+
const aliasTargets = Object.fromEntries(requirements.aliases.flatMap((name) => {
|
|
523
|
+
const target = runtime.catalog.modelAliases?.[name];
|
|
524
|
+
return typeof target === "string" ? [[name, target]] : [];
|
|
525
|
+
}));
|
|
526
|
+
const selectedResources = Object.values(resources).some((entries) => entries.length) ? resources : undefined;
|
|
527
|
+
writePortableWorkflowBundle({ destination, command, workflow: fn, functionSource: registered.run.toString(), requirements, aliasTargets, roles, ...(selectedResources ? { resources: selectedResources } : {}), piVersion: portablePiVersion(), engineVersion: portableEngineVersion(), force });
|
|
528
|
+
options.write(`Bundled ${workflowName} at ${destination}\n`);
|
|
529
|
+
options.write(`Run ${join(destination, command)} setup before launching the workflow.\n`);
|
|
530
|
+
return 0;
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
432
534
|
export async function runCli(args: readonly string[], options: CliOptions = {}, write: (text: string) => void = (text) => { process.stdout.write(text); }): Promise<number> {
|
|
433
535
|
const stderr = options.stderr ?? ((text: string) => { process.stderr.write(text); });
|
|
434
536
|
if (args[0] === "doctor" && args.length === 1) {
|
|
@@ -446,8 +548,13 @@ export async function runCli(args: readonly string[], options: CliOptions = {},
|
|
|
446
548
|
return doctorCleanupExitCode(report);
|
|
447
549
|
} catch (error) { stderr(`Error: ${error instanceof Error ? error.message : String(error)}\n`); return 1; }
|
|
448
550
|
}
|
|
449
|
-
if (args[0] === "inspect"
|
|
450
|
-
try {
|
|
551
|
+
if (args[0] === "inspect") {
|
|
552
|
+
try {
|
|
553
|
+
const parsed = parseInspectArgs(args.slice(1));
|
|
554
|
+
if (options.inspect) await options.inspect(parsed.sessionId, parsed.mode, parsed.failedOnly);
|
|
555
|
+
else await runSessionInspector(parsed.sessionId, parsed.mode, options.cwd ?? process.cwd(), undefined, write, parsed.failedOnly);
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
451
558
|
catch (error) { write(`Error: ${error instanceof Error ? error.message : String(error)}\n`); return 1; }
|
|
452
559
|
}
|
|
453
560
|
if (args[0] === "transcript" && args.length === 2) {
|
|
@@ -457,13 +564,14 @@ export async function runCli(args: readonly string[], options: CliOptions = {},
|
|
|
457
564
|
return 0;
|
|
458
565
|
} catch (error) { write(`Error: ${error instanceof Error ? error.message : String(error)}\n`); return 1; }
|
|
459
566
|
}
|
|
460
|
-
if (args[0] === "run" || args[0] === "export") {
|
|
567
|
+
if (args[0] === "bundle" || args[0] === "run" || args[0] === "export") {
|
|
461
568
|
try {
|
|
462
|
-
const workflowOptions: WorkflowIo = { 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 } : {}) };
|
|
569
|
+
const workflowOptions: WorkflowIo = { 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] } : {}) };
|
|
570
|
+
if (args[0] === "bundle") return await bundleWorkflowCli(args.slice(1), workflowOptions);
|
|
463
571
|
return args[0] === "run" ? await runWorkflowCli(args.slice(1), workflowOptions) : await exportWorkflowCli(args.slice(1), workflowOptions);
|
|
464
572
|
} catch (error) { stderr(`Error: ${error instanceof Error ? error.message : String(error)}\n`); return 1; }
|
|
465
573
|
}
|
|
466
|
-
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");
|
|
574
|
+
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");
|
|
467
575
|
return 1;
|
|
468
576
|
}
|
|
469
577
|
|
package/src/doctor-cleanup.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, Run
|
|
|
11
11
|
const TERMINAL_STATES = new Set<RunState>(["completed", "failed", "stopped"]);
|
|
12
12
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
13
13
|
const REQUIRED_RUN_FILES = ["workflow.js", "state.json", "snapshot.json", "journal.json", "ownership.json", "worktrees.json", "borrowed-worktrees.json", "system-prompts.json"] as const;
|
|
14
|
-
const OPTIONAL_RUN_FILES = new Set(["result.json"]);
|
|
14
|
+
const OPTIONAL_RUN_FILES = new Set(["result.json", "summary.json"]);
|
|
15
15
|
const RUN_DIRECTORIES = new Set(["worktrees"]);
|
|
16
16
|
const RUN_FILES = new Set<string>([...REQUIRED_RUN_FILES, ...OPTIONAL_RUN_FILES]);
|
|
17
17
|
const AGENT_STATES = new Set(["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"]);
|
|
@@ -46,12 +46,43 @@ function accounting(value: unknown, label: string): void { if (!object(value)) t
|
|
|
46
46
|
function resourceExclusions(value: unknown, label: string): void { if (value === undefined) return; if (!object(value)) throw new Error(`${label} is invalid`); if (value.skills !== undefined) stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined) stringList(value.extensions, `${label}.extensions`); }
|
|
47
47
|
function agentDefinition(value: unknown, label: string): void { if (!object(value)) throw new Error(`${label} is invalid`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.description, `${label}.description`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking as string)) throw new Error(`${label}.thinking is invalid`); if (value.tools !== undefined) stringList(value.tools, `${label}.tools`); resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`); }
|
|
48
48
|
function validateScheduledOptions(value: unknown, label: string): void { if (!object(value) || typeof value.label !== "string" || !value.label || typeof value.cwd !== "string" || !value.cwd) throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking as string)) throw new Error(`${label}.thinking is invalid`); optionalString(value.role, `${label}.role`); if (value.schema !== undefined) validateSchema(value.schema, `${label}.schema`); if (value.retries !== undefined) nonNegativeInteger(value.retries, `${label}.retries`); if (value.timeoutMs !== undefined && value.timeoutMs !== null) positiveInteger(value.timeoutMs, `${label}.timeoutMs`); if (value.agentOptions !== undefined && (!object(value.agentOptions) || !jsonValue(value.agentOptions))) throw new Error(`${label}.agentOptions is invalid`); if (value.agentIdentity !== undefined) { if (!object(value.agentIdentity) || !Array.isArray(value.agentIdentity.structuralPath) || value.agentIdentity.structuralPath.some((part) => typeof part !== "string") || typeof value.agentIdentity.callSite !== "string") throw new Error(`${label}.agentIdentity is invalid`); positiveInteger(value.agentIdentity.occurrence, `${label}.agentIdentity.occurrence`); optionalString(value.agentIdentity.parentBreadcrumb, `${label}.agentIdentity.parentBreadcrumb`); optionalString(value.agentIdentity.worktreeOwner, `${label}.agentIdentity.worktreeOwner`); } }
|
|
49
|
-
function
|
|
49
|
+
function validateSessionReference(value: unknown, label: string): void {
|
|
50
|
+
if (!object(value) || typeof value.transport !== "string" || !value.transport || typeof value.sessionId !== "string" || !value.sessionId || (value.locator !== undefined && !jsonValue(value.locator))) throw new Error(`${label} is invalid`);
|
|
51
|
+
}
|
|
52
|
+
function validateAgentSetup(value: unknown, label: string): void {
|
|
53
|
+
if (!object(value)) throw new Error(`${label} is invalid`);
|
|
54
|
+
stringList(value.hookNames, `${label}.hookNames`);
|
|
55
|
+
model(value.model, `${label}.model`);
|
|
56
|
+
stringList(value.tools, `${label}.tools`);
|
|
57
|
+
if (typeof value.cwd !== "string" || !value.cwd) throw new Error(`${label}.cwd is invalid`);
|
|
58
|
+
resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
|
|
59
|
+
}
|
|
60
|
+
function validateAgent(value: unknown, label: string): void {
|
|
61
|
+
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)) throw new Error(`${label} is invalid`);
|
|
62
|
+
optionalString(value.systemPrompt, `${label}.systemPrompt`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.label, `${label}.label`); optionalString(value.parentId, `${label}.parentId`);
|
|
63
|
+
if (value.structuralPath !== undefined) 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`);
|
|
64
|
+
model(value.model, `${label}.model`); stringList(value.tools, `${label}.tools`); nonNegativeInteger(value.attempts, `${label}.attempts`);
|
|
65
|
+
if (value.attemptDetails !== undefined) {
|
|
66
|
+
if (!Array.isArray(value.attemptDetails)) throw new Error(`${label}.attemptDetails is invalid`);
|
|
67
|
+
for (const [index, attempt] of value.attemptDetails.entries()) {
|
|
68
|
+
const at = `${label}.attemptDetails[${String(index)}]`;
|
|
69
|
+
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")) throw new Error(`${at} is invalid`);
|
|
70
|
+
validateAgentSetup(attempt.setup, `${at}.setup`);
|
|
71
|
+
if (attempt.session !== undefined) { validateSessionReference(attempt.session, `${at}.session`); if ((attempt.session as Record<string, unknown>).transport !== attempt.transport) throw new Error(`${at}.session transport does not match attempt transport`); }
|
|
72
|
+
accounting(attempt.accounting, `${at}.accounting`);
|
|
73
|
+
if (attempt.error !== undefined && (!object(attempt.error) || typeof attempt.error.code !== "string" || typeof attempt.error.message !== "string")) throw new Error(`${at}.error is invalid`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (value.accounting !== undefined) accounting(value.accounting, `${label}.accounting`);
|
|
77
|
+
if (value.toolCalls !== undefined) { if (!Array.isArray(value.toolCalls)) throw new Error(`${label}.toolCalls is invalid`); for (const [index, call] of value.toolCalls.entries()) if (!object(call) || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || !["running", "completed", "failed"].includes(call.state as string)) throw new Error(`${label}.toolCalls[${String(index)}] is invalid`); }
|
|
78
|
+
if (value.activity !== undefined) { if (!object(value.activity) || !["reasoning", "tool", "text"].includes(value.activity.kind as string) || typeof value.activity.text !== "string") throw new Error(`${label}.activity is invalid`); }
|
|
79
|
+
if (value.lastEventAt !== undefined) finiteNumber(value.lastEventAt, `${label}.lastEventAt`);
|
|
80
|
+
}
|
|
50
81
|
function validateUsage(value: unknown, label: string): void { if (!object(value)) throw new Error(`${label} is invalid`); for (const key of ["tokens", "costUsd", "durationMs", "agentLaunches"]) finiteNumber(value[key], `${label}.${key}`); }
|
|
51
82
|
function validateBudgetEvents(value: unknown): void { if (value === undefined) return; if (!Array.isArray(value)) throw new Error("Persisted budget events are invalid"); for (const [index, event] of value.entries()) { const label = `budgetEvents[${String(index)}]`; if (!object(event) || typeof event.type !== "string" || !BUDGET_EVENT_TYPES.has(event.type) || !Number.isSafeInteger(event.budgetVersion) || Number(event.budgetVersion) < 1 || !Array.isArray(event.dimensions) || event.dimensions.some((dimension) => typeof dimension !== "string" || !BUDGET_DIMENSIONS.has(dimension)) || typeof event.at !== "number" || !Number.isFinite(event.at) || event.limits === undefined) throw new Error(`${label} is invalid`); validateUsage(event.usage, `${label}.usage`); validateBudget(event.limits); } }
|
|
52
83
|
function validateRunRecord(run: PersistedRun): void {
|
|
53
84
|
const value = run as unknown;
|
|
54
|
-
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 as RunState) || !Array.isArray(value.agents) || !Array.isArray(value.nativeSessions)) throw new Error("Persisted run state is invalid");
|
|
85
|
+
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 as RunState) || !Array.isArray(value.agents) || !Array.isArray(value.agentSessions) || Object.hasOwn(value, "nativeSessions")) throw new Error("Persisted run state is invalid");
|
|
55
86
|
const agents = value.agents as Record<string, unknown>[];
|
|
56
87
|
agents.forEach((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); });
|
|
57
88
|
const agentIds = new Set<string>();
|
|
@@ -72,8 +103,10 @@ function validateRunRecord(run: PersistedRun): void {
|
|
|
72
103
|
parent = typeof parentAgent?.parentId === "string" ? parentAgent.parentId : undefined;
|
|
73
104
|
}
|
|
74
105
|
}
|
|
75
|
-
|
|
106
|
+
const sessions = value.agentSessions as unknown[];
|
|
107
|
+
for (const [index, session] of sessions.entries()) validateSessionReference(session, `agentSessions[${String(index)}]`);
|
|
76
108
|
optionalString(value.parentRunId, "Persisted parent run");
|
|
109
|
+
optionalString(value.failedAt, "Persisted failed path");
|
|
77
110
|
if (value.retry !== undefined) {
|
|
78
111
|
if (!object(value.retry) || typeof value.retry.sourceRunId !== "string" || !value.retry.sourceRunId || typeof value.retry.lineageRootRunId !== "string" || !value.retry.lineageRootRunId) throw new Error("Persisted retry provenance is invalid");
|
|
79
112
|
const sourceRunId = value.retry.sourceRunId;
|
|
@@ -145,6 +178,7 @@ async function validateRunArtifacts(store: RunStore, workflowScript: string, sta
|
|
|
145
178
|
if (!object(record) || typeof record.id !== "string" || !record.id || ownershipIds.has(record.id) || typeof record.label !== "string" || !record.label || typeof record.state !== "string" || !SCHEDULER_STATES.has(record.state)) throw new Error(`${label} is invalid`);
|
|
146
179
|
ownershipIds.add(record.id);
|
|
147
180
|
optionalString(record.parentId, `${label}.parentId`);
|
|
181
|
+
optionalString(record.prompt, `${label}.prompt`);
|
|
148
182
|
validateScheduledOptions(record.options, `${label}.options`);
|
|
149
183
|
}
|
|
150
184
|
for (const record of ownership) if (object(record) && record.parentId !== undefined && (typeof record.parentId !== "string" || !ownershipIds.has(record.parentId))) throw new Error("Persisted ownership parent is missing");
|
|
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { validateWorkflowLaunch, WorkflowError, WORKFLOW_TOOL_DESCRIPTION, WORKFLOW_TOOL_LABEL, WORKFLOW_TOOL_PARAMETERS, WORKFLOW_TOOL_PROMPT_SNIPPET, type WorkflowValidationParameters } from "./index.js";
|
|
5
|
+
import { validateWorkflowLaunch, WorkflowError, WORKFLOW_RETRY_PARAMETERS, WORKFLOW_TOOL_DESCRIPTION, WORKFLOW_TOOL_LABEL, WORKFLOW_TOOL_PARAMETERS, WORKFLOW_TOOL_PROMPT_SNIPPET, type WorkflowValidationParameters } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export const CAPTURE_IDENTITY = "pi-extensible-workflows-eval-capture-v1";
|
|
8
8
|
export const CAPTURE_ERROR_PREFIX = `${CAPTURE_IDENTITY}:`;
|
|
@@ -51,4 +51,18 @@ export default function evalCaptureExtension(pi: ExtensionAPI): void {
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
} as never);
|
|
54
|
+
pi.registerTool({
|
|
55
|
+
name: "workflow_retry",
|
|
56
|
+
label: "Workflow Retry",
|
|
57
|
+
description: "Capture a recovery selection without executing it",
|
|
58
|
+
parameters: WORKFLOW_RETRY_PARAMETERS,
|
|
59
|
+
async execute(_id: string, params: { runId: string }) {
|
|
60
|
+
if (!params.runId.trim()) throw new WorkflowError("RESUME_INCOMPATIBLE", `${CAPTURE_ERROR_PREFIX}RESUME_INCOMPATIBLE: workflow_retry requires an explicit run ID`);
|
|
61
|
+
const selection = { tool: "workflow_retry", arguments: { runId: params.runId } };
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text" as const, text: JSON.stringify({ captured: true, ...selection }) }],
|
|
64
|
+
details: { captured: true, captureIdentity: CAPTURE_IDENTITY, realWorkflowAgentsLaunched: 0, selection },
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
} as never);
|
|
54
68
|
}
|
package/src/execution.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
6
|
import { RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
7
7
|
import type { AgentAttempt } from "./agent-execution.js";
|
|
8
|
-
import type { AgentIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowBridge, WorkflowErrorCode, WorkflowExecution } from "./types.js";
|
|
8
|
+
import type { AgentIdentity, AgentAttemptSummary, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowAgentSessionReference, WorkflowBridge, WorkflowErrorCode, WorkflowExecution } from "./types.js";
|
|
9
9
|
import { WorkflowError } from "./types.js";
|
|
10
10
|
import { asWorkflowError, errorText, fail, isWorkflowAuthored, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
|
|
11
11
|
import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validateShellOptions } from "./validation.js";
|
|
@@ -13,6 +13,7 @@ import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validat
|
|
|
13
13
|
export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
14
14
|
type WorkerErrorShape = { code?: string; message: string; authored?: boolean; failedAt?: string };
|
|
15
15
|
export const HEARTBEAT_TIMEOUT_MS = 5000;
|
|
16
|
+
const HEARTBEAT_INTERVAL_MS = 1000;
|
|
16
17
|
|
|
17
18
|
const OUTCOME_ERRORS = new Set<string>(["AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID"]);
|
|
18
19
|
const WORK_RESULT_BRAND = "__workResult";
|
|
@@ -64,7 +65,7 @@ process.on("message", raw => {
|
|
|
64
65
|
if (message.ok) request.resolve(message.value);
|
|
65
66
|
else request.reject(workflowError(message.error));
|
|
66
67
|
});
|
|
67
|
-
const heartbeat = setInterval(() => send({ type: "heartbeat" }),
|
|
68
|
+
const heartbeat = setInterval(() => send({ type: "heartbeat" }), ${HEARTBEAT_INTERVAL_MS});
|
|
68
69
|
send({ type: "heartbeat" });
|
|
69
70
|
const BRAND = "${WORK_RESULT_BRAND}";
|
|
70
71
|
const workError = (code, message) => Object.assign(new Error(message), { code });
|
|
@@ -416,7 +417,16 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
416
417
|
const controller = new AbortController();
|
|
417
418
|
let settled = false;
|
|
418
419
|
let rejectResult: (error: WorkflowError) => void = () => undefined;
|
|
419
|
-
let
|
|
420
|
+
let lastHeartbeatAt = performance.now();
|
|
421
|
+
let lastWatchdogCheckAt = lastHeartbeatAt;
|
|
422
|
+
const watchdog = setInterval(() => {
|
|
423
|
+
const now = performance.now();
|
|
424
|
+
// A check delayed enough to bridge the normal heartbeat margin makes elapsed time unreliable.
|
|
425
|
+
const watchdogCheckWasDelayed = now - lastWatchdogCheckAt >= HEARTBEAT_TIMEOUT_MS - HEARTBEAT_INTERVAL_MS;
|
|
426
|
+
lastWatchdogCheckAt = now;
|
|
427
|
+
if (watchdogCheckWasDelayed) { lastHeartbeatAt = now; return; }
|
|
428
|
+
if (now - lastHeartbeatAt >= HEARTBEAT_TIMEOUT_MS) stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat");
|
|
429
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
420
430
|
const result = new Promise<JsonValue>((resolve, reject) => {
|
|
421
431
|
rejectResult = reject;
|
|
422
432
|
child.on("message", (raw: unknown) => {
|
|
@@ -424,7 +434,7 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
424
434
|
if (typeof raw !== "string" || Buffer.byteLength(raw) > RPC_LIMIT_BYTES) fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
|
|
425
435
|
const message = JSON.parse(raw) as { type?: string; id?: number; method?: string; args?: JsonValue[]; ok?: boolean; value?: JsonValue; error?: WorkerErrorShape };
|
|
426
436
|
if (!jsonValue(message)) fail("RPC_LIMIT_EXCEEDED", "Worker RPC must contain JSON-compatible values");
|
|
427
|
-
if (message.type === "heartbeat") {
|
|
437
|
+
if (message.type === "heartbeat") { lastHeartbeatAt = performance.now(); return; }
|
|
428
438
|
if (message.type === "result") { encoded(message.value); finish(); resolve(message.value ?? null); return; }
|
|
429
439
|
if (message.type === "error") { finish(); reject(workflowErrorFromWorker(message.error ?? { code: "INTERNAL_ERROR", message: "Worker failed" })); return; }
|
|
430
440
|
if (message.type === "rpc" && message.id !== undefined) void handleRpc(message.id, message.method ?? "", message.args ?? []);
|
|
@@ -439,7 +449,7 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
439
449
|
setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 1000).unref();
|
|
440
450
|
}
|
|
441
451
|
}
|
|
442
|
-
function finish() { settled = true;
|
|
452
|
+
function finish() { settled = true; clearInterval(watchdog); signal?.removeEventListener("abort", cancel); killChild(); rmSync(childDir, { recursive: true, force: true }); }
|
|
443
453
|
function stop(code: WorkflowErrorCode, message: string) { if (settled) return; controller.abort(); finish(); rejectResult(new WorkflowError(code, message)); }
|
|
444
454
|
function branded(result: Record<string, JsonValue>): JsonValue { return { ...result, [WORK_RESULT_BRAND]: true }; }
|
|
445
455
|
async function handleRpc(id: number, method: string, values: JsonValue[]) {
|
|
@@ -520,18 +530,19 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
520
530
|
if (signal?.aborted) cancel(); else signal?.addEventListener("abort", cancel, { once: true });
|
|
521
531
|
return { result, cancel };
|
|
522
532
|
}
|
|
523
|
-
function
|
|
524
|
-
return {
|
|
533
|
+
function attemptSummary(attempt: AgentAttempt, accounting = attempt.accounting): AgentAttemptSummary {
|
|
534
|
+
return { attempt: attempt.attempt, transport: attempt.transport, ...(attempt.session ? { session: attempt.session } : {}), ...(attempt.error ? { error: attempt.error } : {}), accounting, setup: attempt.setup };
|
|
525
535
|
}
|
|
526
|
-
|
|
527
|
-
export async function persistActiveAgentAttempt(store: RunStore, id: string, active: Pick<AgentAttempt, "attempt" | "sessionId" | "sessionFile" | "setup">): Promise<void> {
|
|
536
|
+
export async function persistActiveAgentAttempt(store: RunStore, id: string, active: AgentAttempt): Promise<void> {
|
|
528
537
|
await store.updateState((run) => {
|
|
529
538
|
const agent = run.agents.find((candidate) => candidate.id === id);
|
|
530
539
|
if (!agent) throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
|
|
531
540
|
const accounting = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
|
|
541
|
+
const detail = attemptSummary(active, accounting);
|
|
542
|
+
const details = [...(agent.attemptDetails ?? []).filter((candidate) => candidate.attempt !== active.attempt), detail];
|
|
543
|
+
const session = active.session;
|
|
544
|
+
const agentSessions = session && !run.agentSessions.some(({ transport, sessionId }) => transport === session.transport && sessionId === session.sessionId) ? [...run.agentSessions, session] : run.agentSessions;
|
|
545
|
+
return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: Math.max(candidate.attempts, active.attempt), attemptDetails: details } : candidate), agentSessions };
|
|
535
546
|
});
|
|
536
547
|
}
|
|
537
548
|
|
|
@@ -540,9 +551,10 @@ export async function persistAgentAttempts(store: RunStore, id: string, attempts
|
|
|
540
551
|
const agent = run.agents.find((candidate) => candidate.id === id);
|
|
541
552
|
if (!agent) throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
|
|
542
553
|
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 });
|
|
543
|
-
const attemptDetails = attempts.map((
|
|
544
|
-
const
|
|
545
|
-
|
|
554
|
+
const attemptDetails = attempts.map((attempt) => attemptSummary(attempt));
|
|
555
|
+
const sessions = attempts.map((attempt) => attempt.session).filter((session): session is WorkflowAgentSessionReference => session !== undefined);
|
|
556
|
+
const sessionKeys = new Set(sessions.map(({ transport, sessionId }) => `${transport}:${sessionId}`));
|
|
557
|
+
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] };
|
|
546
558
|
});
|
|
547
559
|
}
|
|
548
560
|
|