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.
Files changed (47) hide show
  1. package/README.md +2 -31
  2. package/dist/src/agent-execution.d.ts +67 -9
  3. package/dist/src/agent-execution.js +385 -141
  4. package/dist/src/bundles.d.ts +53 -0
  5. package/dist/src/bundles.js +457 -0
  6. package/dist/src/cli.d.ts +3 -1
  7. package/dist/src/cli.js +156 -22
  8. package/dist/src/doctor-cleanup.js +68 -31
  9. package/dist/src/eval-capture-extension.js +16 -1
  10. package/dist/src/execution.d.ts +1 -1
  11. package/dist/src/execution.js +29 -13
  12. package/dist/src/host.d.ts +12 -9
  13. package/dist/src/host.js +525 -195
  14. package/dist/src/index.d.ts +5 -4
  15. package/dist/src/index.js +3 -2
  16. package/dist/src/persistence.d.ts +43 -8
  17. package/dist/src/persistence.js +54 -0
  18. package/dist/src/registry.d.ts +3 -2
  19. package/dist/src/registry.js +16 -4
  20. package/dist/src/session-inspector.d.ts +12 -2
  21. package/dist/src/session-inspector.js +50 -24
  22. package/dist/src/types.d.ts +141 -60
  23. package/dist/src/types.js +1 -0
  24. package/dist/src/validation.js +28 -7
  25. package/dist/src/workflow-artifacts.d.ts +1 -0
  26. package/dist/src/workflow-artifacts.js +1 -0
  27. package/dist/src/workflow-evals.d.ts +7 -1
  28. package/dist/src/workflow-evals.js +23 -3
  29. package/evals/cases/recovery-completed-worktree.yaml +17 -0
  30. package/evals/cases/recovery-failed-run.yaml +14 -0
  31. package/package.json +1 -1
  32. package/skills/pi-extensible-workflows/SKILL.md +13 -5
  33. package/src/agent-execution.ts +302 -107
  34. package/src/bundles.ts +471 -0
  35. package/src/cli.ts +130 -22
  36. package/src/doctor-cleanup.ts +38 -4
  37. package/src/eval-capture-extension.ts +15 -1
  38. package/src/execution.ts +27 -15
  39. package/src/host.ts +454 -175
  40. package/src/index.ts +5 -4
  41. package/src/persistence.ts +58 -4
  42. package/src/registry.ts +14 -5
  43. package/src/session-inspector.ts +49 -26
  44. package/src/types.ts +55 -30
  45. package/src/validation.ts +19 -6
  46. package/src/workflow-artifacts.ts +1 -0
  47. package/src/workflow-evals.ts +24 -3
