pi-extensible-workflows 3.1.0 → 3.2.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/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./validation.js";
5
5
  export * from "./registry.js";
6
6
  export * from "./execution.js";
7
7
  export * from "./host.js";
8
+ export * from "./workflow-artifacts.js";
8
9
  export { default } from "./host.js";
9
10
  export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
10
11
  export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, WorktreeReference } from "./persistence.js";
package/src/types.ts CHANGED
@@ -73,7 +73,7 @@ export interface AgentSetupSummary { hookNames: readonly string[]; model: ModelS
73
73
  export interface AgentAttemptSummary { attempt: number; sessionId: string; sessionFile: string; error?: { code: string; message: string }; accounting: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; setup?: AgentSetupSummary }
74
74
  export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase { owner: string; branch: string; path: string; base: string }
75
75
  export interface WorkflowWorktreeReference { readonly path: string; readonly branch: string }
76
- export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined }
76
+ export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; resultPath?: string; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined }
77
77
  export interface WorkflowRunEvent { type: string; message: string }
78
78
  export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
79
79
  export interface WorkflowPhaseRecord { phase: string; afterAgent: number }
package/src/validation.ts CHANGED
@@ -324,6 +324,32 @@ function workflowCallsWithStructure(program: acorn.Program): Array<{ call: Workf
324
324
  visit(program, { execution: "sequential", structure: [] });
325
325
  return calls.sort((left, right) => left.call.start - right.call.start);
326
326
  }
327
+ function memberCall(node: acorn.AnyNode | undefined, objectName: string, propertyName: string): boolean {
328
+ if (node?.type !== "CallExpression" || node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.object.type !== "Identifier" || node.callee.object.name !== objectName || node.callee.property.type !== "Identifier") return false;
329
+ return node.callee.property.name === propertyName;
330
+ }
331
+ function mapCallback(node: acorn.AnyNode): acorn.AnyNode | undefined {
332
+ if (!memberCall(node, "Promise", "all") && !memberCall(node, "Promise", "allSettled")) return undefined;
333
+ if (node.type !== "CallExpression") return undefined;
334
+ const source = node.arguments[0];
335
+ if (source?.type !== "CallExpression" || source.callee.type !== "MemberExpression" || source.callee.computed || source.callee.property.type !== "Identifier" || !["map", "flatMap"].includes(source.callee.property.name)) return undefined;
336
+ const callback = source.arguments[0];
337
+ return callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression" ? callback : undefined;
338
+ }
339
+ function hasUnscopedAgent(node: acorn.AnyNode, scoped = false): boolean {
340
+ const operation = workflowCallKind(node);
341
+ if (operation === "agent") return !scoped;
342
+ const nestedScope = scoped || operation === "parallel" || operation === "pipeline";
343
+ return astChildren(node).some((child) => hasUnscopedAgent(child, nestedScope));
344
+ }
345
+ function validateObviousConcurrentAgentCalls(program: acorn.Program): void {
346
+ const visit = (node: acorn.AnyNode): void => {
347
+ const callback = mapCallback(node);
348
+ if (callback && hasUnscopedAgent(callback)) fail("INVALID_METADATA", "Promise.all/map agent fan-out cannot prove stable call-site identity; use parallel(...) or pipeline(...)");
349
+ for (const child of astChildren(node)) visit(child);
350
+ };
351
+ visit(program);
352
+ }
327
353
  function validateDirectPrimitiveReferences(program: acorn.AnyNode, name: string): void {
328
354
  const visit = (node: acorn.AnyNode, parent?: acorn.AnyNode): void => {
329
355
  if (node.type === "Identifier" && node.name === name) {
@@ -606,6 +632,7 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
606
632
  validateDirectPrimitiveReferences(program, "shell");
607
633
  for (const [index, schema] of schemas.entries()) validateSchema(schema, `schema[${String(index)}]`);
608
634
  const calls = workflowCalls(program);
635
+ validateObviousConcurrentAgentCalls(program);
609
636
  const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
610
637
  for (const call of calls) {
611
638
  const operation = call.callee.name;
@@ -0,0 +1,34 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { JsonValue } from "./types.js";
6
+
7
+ export interface WorkflowArtifact { extension: ".js" | ".json" | ".md"; content: string }
8
+ export type WorkflowTui = { stop(): void; start(): void; requestRender(force?: boolean): void };
9
+
10
+ export function workflowScriptArtifact(script: string): WorkflowArtifact { return { extension: ".js", content: script }; }
11
+ export function workflowResultArtifact(value: JsonValue): WorkflowArtifact { return typeof value === "string" ? { extension: ".md", content: value } : { extension: ".json", content: `${JSON.stringify(value, null, 2)}\n` }; }
12
+
13
+ async function spawnWorkflowEditor(command: string, path: string): Promise<number | null> {
14
+ const [editor, ...editorArgs] = command.split(" ");
15
+ if (!editor) return null;
16
+ return new Promise((resolve) => {
17
+ try {
18
+ const child = spawn(editor, [...editorArgs, path], { stdio: "inherit", shell: process.platform === "win32" });
19
+ child.once("error", () => { resolve(null); });
20
+ child.once("close", (code) => { resolve(code); });
21
+ } catch { resolve(null); }
22
+ });
23
+ }
24
+
25
+ export async function openWorkflowArtifact(tui: WorkflowTui, command: string, artifact: WorkflowArtifact): Promise<number | null> {
26
+ const directory = await mkdtemp(join(tmpdir(), "pi-workflow-editor-"));
27
+ const path = join(directory, `artifact${artifact.extension}`);
28
+ try {
29
+ await writeFile(path, artifact.content, { encoding: "utf8", mode: 0o600 });
30
+ tui.stop();
31
+ try { return await spawnWorkflowEditor(command, path); }
32
+ finally { tui.start(); tui.requestRender(true); }
33
+ } finally { await rm(directory, { recursive: true, force: true }); }
34
+ }