@marcoscale98/pi-extensible-workflows 5.14.1-fork.1

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 (204) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/README.md +64 -0
  3. package/dist/src/agent-execution.d.ts +338 -0
  4. package/dist/src/agent-execution.js +1633 -0
  5. package/dist/src/background-widget.d.ts +74 -0
  6. package/dist/src/background-widget.js +1340 -0
  7. package/dist/src/budget.d.ts +38 -0
  8. package/dist/src/budget.js +188 -0
  9. package/dist/src/changelog.d.ts +6 -0
  10. package/dist/src/changelog.js +112 -0
  11. package/dist/src/decoders.d.ts +96 -0
  12. package/dist/src/decoders.js +629 -0
  13. package/dist/src/execution.d.ts +19 -0
  14. package/dist/src/execution.js +749 -0
  15. package/dist/src/herdr.d.ts +53 -0
  16. package/dist/src/herdr.js +270 -0
  17. package/dist/src/host-delivery.d.ts +95 -0
  18. package/dist/src/host-delivery.js +376 -0
  19. package/dist/src/host-navigator.d.ts +83 -0
  20. package/dist/src/host-navigator.js +1087 -0
  21. package/dist/src/host-phases.d.ts +89 -0
  22. package/dist/src/host-phases.js +320 -0
  23. package/dist/src/host-recovery.d.ts +141 -0
  24. package/dist/src/host-recovery.js +356 -0
  25. package/dist/src/host-runtime.d.ts +78 -0
  26. package/dist/src/host-runtime.js +378 -0
  27. package/dist/src/host-view.d.ts +116 -0
  28. package/dist/src/host-view.js +868 -0
  29. package/dist/src/host.d.ts +40 -0
  30. package/dist/src/host.js +1641 -0
  31. package/dist/src/index.d.ts +29 -0
  32. package/dist/src/index.js +13725 -0
  33. package/dist/src/index.js.map +6 -0
  34. package/dist/src/io.d.ts +14 -0
  35. package/dist/src/io.js +82 -0
  36. package/dist/src/package-identity.d.ts +4 -0
  37. package/dist/src/package-identity.js +6 -0
  38. package/dist/src/paths.d.ts +14 -0
  39. package/dist/src/paths.js +51 -0
  40. package/dist/src/persistence.d.ts +5 -0
  41. package/dist/src/persistence.js +5 -0
  42. package/dist/src/pi-runtime-adapter.d.ts +27 -0
  43. package/dist/src/pi-runtime-adapter.js +243 -0
  44. package/dist/src/pi-runtime-runner.d.ts +42 -0
  45. package/dist/src/pi-runtime-runner.js +505 -0
  46. package/dist/src/registry.d.ts +48 -0
  47. package/dist/src/registry.js +379 -0
  48. package/dist/src/retention.d.ts +17 -0
  49. package/dist/src/retention.js +195 -0
  50. package/dist/src/runtime/agent-runner.d.ts +143 -0
  51. package/dist/src/runtime/agent-runner.js +12 -0
  52. package/dist/src/runtime/index.d.ts +2 -0
  53. package/dist/src/runtime/index.js +2 -0
  54. package/dist/src/runtime/namespace.d.ts +14 -0
  55. package/dist/src/runtime/namespace.js +1 -0
  56. package/dist/src/runtime/workflow-result.d.ts +2 -0
  57. package/dist/src/runtime/workflow-result.js +6 -0
  58. package/dist/src/session-handoff.d.ts +2 -0
  59. package/dist/src/session-handoff.js +82 -0
  60. package/dist/src/session-lease.d.ts +12 -0
  61. package/dist/src/session-lease.js +180 -0
  62. package/dist/src/store.d.ts +95 -0
  63. package/dist/src/store.js +951 -0
  64. package/dist/src/subagent-manager-handle.d.ts +4 -0
  65. package/dist/src/subagent-manager-handle.js +6 -0
  66. package/dist/src/tool-timing.d.ts +12 -0
  67. package/dist/src/tool-timing.js +33 -0
  68. package/dist/src/trajectory-contracts.d.ts +8 -0
  69. package/dist/src/trajectory-contracts.js +9 -0
  70. package/dist/src/trajectory-host-handle.d.ts +12 -0
  71. package/dist/src/trajectory-host-handle.js +6 -0
  72. package/dist/src/trajectory.d.ts +128 -0
  73. package/dist/src/trajectory.js +594 -0
  74. package/dist/src/types.d.ts +960 -0
  75. package/dist/src/types.js +60 -0
  76. package/dist/src/utils.d.ts +57 -0
  77. package/dist/src/utils.js +225 -0
  78. package/dist/src/validation.d.ts +29 -0
  79. package/dist/src/validation.js +1100 -0
  80. package/dist/src/workflow-artifacts.d.ts +14 -0
  81. package/dist/src/workflow-artifacts.js +40 -0
  82. package/dist/starter/index.d.ts +1 -0
  83. package/dist/starter/index.js +103 -0
  84. package/dist/starter/index.js.map +6 -0
  85. package/dist/starter/review-loop.d.ts +72 -0
  86. package/dist/starter/review-loop.js +43 -0
  87. package/dist/starter/roles/developer.md +6 -0
  88. package/dist/starter/roles/oracle.md +18 -0
  89. package/dist/starter/roles/researcher.md +20 -0
  90. package/dist/starter/roles/reviewer.md +19 -0
  91. package/dist/starter/roles/scout.md +18 -0
  92. package/dist/subagents/index.d.ts +8 -0
  93. package/dist/subagents/index.js +8021 -0
  94. package/dist/subagents/index.js.map +6 -0
  95. package/dist/subagents/src/contracts.d.ts +173 -0
  96. package/dist/subagents/src/contracts.js +68 -0
  97. package/dist/subagents/src/decode.d.ts +15 -0
  98. package/dist/subagents/src/decode.js +182 -0
  99. package/dist/subagents/src/index.d.ts +12 -0
  100. package/dist/subagents/src/index.js +180 -0
  101. package/dist/subagents/src/manager.d.ts +3 -0
  102. package/dist/subagents/src/manager.js +1636 -0
  103. package/dist/subagents/src/navigator.d.ts +5 -0
  104. package/dist/subagents/src/navigator.js +627 -0
  105. package/dist/subagents/src/view.d.ts +70 -0
  106. package/dist/subagents/src/view.js +387 -0
  107. package/dist/subagents/src/worktree.d.ts +34 -0
  108. package/dist/subagents/src/worktree.js +68 -0
  109. package/dist/trajectory/assets/favicon.png +0 -0
  110. package/dist/trajectory/assets/index.html +772 -0
  111. package/dist/trajectory/assets/marked.min.js +6 -0
  112. package/dist/trajectory/assets/morphdom.min.js +1 -0
  113. package/dist/trajectory/index.d.ts +5 -0
  114. package/dist/trajectory/index.js +3459 -0
  115. package/dist/trajectory/index.js.map +6 -0
  116. package/dist/trajectory/src/assets/favicon.png +0 -0
  117. package/dist/trajectory/src/assets/index.html +772 -0
  118. package/dist/trajectory/src/assets/marked.min.js +6 -0
  119. package/dist/trajectory/src/assets/morphdom.min.js +1 -0
  120. package/dist/trajectory/src/export.d.ts +25 -0
  121. package/dist/trajectory/src/export.js +80 -0
  122. package/dist/trajectory/src/index.d.ts +26 -0
  123. package/dist/trajectory/src/index.js +732 -0
  124. package/dist/trajectory/src/server.d.ts +7 -0
  125. package/dist/trajectory/src/server.js +2534 -0
  126. package/dist/trajectory/src/server.js.map +6 -0
  127. package/examples/advanced-patterns/auto-model-router.mjs +78 -0
  128. package/examples/workflow-extension-template/README.md +37 -0
  129. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  130. package/examples/workflow-extension-template/index.js +51 -0
  131. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  132. package/package.json +105 -0
  133. package/skills/pi-extensible-workflows/SKILL.md +160 -0
  134. package/src/agent-execution.ts +1533 -0
  135. package/src/background-widget.ts +1496 -0
  136. package/src/budget.ts +109 -0
  137. package/src/changelog.ts +114 -0
  138. package/src/decoders.ts +567 -0
  139. package/src/execution.ts +652 -0
  140. package/src/herdr.ts +240 -0
  141. package/src/host-delivery.ts +346 -0
  142. package/src/host-navigator.ts +871 -0
  143. package/src/host-phases.ts +264 -0
  144. package/src/host-recovery.ts +333 -0
  145. package/src/host-runtime.ts +373 -0
  146. package/src/host-view.ts +828 -0
  147. package/src/host.ts +1502 -0
  148. package/src/index.ts +29 -0
  149. package/src/io.ts +73 -0
  150. package/src/package-identity.ts +7 -0
  151. package/src/paths.ts +49 -0
  152. package/src/persistence.ts +11 -0
  153. package/src/pi-runtime-adapter.ts +247 -0
  154. package/src/pi-runtime-runner.ts +436 -0
  155. package/src/registry.ts +321 -0
  156. package/src/retention.ts +178 -0
  157. package/src/runtime/agent-runner.ts +158 -0
  158. package/src/runtime/index.ts +2 -0
  159. package/src/runtime/namespace.ts +16 -0
  160. package/src/runtime/workflow-result.ts +8 -0
  161. package/src/session-handoff.ts +78 -0
  162. package/src/session-lease.ts +125 -0
  163. package/src/store.ts +832 -0
  164. package/src/subagent-manager-handle.ts +8 -0
  165. package/src/tool-timing.ts +47 -0
  166. package/src/trajectory-contracts.ts +13 -0
  167. package/src/trajectory-host-handle.ts +16 -0
  168. package/src/trajectory.ts +543 -0
  169. package/src/types.ts +374 -0
  170. package/src/utils.ts +192 -0
  171. package/src/validation.ts +910 -0
  172. package/src/workflow-artifacts.ts +35 -0
  173. package/starter/index.ts +35 -0
  174. package/starter/prompts/council.md +16 -0
  175. package/starter/prompts/deep-research.md +16 -0
  176. package/starter/prompts/oracle.md +14 -0
  177. package/starter/prompts/parallel-review.md +22 -0
  178. package/starter/prompts/parallel-scout.md +16 -0
  179. package/starter/prompts/review-loop.md +14 -0
  180. package/starter/prompts/review.md +14 -0
  181. package/starter/prompts/scout.md +14 -0
  182. package/starter/review-loop.ts +73 -0
  183. package/starter/roles/developer.md +6 -0
  184. package/starter/roles/oracle.md +18 -0
  185. package/starter/roles/researcher.md +20 -0
  186. package/starter/roles/reviewer.md +19 -0
  187. package/starter/roles/scout.md +18 -0
  188. package/subagents/README.md +145 -0
  189. package/subagents/index.ts +8 -0
  190. package/subagents/src/contracts.ts +176 -0
  191. package/subagents/src/decode.ts +158 -0
  192. package/subagents/src/index.ts +194 -0
  193. package/subagents/src/manager.ts +1560 -0
  194. package/subagents/src/navigator.ts +488 -0
  195. package/subagents/src/view.ts +376 -0
  196. package/subagents/src/worktree.ts +99 -0
  197. package/trajectory/index.ts +5 -0
  198. package/trajectory/src/assets/favicon.png +0 -0
  199. package/trajectory/src/assets/index.html +772 -0
  200. package/trajectory/src/assets/marked.min.js +6 -0
  201. package/trajectory/src/assets/morphdom.min.js +1 -0
  202. package/trajectory/src/export.ts +85 -0
  203. package/trajectory/src/index.ts +604 -0
  204. package/trajectory/src/server.ts +447 -0