@@ -1,4 +1,4 @@
1
- import type { AgentSessionEvent, CreateAgentSessionOptions, InlineExtension, SessionStats, ToolDefinition } from "@earendil-works/pi-coding-agent";
1
+ import type { CreateAgentSessionOptions, InlineExtension, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  export declare const RUN_STATES: readonly ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"];
3
3
  export declare const AGENT_STATES: readonly ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"];
4
4
  export declare const WORKFLOW_CALL_KINDS: readonly ["agent", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"];
@@ -83,6 +83,7 @@ export interface BudgetApprovalRequest {
83
83
  export interface WorkflowErrorShape {
84
84
  code: WorkflowErrorCode;
85
85
  message: string;
86
+ failedAt?: string;
86
87
  }
87
88
  export interface WorkflowEventBase {
88
89
  runId: string;
@@ -192,15 +193,16 @@ export interface AgentResourcePolicy {
192
193
  global: AgentResourceExclusions;
193
194
  project: AgentResourceExclusions;
194
195
  effective: AgentResourceExclusions;
195
- unmatchedSkills: string[];
196
- unmatchedExtensions: string[];
197
- excludedSkills?: string[];
198
- excludedExtensions?: string[];
196
+ unmatchedSkills: readonly string[];
197
+ unmatchedExtensions: readonly string[];
198
+ excludedSkills?: readonly string[];
199
+ excludedExtensions?: readonly string[];
199
200
  }
200
201
  export interface AgentActivity {
201
202
  kind: "reasoning" | "tool" | "text";
202
203
  text: string;
203
204
  }
205
+ export declare const WORKFLOW_AGENT_STALL_THRESHOLD_MS: number;
204
206
  export interface AgentAccounting {
205
207
  input: number;
206
208
  output: number;
@@ -222,22 +224,17 @@ export interface AgentSetupSummary {
222
224
  unmatchedExtensions: readonly string[];
223
225
  };
224
226
  }
227
+ export interface AgentAttemptError {
228
+ code: string;
229
+ message: string;
230
+ }
225
231
  export interface AgentAttemptSummary {
226
232
  attempt: number;
227
- sessionId: string;
228
- sessionFile: string;
229
- error?: {
230
- code: string;
231
- message: string;
232
- };
233
- accounting: {
234
- input: number;
235
- output: number;
236
- cacheRead: number;
237
- cacheWrite: number;
238
- cost: number;
239
- };
240
- setup?: AgentSetupSummary;
233
+ transport: string;
234
+ session?: WorkflowAgentSessionReference;
235
+ setup: AgentSetupSummary;
236
+ error?: AgentAttemptError;
237
+ accounting: AgentAccounting;
241
238
  }
242
239
  export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase {
243
240
  owner: string;
@@ -250,6 +247,8 @@ export interface WorkflowWorktreeReference {
250
247
  readonly branch: string;
251
248
  }
252
249
  export interface AgentRecord {
250
+ systemPrompt?: string;
251
+ prompt?: string;
253
252
  id: string;
254
253
  name: string;
255
254
  label?: string;
@@ -279,6 +278,14 @@ export interface AgentRecord {
279
278
  state: "running" | "completed" | "failed";
280
279
  }[];
281
280
  activity?: AgentActivity | undefined;
281
+ lastEventAt?: number;
282
+ }
283
+ export type WorkflowDeliveryMode = "foreground" | "background";
284
+ export type WorkflowDeliveryStatus = "attached" | "pending" | "delivered";
285
+ export interface WorkflowRunDelivery {
286
+ mode: WorkflowDeliveryMode;
287
+ state: WorkflowDeliveryStatus;
288
+ toolCallId?: string;
282
289
  }
283
290
  export interface WorkflowRunEvent {
284
291
  type: string;
@@ -301,31 +308,38 @@ export interface RunRecord {
301
308
  cwd: string;
302
309
  sessionId: string;
303
310
  state: RunState;
311
+ agentSessions: readonly WorkflowAgentSessionReference[];
304
312
  parentRunId?: string;
305
313
  retry?: WorkflowRetryProvenance;
306
314
  phase?: string;
307
315
  phaseHistory?: readonly WorkflowPhaseRecord[];
308
316
  agents: readonly AgentRecord[];
317
+ activeShells?: number;
309
318
  error?: WorkflowErrorShape;
319
+ failedAt?: string;
310
320
  budget?: WorkflowBudget;
311
321
  budgetVersion?: number;
312
322
  usage?: WorkflowBudgetUsage;
313
323
  budgetEvents?: readonly BudgetEvent[];
314
324
  events?: readonly WorkflowRunEvent[];
325
+ delivery?: WorkflowRunDelivery;
315
326
  }
316
327
  export declare const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
328
+ export type WorkflowLaunchMode = "foreground" | "background";
317
329
  export interface AgentDefinition {
318
330
  prompt?: string;
319
331
  description?: string;
320
332
  model?: string;
321
333
  thinking?: NonNullable<ModelSpec["thinking"]>;
322
334
  tools?: readonly string[];
335
+ overrideSystemPrompt?: boolean;
323
336
  disabledAgentResources?: AgentResourceExclusions;
324
337
  }
325
338
  export interface LaunchSnapshot {
326
339
  identityVersion?: number;
327
340
  launchKind?: "inline" | "function";
328
341
  functionName?: string;
342
+ launchMode?: WorkflowLaunchMode;
329
343
  script: string;
330
344
  args: JsonValue;
331
345
  metadata: WorkflowMetadata;
@@ -397,47 +411,65 @@ export interface WorkflowVariable {
397
411
  schema: JsonSchema;
398
412
  resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue;
399
413
  }
400
- type NativeAgentMessage = {
401
- role: string;
402
- content?: unknown;
403
- stopReason?: string;
404
- errorMessage?: string;
405
- usage?: {
406
- input: number;
407
- output: number;
408
- cacheRead: number;
409
- cacheWrite: number;
410
- cost: {
411
- total: number;
412
- };
413
- };
414
- };
415
- type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
416
- export interface NativeSession {
414
+ export interface WorkflowAgentSessionReference {
415
+ readonly transport: string;
417
416
  readonly sessionId: string;
418
- readonly sessionFile: string | undefined;
419
- readonly messages: readonly NativeAgentMessage[];
420
- getSessionStats(): NativeSessionStats;
421
- readonly systemPrompt?: string;
422
- readonly model?: {
423
- provider: string;
424
- model?: string;
425
- id?: string;
417
+ readonly locator?: JsonValue;
418
+ }
419
+ export interface WorkflowAgentSessionStats {
420
+ readonly tokens: {
421
+ readonly input: number;
422
+ readonly output: number;
423
+ readonly cacheRead: number;
424
+ readonly cacheWrite: number;
425
+ readonly total: number;
426
426
  };
427
- readonly agent?: {
428
- state: {
429
- tools: readonly {
430
- name: string;
431
- }[];
427
+ readonly cost: number;
428
+ }
429
+ export interface WorkflowAgentMessage {
430
+ readonly role: string;
431
+ readonly content?: unknown;
432
+ readonly stopReason?: string;
433
+ readonly errorMessage?: string;
434
+ readonly usage?: {
435
+ readonly input: number;
436
+ readonly output: number;
437
+ readonly cacheRead: number;
438
+ readonly cacheWrite: number;
439
+ readonly cost: {
440
+ readonly total: number;
432
441
  };
433
442
  };
434
- getLeafId?: () => string | null;
435
- getToolDefinitions?: () => unknown;
436
- subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
437
- prompt(text: string): Promise<void>;
438
- steer?(text: string): Promise<void>;
439
- abort?(): Promise<void>;
440
- dispose(): void;
443
+ }
444
+ export interface WorkflowAgentSessionState {
445
+ readonly model: ModelSpec;
446
+ readonly thinking?: ModelSpec["thinking"];
447
+ readonly tools: readonly string[];
448
+ readonly systemPrompt?: string;
449
+ }
450
+ export interface WorkflowAgentSessionEvent {
451
+ readonly type: string;
452
+ readonly state?: Readonly<WorkflowAgentSessionState>;
453
+ readonly message?: WorkflowAgentMessage;
454
+ readonly assistantMessageEvent?: {
455
+ readonly type: string;
456
+ };
457
+ readonly toolCallId?: string;
458
+ readonly toolName?: string;
459
+ readonly isError?: boolean;
460
+ }
461
+ export interface WorkflowAgentTurnResult {
462
+ readonly assistant?: WorkflowAgentMessage;
463
+ }
464
+ export interface WorkflowAgentSession {
465
+ readonly reference: WorkflowAgentSessionReference;
466
+ getState(): Readonly<WorkflowAgentSessionState>;
467
+ getSessionStats(): WorkflowAgentSessionStats;
468
+ subscribe(listener: (event: WorkflowAgentSessionEvent) => void): () => void;
469
+ prompt(text: string): Promise<WorkflowAgentTurnResult>;
470
+ steer(text: string): Promise<void>;
471
+ abort(): Promise<void>;
472
+ dispose(): Promise<void>;
441
473
  }
442
474
  type SessionTools = NonNullable<CreateAgentSessionOptions["tools"]>;
443
475
  type SessionCustomTools = NonNullable<CreateAgentSessionOptions["customTools"]>;
@@ -449,17 +481,44 @@ export interface SessionInput {
449
481
  agentDir?: string;
450
482
  customTools?: SessionCustomTools;
451
483
  resultTool?: ToolDefinition;
484
+ systemPrompt?: string;
452
485
  systemPromptAppend?: string;
453
486
  extensionFactories?: InlineExtension[];
487
+ additionalSkillPaths?: readonly string[];
454
488
  resourcePolicy?: AgentResourcePolicy;
455
489
  options?: AgentOptions;
456
490
  }
457
- export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
491
+ export interface PreparedAgentSession {
492
+ readonly cwd: string;
493
+ readonly model: ModelSpec;
494
+ readonly tools: readonly string[];
495
+ readonly sessionLabel: string;
496
+ readonly agentDir?: string;
497
+ readonly customTools?: readonly ToolDefinition[];
498
+ readonly resultTool?: ToolDefinition;
499
+ readonly options?: Readonly<Record<string, JsonValue>>;
500
+ readonly systemPrompt?: string;
501
+ readonly systemPromptAppend?: string;
502
+ readonly extensionFactories?: readonly InlineExtension[];
503
+ readonly additionalSkillPaths?: readonly string[];
504
+ readonly resourcePolicy?: Readonly<AgentResourcePolicy>;
505
+ }
506
+ export interface AgentTransportContext {
507
+ readonly run: Readonly<WorkflowRunContext>;
508
+ readonly identity: Readonly<AgentIdentity>;
509
+ readonly attempt: number;
510
+ readonly signal: AbortSignal;
511
+ }
512
+ export interface AgentTransport {
513
+ readonly id: string;
514
+ createSession(prepared: Readonly<PreparedAgentSession>, context: Readonly<AgentTransportContext>): Promise<WorkflowAgentSession>;
515
+ }
458
516
  export interface AgentSetup {
459
517
  prompt: string;
460
518
  options: AgentOptions;
461
519
  sessionInput: SessionInput;
462
- createSession: SessionFactory;
520
+ prepared: Readonly<PreparedAgentSession>;
521
+ transport: AgentTransport;
463
522
  }
464
523
  export interface AgentSetupContext {
465
524
  readonly run: Readonly<WorkflowRunContext>;
@@ -485,11 +544,32 @@ export interface WorkflowRoleDirectoryRegistration {
485
544
  path: string;
486
545
  extension: WorkflowExtensionMetadata;
487
546
  }
547
+ export interface AgentAttemptActionUi {
548
+ notify(message: string, level?: "info" | "warning" | "error"): void;
549
+ confirm(title: string, message: string): Promise<boolean>;
550
+ select(title: string, options: readonly string[]): Promise<string | undefined>;
551
+ input(title: string, placeholder?: string): Promise<string | undefined>;
552
+ }
553
+ export interface AgentAttemptActionContext {
554
+ readonly run: Readonly<RunRecord>;
555
+ readonly agent: Readonly<AgentRecord>;
556
+ readonly attempt: Readonly<AgentAttemptSummary>;
557
+ readonly session?: WorkflowAgentSessionReference;
558
+ readonly liveSession?: WorkflowAgentSession;
559
+ readonly signal: AbortSignal;
560
+ readonly ui: Readonly<AgentAttemptActionUi>;
561
+ }
562
+ export interface AgentAttemptAction {
563
+ readonly label: string;
564
+ visible(context: Readonly<AgentAttemptActionContext>): boolean;
565
+ run(context: Readonly<AgentAttemptActionContext>): void | Promise<void>;
566
+ }
488
567
  export interface WorkflowExtension extends WorkflowExtensionMetadata {
489
568
  functions?: Readonly<Record<string, WorkflowFunction>>;
490
569
  variables?: Readonly<Record<string, WorkflowVariable>>;
491
570
  modelAliases?: Readonly<Record<string, WorkflowModelAlias>>;
492
571
  agentSetupHooks?: Readonly<Record<string, AgentSetupHook>>;
572
+ agentAttemptActions?: Readonly<Record<string, AgentAttemptAction>>;
493
573
  roleDirectories?: readonly (string | URL)[];
494
574
  }
495
575
  export interface WorkflowJournal {
@@ -506,8 +586,8 @@ export interface WorkflowFailureAgent {
506
586
  role?: string;
507
587
  structuralPath: readonly string[];
508
588
  attempt: number;
509
- sessionId?: string;
510
- sessionFile?: string;
589
+ transport?: string;
590
+ session?: WorkflowAgentSessionReference;
511
591
  }
512
592
  export interface WorkflowSiblingAgent {
513
593
  id: string;
@@ -669,6 +749,7 @@ export interface WorkflowValidationParameters {
669
749
  name?: string;
670
750
  description?: string;
671
751
  script?: string;
752
+ scriptPath?: string;
672
753
  workflow?: string;
673
754
  args?: unknown;
674
755
  }
package/dist/src/types.js CHANGED
@@ -16,6 +16,7 @@ export const ERROR_CODES = [
16
16
  "RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
17
17
  "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
18
18
  ];
19
+ export const WORKFLOW_AGENT_STALL_THRESHOLD_MS = 10 * 60 * 1000;
19
20
  export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
20
21
  export class WorkflowError extends Error {
21
22
  code;
@@ -180,7 +180,7 @@ export function parseRoleMarkdown(content, strict = false, rolePath) {
180
180
  return { prompt: content };
181
181
  const meta = {};
182
182
  for (const line of content.slice(4, end).split("\n")) {
183
- const match = /^(model|thinking|tools|description)\s*:\s*(.+)$/.exec(line.trim());
183
+ const match = /^(model|thinking|tools|description|overrideSystemPrompt|override_system_prompt|is_system_prompt)\s*:\s*(.+)$/.exec(line.trim());
184
184
  if (match?.[1] && match[2])
185
185
  meta[match[1]] = match[2].trim();
186
186
  }
@@ -197,6 +197,9 @@ export function parseRoleMarkdown(content, strict = false, rolePath) {
197
197
  definition.thinking = thinking;
198
198
  if (tools)
199
199
  definition.tools = tools;
200
+ const overrideSystemPrompt = meta.overrideSystemPrompt ?? meta.override_system_prompt ?? meta.is_system_prompt;
201
+ if (overrideSystemPrompt)
202
+ definition.overrideSystemPrompt = overrideSystemPrompt === "true";
200
203
  return definition;
201
204
  }
202
205
  const normalized = content.replace(/\r\n?/g, "\n");
@@ -212,16 +215,19 @@ export function parseRoleMarkdown(content, strict = false, rolePath) {
212
215
  if (!object(parsed.frontmatter))
213
216
  fail("INVALID_METADATA", "Role frontmatter must be an object");
214
217
  const { model, thinking, tools, description, disabledAgentResources } = parsed.frontmatter;
218
+ const overrideSystemPrompt = parsed.frontmatter.overrideSystemPrompt ?? parsed.frontmatter.override_system_prompt ?? parsed.frontmatter.is_system_prompt;
215
219
  if (model !== undefined && (typeof model !== "string" || model.trim() === ""))
216
220
  fail("INVALID_METADATA", "Role model must be a non-empty string");
217
221
  if (thinking !== undefined && (typeof thinking !== "string" || !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(thinking)))
218
222
  fail("INVALID_METADATA", `Invalid role thinking level: ${typeof thinking === "string" ? thinking : typeof thinking}`);
219
223
  if (description !== undefined && (typeof description !== "string" || description.trim() === "" || description.length > 1024 || /[\r\n]/.test(description)))
220
224
  fail("INVALID_METADATA", "Role description must be a non-empty single-line string of at most 1024 characters");
225
+ if (overrideSystemPrompt !== undefined && typeof overrideSystemPrompt !== "boolean")
226
+ fail("INVALID_METADATA", "Role overrideSystemPrompt must be a boolean");
221
227
  if (tools !== undefined && (!Array.isArray(tools) || tools.some((tool) => typeof tool !== "string" || tool.trim() === "")))
222
228
  fail("INVALID_METADATA", "Role tools must be an array of non-empty strings");
223
229
  const normalizedResources = validateAgentResourceExclusions(disabledAgentResources, rolePath ?? "<role>", "INVALID_METADATA");
224
- return { prompt: parsed.body, ...(typeof description === "string" ? { description: description.trim() } : {}), ...(typeof model === "string" ? { model: model.trim() } : {}), ...(typeof thinking === "string" ? { thinking: thinking } : {}), ...(Array.isArray(tools) ? { tools: tools.map((tool) => tool.trim()) } : {}), ...(normalizedResources ? { disabledAgentResources: normalizedResources } : {}) };
230
+ return { prompt: parsed.body, ...(typeof description === "string" ? { description: description.trim() } : {}), ...(typeof model === "string" ? { model: model.trim() } : {}), ...(typeof thinking === "string" ? { thinking: thinking } : {}), ...(Array.isArray(tools) ? { tools: tools.map((tool) => tool.trim()) } : {}), ...(typeof overrideSystemPrompt === "boolean" ? { overrideSystemPrompt } : {}), ...(normalizedResources ? { disabledAgentResources: normalizedResources } : {}) };
225
231
  }
226
232
  const ROLE_DIRECTORY = "pi-extensible-workflows";
227
233
  export function workflowRoleDirectories(agentDir = getAgentDir()) {
@@ -819,10 +825,25 @@ export function validateWorkflowLaunchWithRegistry(params, context, registry) {
819
825
  fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
820
826
  if (params.script !== undefined && params.workflow !== undefined)
821
827
  fail("INVALID_METADATA", "Provide either script or workflow, not both");
828
+ if (params.scriptPath !== undefined && (typeof params.scriptPath !== "string" || !params.scriptPath.trim()))
829
+ fail("INVALID_METADATA", "scriptPath must be a non-empty path");
830
+ if ((params.script !== undefined || params.workflow !== undefined) && params.scriptPath !== undefined)
831
+ fail("INVALID_METADATA", "Provide either script, scriptPath, or workflow, not more than one");
832
+ const scriptPath = typeof params.scriptPath === "string" ? params.scriptPath.trim() : undefined;
833
+ let fileScript;
834
+ if (scriptPath !== undefined) {
835
+ try {
836
+ fileScript = readFileSync(resolve(context.cwd, scriptPath), "utf8");
837
+ }
838
+ catch (error) {
839
+ fail("INVALID_SYNTAX", `Cannot read workflow script file ${scriptPath}: ${errorText(error)}`);
840
+ }
841
+ }
822
842
  const functionName = typeof params.workflow === "string" ? params.workflow : undefined;
823
- if (functionName !== undefined && params.name !== undefined)
824
- fail("INVALID_METADATA", "Registered function launches do not accept name; workflow is the run name");
825
- const workflowName = functionName ?? (typeof params.name === "string" ? params.name.trim() : "");
843
+ const explicitName = params.name === undefined ? undefined : typeof params.name === "string" ? params.name.trim() : "";
844
+ if (params.name !== undefined && !explicitName)
845
+ fail("INVALID_METADATA", "Workflow name must be non-empty when provided");
846
+ const workflowName = explicitName ?? functionName ?? "";
826
847
  if (functionName === undefined && !workflowName)
827
848
  fail("INVALID_METADATA", "Inline workflow launches require a non-empty name");
828
849
  const fn = functionName === undefined ? undefined : registry?.function(functionName);
@@ -831,9 +852,9 @@ export function validateWorkflowLaunchWithRegistry(params, context, registry) {
831
852
  const args = params.args === undefined ? null : params.args;
832
853
  if (functionName !== undefined && fn && (!object(args) || !jsonValue(args) || !Value.Check(fn.input, args)))
833
854
  fail("RESULT_INVALID", `Invalid input for ${functionName}`);
834
- const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : "";
855
+ const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : fileScript ?? "";
835
856
  if (!script)
836
- fail("INVALID_SYNTAX", "Provide script or registered function");
857
+ fail("INVALID_SYNTAX", "Provide script, scriptPath, or registered function");
837
858
  const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
838
859
  const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
839
860
  const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
@@ -9,5 +9,6 @@ export type WorkflowTui = {
9
9
  requestRender(force?: boolean): void;
10
10
  };
11
11
  export declare function workflowScriptArtifact(script: string): WorkflowArtifact;
12
+ export declare function workflowPromptArtifact(prompt: string): WorkflowArtifact;
12
13
  export declare function workflowResultArtifact(value: JsonValue): WorkflowArtifact;
13
14
  export declare function openWorkflowArtifact(tui: WorkflowTui, command: string, artifact: WorkflowArtifact): Promise<number | null>;
@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  export function workflowScriptArtifact(script) { return { extension: ".js", content: script }; }
6
+ export function workflowPromptArtifact(prompt) { return { extension: ".md", content: prompt }; }
6
7
  export function workflowResultArtifact(value) { return typeof value === "string" ? { extension: ".md", content: value } : { extension: ".json", content: `${JSON.stringify(value, null, 2)}\n` }; }
7
8
  async function spawnWorkflowEditor(command, path) {
8
9
  const [editor, ...editorArgs] = command.split(" ");
@@ -74,6 +74,10 @@ export interface ParentToolResult {
74
74
  isError?: boolean;
75
75
  text?: string;
76
76
  }
77
+ export interface ParentToolCall {
78
+ name: string;
79
+ arguments: JsonValue;
80
+ }
77
81
  export interface ParentUsage {
78
82
  input: number;
79
83
  output: number;
@@ -216,13 +220,14 @@ export interface EvalCaseResult {
216
220
  realWorkflowAgentsLaunched: number | null;
217
221
  };
218
222
  }
219
- export declare const SAFE_PARENT_EVAL_TOOLS: readonly ["read", "grep", "find", "bash", "workflow"];
223
+ export declare const SAFE_PARENT_EVAL_TOOLS: readonly ["read", "grep", "find", "bash", "workflow", "workflow_retry"];
220
224
  export declare function validateWorkflowEvalCases(values: readonly unknown[], source?: string): readonly WorkflowEvalCase[];
221
225
  export declare function loadWorkflowEvalCases(directory?: string): readonly WorkflowEvalCase[];
222
226
  export declare const INITIAL_WORKFLOW_EVAL_CASES: readonly WorkflowEvalCase[];
223
227
  export declare function matchesJsonResult(shape: JsonResultShape, value: JsonValue): boolean;
224
228
  export declare function matchesOutputSchema(shape: OutputSchemaShape, schema: JsonSchema): boolean;
225
229
  export declare function extractParentOracle(entries: readonly unknown[]): ParentOracle;
230
+ export declare function extractParentToolCalls(oracle: ParentOracle): ParentToolCall[];
226
231
  export declare function extractParentOracleFile(path: string): ParentOracle;
227
232
  export declare function extractCapturedWorkflows(oracle: ParentOracle): CapturedWorkflowCall[];
228
233
  export declare function captureValidationReports(oracle: ParentOracle, calls: readonly CapturedWorkflowCall[]): {
@@ -231,6 +236,7 @@ export declare function captureValidationReports(oracle: ParentOracle, calls: re
231
236
  verified: boolean;
232
237
  };
233
238
  export declare function evalExpectationErrors(oracle: ParentOracle, expectations: EvalExpectations): string[];
239
+ export declare function recoverySelectionErrors(evalCase: Pick<WorkflowEvalCase, "id">, oracle: ParentOracle): string[];
234
240
  export declare function replayExpectationErrors(calls: readonly CapturedWorkflowCall[], reports: readonly ReplayReport[], expectations: EvalExpectations): string[];
235
241
  export declare function staticExpectationResults(calls: readonly CapturedWorkflowCall[], expectations: EvalExpectations): CriterionResult[];
236
242
  export declare function selectStaticCandidate(calls: readonly CapturedWorkflowCall[], validations: readonly ProductionValidationReport[], expectations: EvalExpectations, requiredCount?: number): {
@@ -10,7 +10,7 @@ import { CAPTURE_ERROR_PREFIX, CAPTURE_IDENTITY, resolveWorkflowSkillPath } from
10
10
  export { resolveWorkflowSkillPath } from "./eval-capture-extension.js";
11
11
  import { ERROR_CODES, inspectWorkflowScript, isObject, loadAgentDefinitions, runWorkflow, WORKFLOW_CALL_KINDS, WorkflowError } from "./index.js";
12
12
  const CASE_PROCESS_GRACE_MS = 1_000;
13
- export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow"]);
13
+ export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow", "workflow_retry"]);
14
14
  const EVAL_MODEL_TOKEN = "$EVAL_MODEL";
15
15
  const semantic = (description) => [{ id: "intent", description }];
16
16
  const JSON_RESULT_TYPES = ["null", "boolean", "number", "integer", "string", "array", "object"];
@@ -367,6 +367,13 @@ export function extractParentOracle(entries) {
367
367
  }
368
368
  return { assistantBatches: batches, workflowToolResults, skillReads, ...(firstSignificantAction ? { firstSignificantAction } : {}), ...(parentToolSequence[0] ? { firstTool: parentToolSequence[0] } : {}), firstBatchToolSequence: firstBatch?.tools ?? [], toolsBeforeFirstWorkflow: firstWorkflowIndex < 0 ? parentToolSequence : parentToolSequence.slice(0, firstWorkflowIndex), firstWorkflowBatchToolSequence: firstWorkflowBatch?.tools ?? [], parentToolSequence, workflowCallCount: parentToolSequence.filter((name) => name === "workflow").length, usage: { ...totals, models: [...modelCosts].map(([model, cost]) => ({ model, cost })) } };
369
369
  }
370
+ export function extractParentToolCalls(oracle) {
371
+ return oracle.assistantBatches.flatMap(({ parts }) => parts.flatMap((part) => {
372
+ if (!isObject(part) || part.type !== "toolCall" || typeof part.name !== "string")
373
+ return [];
374
+ return [{ name: part.name, arguments: isJson(part.arguments) ? part.arguments : null }];
375
+ }));
376
+ }
370
377
  export function extractParentOracleFile(path) {
371
378
  const entries = readFileSync(path, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
372
379
  return extractParentOracle(entries);
@@ -434,6 +441,19 @@ export function evalExpectationErrors(oracle, expectations) {
434
441
  }
435
442
  return errors;
436
443
  }
444
+ const RECOVERY_SELECTIONS = {
445
+ "recovery-failed-run": { tool: "workflow_retry", arguments: { runId: "failed-run-42" } },
446
+ "recovery-completed-worktree": { tool: "workflow", arguments: { name: "borrow-worktree", script: "return true;", parentRunId: "completed-run-42" } },
447
+ };
448
+ export function recoverySelectionErrors(evalCase, oracle) {
449
+ const expected = RECOVERY_SELECTIONS[evalCase.id];
450
+ if (!expected)
451
+ return [];
452
+ const actual = extractParentToolCalls(oracle);
453
+ if (actual.length === 1 && actual[0]?.name === expected.tool && equalJson(actual[0].arguments, expected.arguments))
454
+ return [];
455
+ return [`${evalCase.id} parent tool calls were ${JSON.stringify(actual)}; expected ${JSON.stringify([expected])}`];
456
+ }
437
457
  export function replayExpectationErrors(calls, reports, expectations) {
438
458
  const errors = [];
439
459
  const staticCalls = calls.flatMap((call) => { try {
@@ -862,7 +882,7 @@ function semanticJudgePrompt(evalCase, calls, cwd, home) {
862
882
  } }));
863
883
  const roleText = [...usedRoles].map((role) => `${role}: ${roles[role]?.description ?? "no description"}`).join("\n") || "none";
864
884
  const docs = "agent(prompt, options) delegates; shell(command, options) runs a deterministic host command and returns exitCode/stdout/stderr; parallel(name, tasks) runs independent tasks concurrently; pipeline(name, items, stages) applies ordered stages; prompt(template, data) carries values into prompts. A role owns model/thinking/tools policy.";
865
- return `Judge whether the captured workflow design satisfies each criterion. Do not execute it. Return only JSON: {"criteria":[{"id":"criterion id","pass":true,"evidence":"specific script evidence"}]}.\n\nOriginal request:\n${evalCase.prompt}\n\nCriteria:\n${JSON.stringify(evalCase.semanticCriteria ?? [])}\n\nDSL:\n${docs}\n\nRelevant roles:\n${roleText}\n\nCaptured workflow script(s):\n${calls.map((call, index) => `--- ${String(index)} ---\n${call.script ?? "<missing>"}`).join("\n")}`;
885
+ return `Judge whether the captured workflow design satisfies each criterion. Do not execute it. Return only JSON: {"criteria":[{"id":"criterion id","pass":true,"evidence":"specific script evidence"}]}.\n\nOriginal request:\n${evalCase.prompt}\n\nCriteria:\n${JSON.stringify(evalCase.semanticCriteria ?? [])}\n\nDSL:\n${docs}\n\nRelevant roles:\n${roleText}\n\nCaptured workflow call(s):\n${calls.map((call, index) => `--- ${String(index)} ---\nArguments:\n${JSON.stringify(call.arguments)}\nScript:\n${call.script ?? "<missing>"}`).join("\n")}`;
866
886
  }
867
887
  async function runSemanticJudge(input, calls, cwd, home, sessionDir, maxCost) {
868
888
  const args = ["--offline", "--no-extensions", "--no-skills", "--no-context-files", "--no-tools", "--mode", "json", "--session-dir", sessionDir, "--session-id", randomUUID()];
@@ -1045,7 +1065,7 @@ export async function captureEvalCase(input) {
1045
1065
  const parentUsageThroughCandidate = usageThroughCandidate(oracle, workflows, selection.callIndices);
1046
1066
  const parentAccounting = parentUsageThroughCandidate ?? oracle.usage;
1047
1067
  const unsafeTool = oracle.parentToolSequence.find((tool) => !SAFE_PARENT_EVAL_TOOLS.includes(tool));
1048
- const errors = [...evalExpectationErrors(oracle, input.case.expectations), ...validation.errors, ...(unsafeTool ? [`parent tool is outside the safe eval allowlist: ${unsafeTool}`] : [])];
1068
+ const errors = [...evalExpectationErrors(oracle, input.case.expectations), ...recoverySelectionErrors(input.case, oracle), ...validation.errors, ...(unsafeTool ? [`parent tool is outside the safe eval allowlist: ${unsafeTool}`] : [])];
1049
1069
  if (requiredCount > 0 && selection.callIndices.length === 0)
1050
1070
  errors.push("Catastrophic validity failure: no production-valid workflow candidate satisfied static expectations.");
1051
1071
  let judge;
@@ -0,0 +1,17 @@
1
+ id: recovery-completed-worktree
2
+ prompt: >-
3
+ The completed workflow run ID is "completed-run-42". You need a new workflow to do named worktree work,
4
+ so launch the workflow with exactly parentRunId "completed-run-42". Use exactly the workflow arguments {"name":"borrow-worktree","script":"return true;","parentRunId":"completed-run-42"}.
5
+ Do not call workflow_retry because the source is completed and parentRunId only borrows the named worktree; it does not replay or resume.
6
+ maxCost: 0.1
7
+ expectations:
8
+ firstTool: workflow
9
+ firstBatchToolSequence:
10
+ startsWith: [workflow]
11
+ parentToolSequence:
12
+ startsWith: [workflow]
13
+ workflowCallCount: 1
14
+ expectedWorkflowCalls: 1
15
+ semanticCriteria:
16
+ - id: parent-run-id
17
+ description: The captured workflow call has exactly name borrow-worktree, script return true;, and parentRunId completed-run-42; it does not call workflow_retry.
@@ -0,0 +1,14 @@
1
+ id: recovery-failed-run
2
+ prompt: >-
3
+ A workflow failure diagnostic identifies the persisted failed run ID "failed-run-42" and says the exact next action is
4
+ workflow_retry({ runId: "failed-run-42" }). Select that recovery tool now with exactly {"runId":"failed-run-42"}.
5
+ Do not launch a new workflow, use parentRunId, or answer with prose.
6
+ maxCost: 0.1
7
+ expectedWorkflowCalls: 0
8
+ expectations:
9
+ firstTool: workflow_retry
10
+ firstBatchToolSequence:
11
+ equals: [workflow_retry]
12
+ parentToolSequence:
13
+ equals: [workflow_retry]
14
+ workflowCallCount: 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensible-workflows",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Deterministic multi-agent workflow orchestration for Pi",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -27,15 +27,23 @@ Await `parallel(...)` or `pipeline(...)` results before interpolation. Runs are
27
27
  ## Runtime and safety rules
28
28
 
29
29
 
30
- Inline launches require a non-empty `name`. Registered function launches must omit `name`; they use `workflow` as the run name:
31
30
 
32
31
  ```json
33
32
  { "workflow": "workflowName", "args": { "issue": 42 } }
34
33
  ```
35
- Recovery map: `agent(..., { retries })` reruns one agent call in the same run for transient failures; `workflow_retry({ runId })` replays a failed run into a child; `workflow_resume({ runId, budget? })` continues a `budget_exhausted` run; `parentRunId` on a new launch only borrows named worktrees and never replays or resumes. Use each only for its stated case.
36
- For an explicitly failed run, use the exact `runId` from failure diagnostics with `workflow_retry({ runId })`: diagnostics list replayable and incomplete paths, artifacts, and valid named worktrees; the tool creates a linked child, replays completed agent, shell, function, and checkpoint operations, and executes incomplete work. External side effects before failure are not guaranteed exactly once.
37
- Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground results retain their value and completed `runId`, while background launches return `runId` immediately. Retry versus per-agent `retries` and `workflow_resume` is always explicit.
38
- Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task.
34
+
35
+ A reviewed JavaScript file on disk can use `scriptPath` instead of `script`.
36
+
37
+ Recovery map:
38
+ - `agent(..., { retries })` reruns one agent call in the same run for transient failures;
39
+ - `workflow_retry({ runId, foreground? })` replays a failed run into a child;
40
+ - `workflow_resume({ runId, budget?, foreground? })` continues a `budget_exhausted` run;
41
+ - recovery inherits the source snapshot's foreground/background launch mode, while legacy snapshots without `launchMode` recover in background; set `foreground: true` or `false` to override it;
42
+ - `parentRunId` on a new launch only borrows named worktrees and never replays or resumes.
43
+
44
+ For an explicitly failed run, use the exact `runId` from failure diagnostics with `workflow_retry({ runId, foreground: <prevValue> })`: diagnostics list replayable and incomplete paths, artifacts, and valid named worktrees; the tool creates a linked child, replays completed agent, shell, function, and checkpoint operations, and executes incomplete work. External side effects before failure are not guaranteed exactly once.
45
+ `workflow_stop` requires the exact run ID; foreground launches and foreground recovery retain their terminal value and completed `runId`, while background launches and recovery return `runId` immediately and deliver completion or failure as a follow-up. Retry versus per-agent `retries` and `workflow_resume` is always explicit.
46
+ Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task. Make sure to call workflow_catalog more than once if you need to inspect details about a global function.
39
47
 
40
48
  Workflow JavaScript has no imports, filesystem, network, process, or timers. Delegate that work to agents. `shell(command, options)` is the trusted host RPC for deterministic gates: it inherits the workflow or active-worktree cwd, merges string `env` overrides, and returns `{ exitCode, stdout, stderr }`; nonzero exits are results, but launch failures and timeouts fail with `SHELL_FAILED`.
41
49