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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Turn multi-agent tasks into deterministic jobs that fan out in parallel, pause for approval, and resume without rerunning completed work.
8
8
 
9
- [Documentation](https://vekexasia.github.io/pi-extensible-workflows/) | [Developer guide](https://vekexasia.github.io/pi-extensible-workflows/developers.html) | [Agent guide](https://vekexasia.github.io/pi-extensible-workflows/agents.html) | [Extension authoring](https://vekexasia.github.io/pi-extensible-workflows/extensions.html)
9
+ [Documentation](https://vekexasia.github.io/pi-extensible-workflows/) | [Developer guide](https://vekexasia.github.io/pi-extensible-workflows/developers.html) | [Agent guide](https://vekexasia.github.io/pi-extensible-workflows/agents.html) | [Extension authoring](https://vekexasia.github.io/pi-extensible-workflows/extensions.html) | [Video overview](https://youtu.be/qAiivspEHmU)
10
10
 
11
11
  Requires Node.js 22.19 or newer. This is a trusted Pi extension with the same filesystem and process access as Pi.
12
12
 
@@ -20,32 +20,24 @@ For source installs and local development, see the [installation guide](https://
20
20
 
21
21
  ## Capabilities
22
22
 
23
- The main Pi agent acts as the orchestrator: it writes workflow scripts on the fly for each task. Pi extensions can add reusable functions and variables to those scripts; every registered function is also directly runnable as a top-level workflow.
24
-
25
- Inline workflow launches require a non-empty `name`; registered function launches reject `name` and use their registered function name as the run name. Workflow worktree scopes always use the explicit `withWorktree(name, callback)` form.
26
-
27
- A workflow can fan out across specialized agents, combine their results, and resume without rerunning completed work.
23
+ The default path is a named inline workflow: write a `script` that fans out independent work with `parallel(...)`, awaits the keyed results, passes them into one summarizing `agent(...)`, and returns. Inline launches require a non-empty `name`; registered function launches reject `name` and use their registered function name as the run name. Runs are backgrounded by default; set `foreground: true` when the final value must be returned in the same tool call.
28
24
 
29
25
  ```js
30
26
  const reviews = await parallel("review", {
31
- correctness: () =>
32
- agent("Review the current changes for correctness issues."),
33
- security: () =>
34
- agent("Review the current changes for security risks.", {
35
- role: "security-specialist",
36
- }),
27
+ correctness: () => agent("Review the current changes for correctness issues."),
28
+ security: () => agent("Review the current changes for security risks."),
37
29
  tests: () => agent("Review the current changes for missing test coverage."),
38
30
  });
39
31
 
40
- const summary = await agent(
41
- prompt("Deduplicate and prioritize these findings:\n\n{reviews}", {
42
- reviews,
43
- }),
32
+ return await agent(
33
+ prompt("Summarize and prioritize these findings:\n\n{reviews}", { reviews }),
44
34
  );
45
-
46
- return summary;
47
35
  ```
48
36
 
37
+ **Advanced capabilities:** Use registered functions, `outputSchema`, budgets, checkpoints, worktrees, retry/resume, CLI export, and `pipeline(...)` when the task requires them. They remain available without complicating the basic inline path. Workflow worktree scopes always use the explicit `withWorktree(name, callback)` form.
38
+
39
+ The main Pi agent writes these scripts on the fly for each task; extensions can add reusable functions and variables, and completed workflows can resume without rerunning completed work.
40
+
49
41
  Learn more about roles, workflow contracts, and extension APIs in the documentation:
50
42
 
51
43
  - [Workflow tool and invocation API](https://vekexasia.github.io/pi-extensible-workflows/developers.html#tool-api)
@@ -53,6 +45,7 @@ Learn more about roles, workflow contracts, and extension APIs in the documentat
53
45
  - [Aggregate run budgets](https://vekexasia.github.io/pi-extensible-workflows/developers.html#budgets)
54
46
  - [Workflow DSL and worktrees](https://vekexasia.github.io/pi-extensible-workflows/developers.html#dsl)
55
47
  - [Extension authoring guide](https://vekexasia.github.io/pi-extensible-workflows/extensions.html)
48
+ - [Copy-paste extension template](https://github.com/vekexasia/pi-extensible-workflows/tree/main/examples/workflow-extension-template)
56
49
  - [Run artifacts and lifecycle events](https://vekexasia.github.io/pi-extensible-workflows/developers.html#lifecycle)
57
50
  - [Run inspection and recovery](https://vekexasia.github.io/pi-extensible-workflows/developers.html#operations)
58
51
  - [Agent patterns and model selection](https://vekexasia.github.io/pi-extensible-workflows/agents.html#patterns)
@@ -1,5 +1,5 @@
1
1
  import { fork, spawn } from "node:child_process";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { StringDecoder } from "node:string_decoder";
@@ -72,6 +72,7 @@ const named = (value, kind) => { if (typeof value !== "string" || !value.trim())
72
72
  const path = (...names) => names.map(encodeURIComponent).join("/");
73
73
  const inheritedAgentPath = new AsyncLocalStorage();
74
74
  const agentOccurrences = new Map();
75
+ const agentInflight = new Set();
75
76
  const shellOccurrences = new Map();
76
77
  const worktreeOwners = new AsyncLocalStorage();
77
78
  const rejectAgent = () => { throw workError("INVALID_METADATA", "Workflow agent calls must use a direct agent(...) call; aliases and indirect calls are unsupported"); };
@@ -92,14 +93,22 @@ const internalAgent = (...values) => {
92
93
  const callSite = values.pop();
93
94
  if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow agent call-site identity");
94
95
  const inherited = inheritedAgentPath.getStore() || [];
95
- // ponytail: same-callsite races outside parallel/pipeline lack a stable structural scope and are unsupported.
96
96
  const occurrenceKey = JSON.stringify([inherited, callSite]);
97
+ if (agentInflight.has(occurrenceKey)) throw workError("INVALID_METADATA", "Concurrent agent calls from the same source call site are unsupported; use parallel(...) or pipeline(...)");
98
+ agentInflight.add(occurrenceKey);
97
99
  const occurrence = (agentOccurrences.get(occurrenceKey) || 0) + 1;
98
100
  agentOccurrences.set(occurrenceKey, occurrence);
99
101
  const options = values.length < 2 || values[1] === undefined ? {} : values[1];
100
102
  const worktreeOwner = worktreeOwners.getStore();
101
103
  const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
102
- const result = rpc("agent", [values[0], options, identity]).then(unwrap);
104
+ let result;
105
+ try {
106
+ result = rpc("agent", [values[0], options, identity]).then(unwrap);
107
+ } catch (error) {
108
+ agentInflight.delete(occurrenceKey);
109
+ throw error;
110
+ }
111
+ void result.then(() => agentInflight.delete(occurrenceKey), () => agentInflight.delete(occurrenceKey));
103
112
  Object.defineProperties(result, {
104
113
  toJSON: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before serialization"); } },
105
114
  toString: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before interpolation"); } },
@@ -421,7 +430,7 @@ function workflowErrorFromWorker(error) {
421
430
  export function runWorkflow(script, args = null, bridge = {}, signal) {
422
431
  encoded(args);
423
432
  const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
424
- const childDir = mkdtempSync(join(tmpdir(), "pi-wf-"));
433
+ const childDir = realpathSync(mkdtempSync(join(tmpdir(), "pi-wf-")));
425
434
  const childFile = join(childDir, "child.cjs");
426
435
  writeFileSync(childFile, childSource);
427
436
  const child = fork(childFile, [String(RPC_LIMIT_BYTES), config], {
@@ -34,8 +34,8 @@ export declare function formatWorkflowPreview(args: {
34
34
  description?: unknown;
35
35
  }): string;
36
36
  export declare const WORKFLOW_TOOL_LABEL = "Workflow";
37
- export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
38
- export declare const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that orchestrates subagents. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Runs in the background by default; completion arrives as a follow-up message. Foreground results include the completed run ID. Use workflow_retry with an explicit failed run ID to replay completed structural operations; parentRunId only reuses named worktrees.";
37
+ export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default";
38
+ export declare const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Foreground results include the completed run ID. 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.";
39
39
  export declare const WORKFLOW_TOOL_PARAMETERS: Type.TObject<{
40
40
  name: Type.TOptional<Type.TString>;
41
41
  description: Type.TOptional<Type.TString>;
@@ -64,7 +64,6 @@ export interface WorkflowPhaseView {
64
64
  name: string;
65
65
  occurrence: number;
66
66
  state: WorkflowPhaseState;
67
- status: WorkflowPhaseState;
68
67
  observed: boolean;
69
68
  afterAgent?: number;
70
69
  agents: readonly AgentRecord[];
@@ -83,7 +82,48 @@ export declare function buildWorkflowPhaseModel(run: Pick<PersistedRun, "state"
83
82
  export interface WorkflowPhaseSelection {
84
83
  phaseId?: string | undefined;
85
84
  agentId?: string | undefined;
85
+ nodeId?: string | undefined;
86
+ expandedNodeIds?: readonly string[] | undefined;
87
+ treeOnly?: boolean | undefined;
88
+ detailsOnly?: boolean | undefined;
89
+ actions?: {
90
+ title: string;
91
+ options: readonly string[];
92
+ index: number;
93
+ } | undefined;
86
94
  }
95
+ export type WorkflowPhaseTreeNodeKind = "phase" | "operation" | "agent";
96
+ export interface WorkflowPhaseTreeNode {
97
+ id: string;
98
+ kind: WorkflowPhaseTreeNodeKind;
99
+ label: string;
100
+ depth: number;
101
+ phaseId: string;
102
+ operationPath: readonly string[];
103
+ parentId?: string;
104
+ children: readonly string[];
105
+ state: WorkflowPhaseState | AgentRecord["state"];
106
+ agentId?: string;
107
+ agent?: AgentRecord;
108
+ phase?: WorkflowPhaseView;
109
+ }
110
+ export interface WorkflowPhaseTree {
111
+ roots: readonly string[];
112
+ nodes: readonly WorkflowPhaseTreeNode[];
113
+ byId: ReadonlyMap<string, WorkflowPhaseTreeNode>;
114
+ }
115
+ export interface WorkflowPhaseTreeSelection {
116
+ nodeId?: string | undefined;
117
+ }
118
+ export type WorkflowPhaseTreeDirection = "up" | "down" | "left" | "right";
119
+ export declare function buildWorkflowPhaseTree(model: WorkflowPhaseModel): WorkflowPhaseTree;
120
+ export declare function workflowPhaseTreeVisibleNodes(tree: WorkflowPhaseTree, expanded?: ReadonlySet<string>): readonly WorkflowPhaseTreeNode[];
121
+ export declare function workflowPhaseTreeInitialExpanded(tree: WorkflowPhaseTree): ReadonlySet<string>;
122
+ export declare function preserveWorkflowPhaseTreeSelection(tree: WorkflowPhaseTree, selection: WorkflowPhaseTreeSelection): WorkflowPhaseTreeSelection;
123
+ export declare function navigateWorkflowPhaseTree(tree: WorkflowPhaseTree, selectedNodeId: string | undefined, expandedNodeIds: ReadonlySet<string>, direction: WorkflowPhaseTreeDirection): {
124
+ nodeId?: string;
125
+ expandedNodeIds: ReadonlySet<string>;
126
+ };
87
127
  export declare function preserveWorkflowPhaseSelection(model: WorkflowPhaseModel, selection: WorkflowPhaseSelection): WorkflowPhaseSelection;
88
128
  export declare function formatWorkflowProgress(run: PersistedRun, spinner?: string, styles?: WorkflowProgressStyles): string;
89
129
  export declare function truncateWorkflowProgress(text: string, width: number): string[];
@@ -94,7 +134,7 @@ export declare function formatNavigatorDashboard(run: PersistedRun, checkpoints:
94
134
  export declare function formatNavigatorRun(loaded: {
95
135
  run: PersistedRun;
96
136
  snapshot: Readonly<LaunchSnapshot>;
97
- }, checkpoints: readonly AwaitingCheckpoint[], _worktrees: readonly WorktreeReference[]): string;
137
+ }, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[]): string;
98
138
  export declare function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection?: WorkflowPhaseSelection, styles?: WorkflowProgressStyles): string[];
99
139
  export declare function formatWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string;
100
140
  export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard?: typeof copyToClipboard, createSession?: SessionFactory, agentDir?: string): void;