@@ -0,0 +1,652 @@
1
+ import { fork, spawn, type ChildProcess } from "node:child_process";
2
+ import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { StringDecoder } from "node:string_decoder";
6
+ import { RunStore, structuralPath as operationPath } from "./persistence.js";
7
+ import type { AgentAttempt } from "./agent-execution.js";
8
+ import type { AgentIdentity, AgentAttemptSummary, FunctionIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowAgentSessionReference, WorkflowBridge, WorkflowErrorCode, WorkflowExecution } from "./types.js";
9
+ import { WorkflowError, roleNameOf, sumAccounting, zeroAccounting } from "./types.js";
10
+ import { asWorkflowError, errorText, fail, isWorkflowAuthored, isWorkflowErrorCode, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
11
+ import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validateShellOptions } from "./validation.js";
12
+
13
+ export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
14
+ type WorkerErrorShape = { code?: string; message: string; authored?: boolean; failedAt?: string };
15
+ export const HEARTBEAT_TIMEOUT_MS = 5000;
16
+ type RpcMessage =
17
+ | { type: "heartbeat" }
18
+ | { type: "result"; value: JsonValue }
19
+ | { type: "error"; error: WorkerErrorShape }
20
+ | { type: "rpc"; id: number; method: string; args: JsonValue[] }
21
+ | { type: "rpcResult"; id: number; ok: true; value: JsonValue }
22
+ | { type: "rpcResult"; id: number; ok: false; error: WorkerErrorShape }
23
+ | { type: "cancel" };
24
+ function hasOwn(value: object, key: string): boolean { return Object.prototype.hasOwnProperty.call(value, key); }
25
+ function isWorkerErrorShape(value: unknown): value is WorkerErrorShape {
26
+ return object(value) && typeof value.message === "string" && (value.code === undefined || typeof value.code === "string") && (value.authored === undefined || typeof value.authored === "boolean") && (value.failedAt === undefined || typeof value.failedAt === "string");
27
+ }
28
+ function isRpcMessage(value: unknown): value is RpcMessage {
29
+ if (!object(value) || typeof value.type !== "string") return false;
30
+ if (value.type === "heartbeat" || value.type === "cancel") return true;
31
+ if (value.type === "result") return hasOwn(value, "value") && jsonValue(value.value);
32
+ if (value.type === "error") return isWorkerErrorShape(value.error);
33
+ if (value.type === "rpc") return positiveInteger(value.id) && typeof value.method === "string" && Boolean(value.method.trim()) && Array.isArray(value.args) && value.args.every((arg) => jsonValue(arg));
34
+ if (value.type === "rpcResult") {
35
+ if (!positiveInteger(value.id) || typeof value.ok !== "boolean") return false;
36
+ if (value.ok) return hasOwn(value, "value") && !hasOwn(value, "error") && jsonValue(value.value);
37
+ return !hasOwn(value, "value") && isWorkerErrorShape(value.error);
38
+ }
39
+ return false;
40
+ }
41
+ const HEARTBEAT_INTERVAL_MS = 1000;
42
+
43
+ const OUTCOME_ERRORS = new Set<string>(["AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID"]);
44
+ const WORK_RESULT_BRAND = "__workResult";
45
+
46
+ const childSource = String.raw`
47
+ "use strict";
48
+ const { AsyncLocalStorage } = require("node:async_hooks");
49
+ const vm = require("node:vm");
50
+ const LIMIT = parseInt(process.argv[2], 10);
51
+ const config = JSON.parse(process.argv[3]);
52
+ for (const key of ["getBuiltinModule","binding","_linkedBinding","dlopen","kill","abort","exit","reallyExit","_kill","umask","chdir","setuid","setgid","seteuid","setegid","setgroups","initgroups"]) {
53
+ if (key in process) process[key] = undefined;
54
+ }
55
+ let nextId = 0;
56
+ let cancelled = false;
57
+ const pending = new Map();
58
+ const inflight = new Set();
59
+ const hasMessage = error => Boolean(error && typeof error === "object" && typeof error.message === "string");
60
+ const errorText = error => hasMessage(error) ? error.message : String(error);
61
+ const errorCode = error => { if (!error || typeof error !== "object") return undefined; const code = error.code; return typeof code === "string" ? code : undefined; };
62
+ const errorAuthored = error => Boolean(error && typeof error === "object" && error.authored === true);
63
+ const workerError = error => { const code = errorCode(error); return { code: code || "INTERNAL_ERROR", message: errorText(error), ...(error && typeof error === "object" && typeof error.failedAt === "string" ? { failedAt: error.failedAt } : {}), ...(errorAuthored(error) || hasMessage(error) && !code ? { authored: true } : {}) }; };
64
+ const workflowError = error => Object.assign(new Error(errorText(error)), workerError(error));
65
+ function send(value) {
66
+ const json = JSON.stringify(value);
67
+ if (json === undefined || Buffer.byteLength(json) > LIMIT) throw Object.assign(new Error("RPC value exceeds the 10 MB JSON boundary"), { code: "RPC_LIMIT_EXCEEDED" });
68
+ process.send(json);
69
+ }
70
+ function rpc(method, args) {
71
+ if (cancelled) throw Object.assign(new Error("Workflow cancelled"), { code: "CANCELLED" });
72
+ const id = ++nextId;
73
+ send({ type: "rpc", id, method, args });
74
+ const promise = new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
75
+ inflight.add(promise);
76
+ void promise.then(() => inflight.delete(promise), () => inflight.delete(promise));
77
+ return promise;
78
+ }
79
+ process.on("message", raw => {
80
+ let message;
81
+ try {
82
+ if (typeof raw !== "string" || Buffer.byteLength(raw) > LIMIT) throw Object.assign(new Error("RPC value exceeds the 10 MB JSON boundary"), { code: "RPC_LIMIT_EXCEEDED" });
83
+ message = JSON.parse(raw);
84
+ } catch (error) { send({ type: "error", error: workerError(error) }); return; }
85
+ if (message.type === "cancel") { cancelled = true; for (const { reject } of pending.values()) reject(Object.assign(new Error("Workflow cancelled"), { code: "CANCELLED" })); pending.clear(); return; }
86
+ if (message.type !== "rpcResult") return;
87
+ const request = pending.get(message.id);
88
+ if (!request) return;
89
+ pending.delete(message.id);
90
+ if (message.ok) request.resolve(message.value);
91
+ else request.reject(workflowError(message.error));
92
+ });
93
+ const heartbeat = setInterval(() => send({ type: "heartbeat" }), ${HEARTBEAT_INTERVAL_MS});
94
+ send({ type: "heartbeat" });
95
+ const BRAND = "${WORK_RESULT_BRAND}";
96
+ const workError = (code, message) => Object.assign(new Error(message), { code });
97
+ const isBranded = value => value && typeof value === "object" && value[BRAND] === true;
98
+ const unwrap = result => {
99
+ if (!isBranded(result)) return result;
100
+ if (result.ok) return result.value;
101
+ throw Object.assign(workflowError(result.error), { failedAt: result.failedAt });
102
+ };
103
+ const named = (value, kind) => { if (typeof value !== "string" || !value.trim()) throw workError("INVALID_METADATA", kind + " requires a stable explicit name"); return value; };
104
+ // Each argument is one path segment. A failedAt coming back from a nested operation is already a
105
+ // full path, so it is passed through untouched: feeding it here would encode its separators again.
106
+ const path = (...names) => names.map(encodeURIComponent).join("/");
107
+ const inheritedAgentPath = new AsyncLocalStorage();
108
+ const agentOccurrences = new Map();
109
+ const agentInflight = new Set();
110
+ const shellOccurrences = new Map();
111
+ const worktreeOwners = new AsyncLocalStorage();
112
+ const rejectAgent = () => { throw workError("INVALID_METADATA", "Workflow agent calls must use a direct agent(...) call; aliases and indirect calls are unsupported"); };
113
+ // A pending result that reaches JSON.stringify or a template literal is a forgotten await; fail loudly instead of persisting "[object Promise]".
114
+ const guardedResult = (result, kind) => Object.defineProperties(result, {
115
+ toJSON: { value() { throw workError("INVALID_METADATA", "Workflow " + kind + " result is a Promise; await it before serialization"); } },
116
+ toString: { value() { throw workError("INVALID_METADATA", "Workflow " + kind + " result is a Promise; await it before interpolation"); } },
117
+ [Symbol.toPrimitive]: { value() { throw workError("INVALID_METADATA", "Workflow " + kind + " result is a Promise; await it before interpolation"); } },
118
+ });
119
+ const guardedAgentResult = result => guardedResult(result, "agent");
120
+ const rejectShell = () => { throw workError("INVALID_METADATA", "Workflow shell calls must use a direct shell(...) call; aliases and indirect calls are unsupported"); };
121
+ const rejectWorktree = () => { throw workError("INVALID_METADATA", "withWorktree calls must use a direct withWorktree(...) call; aliases and indirect calls are unsupported"); };
122
+ const internalWithWorktree = async (...values) => {
123
+ if (values.length !== 2) throw workError("INVALID_METADATA", "withWorktree requires a name and callback");
124
+ const name = values[0];
125
+ const callback = values[1];
126
+ if (typeof name !== "string" || !name.trim()) throw workError("INVALID_METADATA", "withWorktree name must be a non-empty string");
127
+ if (typeof callback !== "function") throw workError("INVALID_METADATA", "withWorktree callback must be a function");
128
+ const owner = path("worktree", "named", name.trim());
129
+ const reference = await rpc("worktree", [owner]);
130
+ if (!reference || typeof reference !== "object" || typeof reference.path !== "string" || typeof reference.branch !== "string") throw workError("WORKTREE_FAILED", "Worktree reference is invalid");
131
+ return await worktreeOwners.run(owner, () => callback(Object.freeze({ path: reference.path, branch: reference.branch })));
132
+ };
133
+ const internalAgent = (...values) => {
134
+ const callSite = values.pop();
135
+ if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow agent call-site identity");
136
+ const inherited = inheritedAgentPath.getStore() || [];
137
+ const occurrenceKey = JSON.stringify([inherited, callSite]);
138
+ if (agentInflight.has(occurrenceKey)) throw workError("INVALID_METADATA", "Concurrent agent calls from the same source call site are unsupported; use parallel(...) or pipeline(...)");
139
+ agentInflight.add(occurrenceKey);
140
+ const occurrence = (agentOccurrences.get(occurrenceKey) || 0) + 1;
141
+ agentOccurrences.set(occurrenceKey, occurrence);
142
+ const options = values.length < 2 || values[1] === undefined ? {} : values[1];
143
+ const worktreeOwner = worktreeOwners.getStore();
144
+ const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
145
+ let result;
146
+ try {
147
+ result = rpc("agent", [values[0], options, identity]).then(unwrap);
148
+ } catch (error) {
149
+ agentInflight.delete(occurrenceKey);
150
+ throw error;
151
+ }
152
+ void result.then(() => agentInflight.delete(occurrenceKey), () => agentInflight.delete(occurrenceKey));
153
+ return guardedAgentResult(result);
154
+ };
155
+ const AGENT_HANDLE_KEYS = ["name", "role", "model", "tools", "skills", "extensions", "contextFiles", "label"];
156
+ const agentHandleNames = new Set();
157
+ const agentHandleInflight = new Set();
158
+ const internalAgentCreate = (...values) => {
159
+ if (values.length !== 1 || !values[0] || typeof values[0] !== "object" || Array.isArray(values[0])) throw workError("INVALID_METADATA", "agent.create requires one options object");
160
+ const options = values[0];
161
+ const name = named(options.name, "agent.create");
162
+ const unsupported = Object.keys(options).find(key => !AGENT_HANDLE_KEYS.includes(key));
163
+ if (unsupported) throw workError("INVALID_METADATA", "agent.create option is unsupported: " + unsupported);
164
+ if (agentHandleNames.has(name)) throw workError("INVALID_METADATA", "Duplicate agent handle name: " + name);
165
+ agentHandleNames.add(name);
166
+ const config = Object.freeze(Object.fromEntries(Object.entries(options).filter(([key]) => key !== "name")));
167
+ const worktreeOwner = worktreeOwners.getStore();
168
+ // The turn counter is arrival-ordered, so a handle must stay in the scope that created it: sends racing across
169
+ // parallel branches would otherwise take each other's journal path on replay.
170
+ const scope = JSON.stringify(inheritedAgentPath.getStore() || []);
171
+ let turn = 0;
172
+ const send = (...sent) => {
173
+ if (sent.length < 1 || sent.length > 2) throw workError("INVALID_METADATA", "send requires a prompt and optional options");
174
+ if (typeof sent[0] !== "string") throw workError("INVALID_METADATA", "send prompt must be a string");
175
+ const sendOptions = sent.length < 2 || sent[1] === undefined ? {} : sent[1];
176
+ if (!sendOptions || typeof sendOptions !== "object" || Array.isArray(sendOptions) || Object.keys(sendOptions).some(key => key !== "outputSchema" && key !== "timeoutMs")) throw workError("INVALID_METADATA", "send options must contain only outputSchema and timeoutMs");
177
+ if (JSON.stringify(inheritedAgentPath.getStore() || []) !== scope) throw workError("INVALID_METADATA", "Agent handle sends must run in the scope that created the handle: " + name);
178
+ if (agentHandleInflight.has(name)) throw workError("INVALID_METADATA", "Concurrent send(...) calls on one agent handle are unsupported; await each turn");
179
+ agentHandleInflight.add(name);
180
+ turn += 1;
181
+ const identity = { structuralPath: [...(inheritedAgentPath.getStore() || [])], callSite: "handle:" + name, occurrence: turn, handle: name, turn, ...(worktreeOwner ? { worktreeOwner } : {}) };
182
+ let result;
183
+ try {
184
+ result = rpc("agent", [sent[0], { ...config, ...sendOptions }, identity]).then(unwrap);
185
+ } catch (error) {
186
+ agentHandleInflight.delete(name);
187
+ throw error;
188
+ }
189
+ void result.then(() => agentHandleInflight.delete(name), () => agentHandleInflight.delete(name));
190
+ return guardedAgentResult(result);
191
+ };
192
+ return Object.freeze({ name, send });
193
+ };
194
+ const internalShell = (...values) => {
195
+ const callSite = values.pop();
196
+ if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow shell call-site identity");
197
+ if (values.length !== 1 && values.length !== 2) throw workError("INVALID_METADATA", "shell requires a command string and optional options");
198
+ const command = values[0];
199
+ if (typeof command !== "string") throw workError("INVALID_METADATA", "shell command must be a string");
200
+ const options = values.length < 2 || values[1] === undefined ? {} : values[1];
201
+ if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some(key => key !== "timeoutMs" && key !== "env")) throw workError("INVALID_METADATA", "shell options must contain only timeoutMs and env");
202
+ if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0)) throw workError("INVALID_METADATA", "shell timeoutMs must be a positive integer");
203
+ if (options.env !== undefined && (!options.env || typeof options.env !== "object" || Array.isArray(options.env) || Object.values(options.env).some(value => typeof value !== "string"))) throw workError("INVALID_METADATA", "shell env must be an object of strings");
204
+ const inherited = inheritedAgentPath.getStore() || [];
205
+ const worktreeOwner = worktreeOwners.getStore();
206
+ const occurrenceKey = JSON.stringify([inherited, callSite, worktreeOwner || null]);
207
+ const occurrence = (shellOccurrences.get(occurrenceKey) || 0) + 1;
208
+ shellOccurrences.set(occurrenceKey, occurrence);
209
+ const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
210
+ return guardedResult(rpc("shell", [command, options, identity]), "shell");
211
+ };
212
+ const shell = rejectShell;
213
+ const agent = Object.freeze(Object.assign(rejectAgent, { create: internalAgentCreate }));
214
+ const promptPath = (at, key) => /^[A-Za-z_$][\w$]*$/.test(key) ? at + "." + key : at + "[" + JSON.stringify(key) + "]";
215
+ const plainPromptObject = value => {
216
+ const proto = Object.getPrototypeOf(value);
217
+ return proto === null || Object.getPrototypeOf(proto) === null && Object.prototype.hasOwnProperty.call(proto, "constructor") && typeof proto.constructor === "function" && Function.prototype.toString.call(proto.constructor) === Function.prototype.toString.call(Object);
218
+ };
219
+ const promptValue = (value, at, seen) => {
220
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
221
+ if (typeof value === "number") { if (!Number.isFinite(value)) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" must be a finite number"); return; }
222
+ if (typeof value !== "object") throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" cannot be " + typeof value);
223
+ if (typeof value.then === "function") throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" is a Promise or thenable; await it before calling prompt()");
224
+ if (!Array.isArray(value) && !plainPromptObject(value)) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" must be a plain object");
225
+ if (seen.has(value)) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" contains a cycle");
226
+ const keys = Reflect.ownKeys(value);
227
+ const symbol = keys.find(key => typeof key === "symbol");
228
+ if (symbol) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" contains a symbol key");
229
+ seen.add(value);
230
+ if (Array.isArray(value)) {
231
+ for (let index = 0; index < value.length; index += 1) promptProperty(value, String(index), at + "[" + index + "]", seen);
232
+ for (const key of keys) {
233
+ const index = Number(key);
234
+ if (key !== "length" && !(Number.isInteger(index) && index >= 0 && index < value.length && String(index) === key)) promptProperty(value, key, promptPath(at, key), seen);
235
+ }
236
+ } else for (const key of keys) promptProperty(value, key, promptPath(at, key), seen);
237
+ seen.delete(value);
238
+ };
239
+ const promptProperty = (value, key, at, seen) => {
240
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
241
+ if (descriptor && (descriptor.get || descriptor.set)) throw workError("INVALID_METADATA", "Prompt value \"" + at + "\" cannot use getters or setters");
242
+ promptValue(descriptor && descriptor.value, at, seen);
243
+ };
244
+ const prompt = (template, values) => {
245
+ if (typeof template !== "string") throw workError("INVALID_METADATA", "prompt() template must be a string");
246
+ if (!values || typeof values !== "object" || Array.isArray(values) || !plainPromptObject(values)) throw workError("INVALID_METADATA", "prompt() values must be a plain object");
247
+ const placeholders = [...template.matchAll(/{{|}}|{([A-Za-z_$][\w$]*)}/g)].flatMap(match => match[1] === undefined ? [] : [match[1]]);
248
+ const used = new Set(placeholders);
249
+ const keys = Reflect.ownKeys(values);
250
+ const symbol = keys.find(key => typeof key === "symbol");
251
+ if (symbol) throw workError("INVALID_METADATA", "prompt() values must use string keys");
252
+ const missing = placeholders.find(key => !Object.prototype.hasOwnProperty.call(values, key));
253
+ if (missing) throw workError("INVALID_METADATA", "Missing prompt value \"" + missing + "\"");
254
+ const unused = keys.find(key => !used.has(key));
255
+ if (unused !== undefined) throw workError("INVALID_METADATA", "Unused prompt value \"" + unused + "\"");
256
+ for (const key of keys) promptProperty(values, key, key, new Set());
257
+ return template.replace(/{{|}}|{([A-Za-z_$][\w$]*)}/g, (match, key) => match === "{{" ? "{" : match === "}}" ? "}" : typeof values[key] === "string" ? values[key] : JSON.stringify(values[key], null, 2));
258
+ };
259
+ const checkpoint = input => rpc("checkpoint", [input]).then(unwrap);
260
+ const phase = name => rpc("phase", [name]);
261
+ const log = message => rpc("log", [message]);
262
+ const functionOccurrences = new Map();
263
+ const functionIdentity = name => {
264
+ const structuralPath = inheritedAgentPath.getStore() || [];
265
+ const key = JSON.stringify([structuralPath, name]);
266
+ const occurrence = (functionOccurrences.get(key) || 0) + 1;
267
+ functionOccurrences.set(key, occurrence);
268
+ const worktreeOwner = worktreeOwners.getStore();
269
+ return { path: path("function", ...structuralPath, name, String(occurrence)), structuralPath: [...structuralPath], occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
270
+ };
271
+ const functions = Object.freeze(Object.fromEntries(Object.entries(config.functions || {}).map(([local, target]) => [local, (...values) => {
272
+ if (values.length !== 1 || !values[0] || typeof values[0] !== "object" || Array.isArray(values[0])) throw workError("RESULT_INVALID", local + " requires exactly one JSON object argument");
273
+ const identity = functionIdentity(target.name);
274
+ const result = rpc("function", [target.name, values[0], identity]).then(unwrap);
275
+ Object.defineProperty(result, "toJSON", { value() { throw workError("INVALID_METADATA", "Workflow function result is a Promise; await it before serialization"); } });
276
+ return result;
277
+ }])));
278
+ const recordEntries = (value, kind) => {
279
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw workError("INVALID_METADATA", kind + " must be a record");
280
+ return Object.entries(value);
281
+ };
282
+ const parallel = async (operationName, tasks) => {
283
+ named(operationName, "parallel");
284
+ const entries = recordEntries(tasks, "parallel tasks");
285
+ for (const [name, run] of entries) {
286
+ named(name, "parallel task");
287
+ if (typeof run !== "function") throw workError("INVALID_METADATA", "parallel task values must be run functions");
288
+ }
289
+ const results = await Promise.all(entries.map(async ([name, run]) => {
290
+ // Captured outside the scope's run() so the fallback failedAt below is absolute: a plain throw
291
+ // inside a nested scope must still name the full outer path, or sibling scopes with the same
292
+ // inner names become indistinguishable in diagnostics and retry provenance.
293
+ const parent = inheritedAgentPath.getStore() || [];
294
+ try {
295
+ return { name, ok: true, value: await inheritedAgentPath.run([...parent, operationName, name], run) };
296
+ } catch (error) {
297
+ if (errorCode(error) === "CANCELLED") throw error;
298
+ const failedAt = error && typeof error === "object" && typeof error.failedAt === "string" ? error.failedAt : undefined;
299
+ return { name, ok: false, failedAt: failedAt || path(...parent, operationName, name), error: workerError(error) };
300
+ }
301
+ }));
302
+ const failure = results.find(result => !result.ok);
303
+ if (failure) throw Object.assign(workflowError(failure.error), { failedAt: failure.failedAt });
304
+ return Object.fromEntries(results.map(result => [result.name, result.value]));
305
+ };
306
+ const pipeline = async (operationName, items, stages) => {
307
+ named(operationName, "pipeline");
308
+ const itemEntries = recordEntries(items, "pipeline items");
309
+ const stageEntries = recordEntries(stages, "pipeline stages");
310
+ if (!stageEntries.length) throw workError("INVALID_METADATA", "pipeline requires at least one stage");
311
+ for (const [name] of itemEntries) named(name, "pipeline item");
312
+ for (const [stageName, run] of stageEntries) {
313
+ named(stageName, "pipeline stage");
314
+ if (typeof run !== "function") throw workError("INVALID_METADATA", "pipeline stage values must be run functions");
315
+ }
316
+ const results = await Promise.all(itemEntries.map(async ([name, initial]) => {
317
+ let value = initial;
318
+ // See the parallel fallback above: parent makes the stage-level failedAt fallback absolute.
319
+ const parent = inheritedAgentPath.getStore() || [];
320
+ let failedAt = path(...parent, operationName, name);
321
+ try {
322
+ for (const [stageName, run] of stageEntries) {
323
+ failedAt = path(...parent, operationName, name, stageName);
324
+ value = await inheritedAgentPath.run([...parent, operationName, name, stageName], () => run(value));
325
+ }
326
+ return { name, ok: true, value };
327
+ } catch (error) {
328
+ if (errorCode(error) === "CANCELLED") throw error;
329
+ const nestedFailedAt = error && typeof error === "object" && typeof error.failedAt === "string" ? error.failedAt : undefined;
330
+ return { name, ok: false, failedAt: nestedFailedAt || failedAt, error: workerError(error) };
331
+ }
332
+ }));
333
+ const failure = results.find(result => !result.ok);
334
+ if (failure) throw Object.assign(workflowError(failure.error), { failedAt: failure.failedAt });
335
+ return Object.fromEntries(results.map(result => [result.name, result.value]));
336
+ };
337
+ const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
338
+ const sandbox = { agent, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
339
+ for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
340
+ for (const name of ["Date","eval","Function","WebAssembly","process","require","module","exports","console","fetch","XMLHttpRequest","WebSocket","performance","crypto","setTimeout","setInterval","setImmediate","queueMicrotask","Intl","SharedArrayBuffer","Atomics"]) sandbox[name] = undefined;
341
+ const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
342
+ const body = config.script;
343
+ Promise.resolve().then(() => new vm.Script("(async(__pi_extensible_workflows_agent,__pi_extensible_workflows_withWorktree,__pi_extensible_workflows_shell)=>{" + body + "\n})", { filename: "workflow.js" }).runInContext(context)(internalAgent, internalWithWorktree, internalShell))
344
+ .then(async value => { await Promise.all(inflight); send({ type: "result", value: value === undefined ? null : value }); })
345
+ .catch(error => send({ type: "error", error: workerError(error) }))
346
+ .finally(() => clearInterval(heartbeat));
347
+ `;
348
+
349
+ export function encoded(value: JsonValue): string;
350
+ export function encoded(value: unknown): asserts value is JsonValue;
351
+ export function encoded(value: unknown): string {
352
+ if (!jsonValue(value)) fail("RPC_LIMIT_EXCEEDED", "RPC values must be JSON-compatible");
353
+ const json = JSON.stringify(value);
354
+ if (Buffer.byteLength(json) > RPC_LIMIT_BYTES) fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
355
+ return json;
356
+ }
357
+
358
+ function encodedRpcResult(id: number, value: JsonValue): string {
359
+ return encoded({ type: "rpcResult", id, ok: true, value });
360
+ }
361
+
362
+ function readAgentIdentity(value: unknown): AgentIdentity {
363
+ if (!object(value)) fail("INTERNAL_ERROR", "Invalid workflow agent identity");
364
+ const structuralPath = value.structuralPath;
365
+ const callSite = value.callSite;
366
+ const occurrence = value.occurrence;
367
+ const worktreeOwner = value.worktreeOwner;
368
+ const parentBreadcrumb = value.parentBreadcrumb;
369
+ const handle = value.handle;
370
+ const turn = value.turn;
371
+ if (!Array.isArray(structuralPath) || !structuralPath.every((part): part is string => typeof part === "string" && Boolean(part.trim())) || typeof callSite !== "string" || !callSite || !positiveInteger(occurrence) || parentBreadcrumb !== undefined && (typeof parentBreadcrumb !== "string" || !parentBreadcrumb.trim()) || worktreeOwner !== undefined && (typeof worktreeOwner !== "string" || !worktreeOwner) || handle !== undefined && (typeof handle !== "string" || !handle.trim() || !positiveInteger(turn))) fail("INTERNAL_ERROR", "Invalid workflow agent identity");
372
+ return { structuralPath: [...structuralPath], callSite, occurrence, ...(typeof parentBreadcrumb === "string" ? { parentBreadcrumb } : {}), ...(typeof worktreeOwner === "string" ? { worktreeOwner } : {}), ...(typeof handle === "string" ? { handle, turn: turn as number } : {}) };
373
+ }
374
+ function readFunctionIdentity(value: unknown): FunctionIdentity {
375
+ if (!object(value)) fail("INTERNAL_ERROR", "Invalid workflow function identity");
376
+ const path = value.path;
377
+ const structuralPath = value.structuralPath;
378
+ const occurrence = value.occurrence;
379
+ const worktreeOwner = value.worktreeOwner;
380
+ if (typeof path !== "string" || !path || !Array.isArray(structuralPath) || !structuralPath.every((part): part is string => typeof part === "string" && Boolean(part.trim())) || !positiveInteger(occurrence) || worktreeOwner !== undefined && (typeof worktreeOwner !== "string" || !worktreeOwner)) fail("INTERNAL_ERROR", "Invalid workflow function identity");
381
+ return { path, structuralPath: [...structuralPath], occurrence, ...(typeof worktreeOwner === "string" ? { worktreeOwner } : {}) };
382
+ }
383
+ function readShellIdentity(value: unknown): ShellIdentity {
384
+ const identity = readAgentIdentity(value);
385
+ return { structuralPath: identity.structuralPath, callSite: identity.callSite, occurrence: identity.occurrence, ...(identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {}) };
386
+ }
387
+ export function agentHandleTurnPath(handle: string, turn: number): string {
388
+ return operationPath("agent", "handle", handle, `turn:${String(turn)}`);
389
+ }
390
+ export function agentIdentityPath(identity: AgentIdentity): string {
391
+ if (identity.handle !== undefined && identity.turn !== undefined) return agentHandleTurnPath(identity.handle, identity.turn);
392
+ return operationPath("agent", ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
393
+ }
394
+ export function shellIdentityPath(identity: ShellIdentity): string {
395
+ return operationPath("shell", ...(identity.worktreeOwner ? ["worktree", identity.worktreeOwner] : []), ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
396
+ }
397
+ export function readShellResult(value: unknown): ShellResult {
398
+ if (!object(value) || (value.exitCode !== null && !Number.isInteger(value.exitCode)) || typeof value.stdout !== "string" || typeof value.stderr !== "string") fail("SHELL_FAILED", "Shell bridge returned an invalid result");
399
+ return { exitCode: value.exitCode as number | null, stdout: value.stdout, stderr: value.stderr };
400
+ }
401
+ export function agentWorktree(identity: AgentIdentity): { worktreeOwner?: string } {
402
+ return identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {};
403
+ }
404
+ function shellProcessKill(child: ChildProcess): void {
405
+ let forceKill: ReturnType<typeof setTimeout> | undefined;
406
+ const killProcessTree = (signal: "SIGTERM" | "SIGKILL") => {
407
+ try {
408
+ if (child.pid && process.platform !== "win32") process.kill(-child.pid, signal);
409
+ else if (child.pid && process.platform === "win32") {
410
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
411
+ killer.unref();
412
+ } else child.kill(signal);
413
+ } catch {
414
+ try { child.kill(signal); } catch { /* The process may already have exited. */ }
415
+ }
416
+ };
417
+ child.once("close", () => { if (forceKill) clearTimeout(forceKill); });
418
+ killProcessTree("SIGTERM");
419
+ forceKill = setTimeout(() => { forceKill = undefined; killProcessTree("SIGKILL"); }, 1000);
420
+ forceKill.unref();
421
+ }
422
+ export function executeShellCommand(command: string, options: ShellOptions, signal: AbortSignal, cwd = process.cwd()): Promise<ShellResult> {
423
+ return new Promise((resolve, reject) => {
424
+ let child: ChildProcess;
425
+ try { child = spawn(command, { shell: true, cwd, env: { ...process.env, ...(options.env ?? {}) }, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] }); }
426
+ catch (error) { reject(new WorkflowError("SHELL_FAILED", errorText(error))); return; }
427
+ let settled = false;
428
+ let timedOut = false;
429
+ let outputBytes = 0;
430
+ let timeout: ReturnType<typeof setTimeout> | undefined;
431
+ const stdoutDecoder = new StringDecoder("utf8");
432
+ const stderrDecoder = new StringDecoder("utf8");
433
+ let stdout = "";
434
+ let stderr = "";
435
+ const cleanup = () => {
436
+ if (timeout) clearTimeout(timeout);
437
+ signal.removeEventListener("abort", onAbort);
438
+ };
439
+ const failShell = (error: WorkflowError) => {
440
+ if (settled) return;
441
+ settled = true;
442
+ cleanup();
443
+ shellProcessKill(child);
444
+ reject(error);
445
+ };
446
+ const onAbort = () => { failShell(new WorkflowError("CANCELLED", "Workflow cancelled")); };
447
+ const capture = (target: "stdout" | "stderr", chunk: Buffer) => {
448
+ if (settled) return;
449
+ outputBytes += chunk.byteLength;
450
+ if (outputBytes > RPC_LIMIT_BYTES) { failShell(new WorkflowError("RPC_LIMIT_EXCEEDED", "Shell result exceeds the 10 MB JSON boundary")); return; }
451
+ if (target === "stdout") stdout += stdoutDecoder.write(chunk); else stderr += stderrDecoder.write(chunk);
452
+ };
453
+ child.stdout?.on("data", (chunk: Buffer) => { capture("stdout", chunk); });
454
+ child.stderr?.on("data", (chunk: Buffer) => { capture("stderr", chunk); });
455
+ child.once("error", (error) => { failShell(new WorkflowError("SHELL_FAILED", errorText(error))); });
456
+ child.once("close", (exitCode) => {
457
+ if (settled) return;
458
+ settled = true;
459
+ cleanup();
460
+ stdout += stdoutDecoder.end();
461
+ stderr += stderrDecoder.end();
462
+ if (signal.aborted) { reject(new WorkflowError("CANCELLED", "Workflow cancelled")); return; }
463
+ if (timedOut) { reject(new WorkflowError("SHELL_FAILED", `Shell command timed out after ${String(options.timeoutMs)}ms`)); return; }
464
+ const result = { exitCode: exitCode === null ? null : exitCode, stdout, stderr };
465
+ try { encodedRpcResult(Number.MAX_SAFE_INTEGER, result); } catch (error) { reject(error instanceof WorkflowError ? error : new WorkflowError("RPC_LIMIT_EXCEEDED", errorText(error))); return; }
466
+ resolve(result);
467
+ });
468
+ if (signal.aborted) { onAbort(); return; }
469
+ signal.addEventListener("abort", onAbort, { once: true });
470
+ if (options.timeoutMs !== undefined) timeout = setTimeout(() => { timedOut = true; shellProcessKill(child); }, options.timeoutMs);
471
+ });
472
+ }
473
+ /** The wire shape of a host-side failure: the code, the message, and the authored flag that tells the worker whether the text is safe to surface verbatim. */
474
+ function workerErrorShape(typed: WorkflowError): WorkerErrorShape {
475
+ return { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) };
476
+ }
477
+ function workflowErrorFromWorker(error: WorkerErrorShape): WorkflowError {
478
+ const code = isWorkflowErrorCode(error.code) ? error.code : "INTERNAL_ERROR";
479
+ const typed = markWorkflowAuthored(new WorkflowError(code, error.message), Boolean(error.authored) || error.code === undefined);
480
+ return error.failedAt === undefined ? typed : Object.assign(typed, { failedAt: error.failedAt });
481
+ }
482
+ export function runWorkflow(script: string, args: JsonValue = null, bridge: WorkflowBridge = {}, signal?: AbortSignal): WorkflowExecution {
483
+ encoded(args);
484
+ const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {} });
485
+ const childDir = realpathSync(mkdtempSync(join(tmpdir(), "pi-wf-")));
486
+ const childFile = join(childDir, "child.cjs");
487
+ writeFileSync(childFile, childSource);
488
+ const child: ChildProcess = fork(childFile, [String(RPC_LIMIT_BYTES), config], {
489
+ execArgv: (() => {
490
+ const filtered: string[] = [];
491
+ const skip = new Set(["--input-type", "-e", "--eval", "-p", "--print"]);
492
+ let skipNext = false;
493
+ for (const arg of process.execArgv) {
494
+ if (skipNext) { skipNext = false; continue; }
495
+ if (skip.has(arg) || skip.has(arg.split("=")[0] ?? "")) { if (!arg.includes("=")) skipNext = true; continue; }
496
+ filtered.push(arg);
497
+ }
498
+ // NOTE: Bun has no --permission model and rejects/ignores these Node flags; the worker keeps only the vm sandbox there.
499
+ return process.versions.bun ? filtered : [...filtered, "--max-old-space-size=128", "--permission", `--allow-fs-read=${childDir}`];
500
+ })(),
501
+ // NOTE: a Bun-compiled pi binary is process.execPath; BUN_BE_BUN makes it run child.cjs instead of the pi CLI.
502
+ env: process.versions.bun ? { ...process.env, BUN_BE_BUN: "1" } : process.env,
503
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
504
+ serialization: "advanced",
505
+ });
506
+ const controller = new AbortController();
507
+ let settled = false;
508
+ let rejectResult: (error: WorkflowError) => void = () => undefined;
509
+ let lastHeartbeatAt = performance.now();
510
+ let lastWatchdogCheckAt = lastHeartbeatAt;
511
+ const watchdog = setInterval(() => {
512
+ const now = performance.now();
513
+ // A check delayed enough to bridge the normal heartbeat margin makes elapsed time unreliable.
514
+ const watchdogCheckWasDelayed = now - lastWatchdogCheckAt >= HEARTBEAT_TIMEOUT_MS - HEARTBEAT_INTERVAL_MS;
515
+ lastWatchdogCheckAt = now;
516
+ if (watchdogCheckWasDelayed) { lastHeartbeatAt = now; return; }
517
+ if (now - lastHeartbeatAt >= HEARTBEAT_TIMEOUT_MS) stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat");
518
+ }, HEARTBEAT_INTERVAL_MS);
519
+ const result = new Promise<JsonValue>((resolve, reject) => {
520
+ rejectResult = reject;
521
+ child.on("message", (raw: unknown) => {
522
+ try {
523
+ if (typeof raw !== "string" || Buffer.byteLength(raw) > RPC_LIMIT_BYTES) fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
524
+ const parsed: unknown = JSON.parse(raw);
525
+ if (!isRpcMessage(parsed)) fail("INTERNAL_ERROR", "Worker RPC message is invalid");
526
+ const message = parsed;
527
+ if (message.type === "heartbeat") { lastHeartbeatAt = performance.now(); return; }
528
+ if (message.type === "result") { encoded(message.value); finish(); resolve(message.value); return; }
529
+ if (message.type === "error") { controller.abort(); finish(); reject(workflowErrorFromWorker(message.error)); return; }
530
+ if (message.type === "rpc") void handleRpc(message.id, message.method, message.args);
531
+ } catch (error) { stop(error instanceof WorkflowError ? error.code : "INTERNAL_ERROR", errorText(error)); }
532
+ });
533
+ child.on("error", (error: Error) => { stop("INTERNAL_ERROR", error.message); });
534
+ child.on("exit", (code) => { if (!settled && code !== 0) stop("INTERNAL_ERROR", `Workflow child exited with code ${String(code)}`); });
535
+ });
536
+ function killChild() {
537
+ if (!child.killed) {
538
+ child.kill("SIGTERM");
539
+ setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 1000).unref();
540
+ }
541
+ }
542
+ function finish() { settled = true; clearInterval(watchdog); signal?.removeEventListener("abort", cancel); killChild(); rmSync(childDir, { recursive: true, force: true }); }
543
+ function stop(code: WorkflowErrorCode, message: string) { if (settled) return; controller.abort(); finish(); rejectResult(new WorkflowError(code, message)); }
544
+ function branded(result: Record<string, JsonValue>): JsonValue { return { ...result, [WORK_RESULT_BRAND]: true }; }
545
+ async function handleRpc(id: number, method: string, values: JsonValue[]) {
546
+ try {
547
+ encoded(values);
548
+ let value: JsonValue = null;
549
+ if (method === "agent") {
550
+ if (!bridge.agent) fail("AGENT_FAILED", "No agent bridge is available");
551
+ if (typeof values[0] !== "string") fail("INTERNAL_ERROR", "agent prompt must be a string");
552
+ const opts = validateAgentOptions(values[1]);
553
+ const identity = readAgentIdentity(values[2]);
554
+ const path = agentIdentityPath(identity);
555
+ const label = typeof opts.label === "string" ? opts.label : roleNameOf(opts.role) ?? "agent";
556
+ try {
557
+ const result = await bridge.agent(values[0], opts, controller.signal, identity);
558
+ value = branded({ name: label, ok: true, value: result ?? null });
559
+ } catch (error) {
560
+ const typed = asWorkflowError(error);
561
+ if (!OUTCOME_ERRORS.has(typed.code)) throw typed;
562
+ value = branded({ name: label, ok: false, failedAt: path, error: workerErrorShape(typed) });
563
+ }
564
+ } else if (method === "shell") {
565
+ if (!bridge.shell) fail("SHELL_FAILED", "No shell bridge is available");
566
+ const command = validateShellCommand(values[0]);
567
+ const options = validateShellOptions(values[1]);
568
+ const identity = readShellIdentity(values[2]);
569
+ const result = readShellResult(await bridge.shell(command, options, controller.signal, identity));
570
+ if (!jsonValue(result)) fail("RPC_LIMIT_EXCEEDED", "RPC values must be JSON-compatible");
571
+ value = result;
572
+ } else if (method === "checkpoint") {
573
+ if (!bridge.checkpoint || !object(values[0])) fail("INTERNAL_ERROR", "checkpoint requires an available bridge and object input");
574
+ const name = typeof values[0].name === "string" ? values[0].name : "checkpoint";
575
+ try {
576
+ const result = await bridge.checkpoint(values[0], controller.signal);
577
+ if (typeof result !== "boolean") fail("INTERNAL_ERROR", "checkpoint must return a boolean");
578
+ value = branded({ name, ok: true, value: result ? "approved" : "rejected" });
579
+ } catch (error) {
580
+ const typed = asWorkflowError(error);
581
+ if (!OUTCOME_ERRORS.has(typed.code)) throw typed;
582
+ value = branded({ name, ok: false, failedAt: name, error: workerErrorShape(typed) });
583
+ }
584
+ } else if (method === "function") {
585
+ if (!bridge.function || typeof values[0] !== "string" || !object(values[1])) fail("INTERNAL_ERROR", "function requires an available bridge, name, and object input");
586
+ const identity = readFunctionIdentity(values[2]);
587
+ const name = values[0];
588
+ const expectedPath = operationPath("function", ...identity.structuralPath, name, String(identity.occurrence));
589
+ if (identity.path !== expectedPath) fail("INTERNAL_ERROR", "Workflow function identity path is inconsistent");
590
+ try {
591
+ const result = await bridge.function(values[0], values[1], controller.signal, identity);
592
+ value = branded({ name, ok: true, value: result ?? null });
593
+ } catch (error) {
594
+ const typed = asWorkflowError(error);
595
+ if (!OUTCOME_ERRORS.has(typed.code)) throw typed;
596
+ value = branded({ name, ok: false, failedAt: name, error: workerErrorShape(typed) });
597
+ }
598
+ } else if (method === "worktree") {
599
+ if (!bridge.worktree || typeof values[0] !== "string" || !values[0]) fail("INTERNAL_ERROR", "worktree requires an active host bridge and scope");
600
+ value = await bridge.worktree(values[0], controller.signal);
601
+ } else if (method === "phase") {
602
+ if (typeof values[0] !== "string") fail("INTERNAL_ERROR", "phase name must be a string");
603
+ await bridge.phase?.(values[0]);
604
+ } else if (method === "log") {
605
+ if (typeof values[0] !== "string") fail("INTERNAL_ERROR", "log message must be a string");
606
+ await bridge.log?.(values[0]);
607
+ }
608
+ else fail("INTERNAL_ERROR", `Unknown worker RPC method: ${method}`);
609
+ encoded(value);
610
+ child.send(encodedRpcResult(id, value));
611
+ } catch (error) {
612
+ const typed = asWorkflowError(error);
613
+ child.send(encoded({ type: "rpcResult", id, ok: false, error: workerErrorShape(typed) }));
614
+ }
615
+ }
616
+ function cancel() {
617
+ if (settled) return;
618
+ controller.abort();
619
+ child.send(encoded({ type: "cancel" }));
620
+ stop("CANCELLED", "Workflow cancelled");
621
+ }
622
+ if (signal?.aborted) cancel(); else signal?.addEventListener("abort", cancel, { once: true });
623
+ return { result, cancel };
624
+ }
625
+ function attemptSummary(attempt: AgentAttempt, accounting = attempt.accounting): AgentAttemptSummary {
626
+ return { attempt: attempt.attempt, transport: attempt.transport, ...(attempt.session ? { session: attempt.session } : {}), ...(attempt.error ? { error: attempt.error } : {}), accounting, setup: attempt.setup };
627
+ }
628
+ export async function persistActiveAgentAttempt(store: RunStore, id: string, active: AgentAttempt): Promise<void> {
629
+ await store.updateState((run) => {
630
+ const agent = run.agents.find((candidate) => candidate.id === id);
631
+ if (!agent) throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
632
+ const accounting = zeroAccounting();
633
+ const detail = attemptSummary(active, accounting);
634
+ const details = [...(agent.attemptDetails ?? []).filter((candidate) => candidate.attempt !== active.attempt), detail];
635
+ const session = active.session;
636
+ const agentSessions = session && !run.agentSessions.some(({ transport, sessionId }) => transport === session.transport && sessionId === session.sessionId) ? [...run.agentSessions, session] : run.agentSessions;
637
+ return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: Math.max(candidate.attempts, active.attempt), attemptDetails: details } : candidate), agentSessions };
638
+ });
639
+ }
640
+
641
+ export async function persistAgentAttempts(store: RunStore, id: string, attempts: readonly AgentAttempt[]): Promise<void> {
642
+ await store.updateState((run) => {
643
+ const agent = run.agents.find((candidate) => candidate.id === id);
644
+ if (!agent) throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
645
+ const total = sumAccounting(attempts.map((attempt) => attempt.accounting));
646
+ const attemptDetails = attempts.map((attempt) => attemptSummary(attempt));
647
+ const sessions = attempts.map((attempt) => attempt.session).filter((session): session is WorkflowAgentSessionReference => session !== undefined);
648
+ const sessionKeys = new Set(sessions.map(({ transport, sessionId }) => `${transport}:${sessionId}`));
649
+ 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] };
650
+ });
651
+ }
652
+ export type { AgentIdentity, FunctionIdentity, ShellIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";