pi-ultracode 0.1.2 → 0.3.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.
@@ -0,0 +1,317 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import {
3
+ DEFAULT_WORKFLOW_CHECKPOINT_LIMIT,
4
+ } from "./parser.ts";
5
+ import {
6
+ WorkflowAbortError,
7
+ WorkflowPolicyError,
8
+ WorkflowStallError,
9
+ WORKFLOW_ABORT_ERROR_CODE,
10
+ WORKFLOW_POLICY_ERROR_CODE,
11
+ WORKFLOW_STALL_ERROR_CODE,
12
+ type PanelReservation,
13
+ } from "./admission.ts";
14
+
15
+ export interface ScriptExecutorHost {
16
+ agent(payload: any): Promise<unknown>;
17
+ reservePanel(payload: {
18
+ callPath?: unknown;
19
+ reserveAgents: number;
20
+ branchCount: number;
21
+ parentReservationIds?: string[];
22
+ }): Promise<PanelReservation>;
23
+ completePanelBranch(payload: {
24
+ callPath?: unknown;
25
+ branchIndex: number;
26
+ outcome: "success" | "failed";
27
+ }): Promise<void>;
28
+ releasePanel(payload: {
29
+ reservationId: string;
30
+ callPath?: unknown;
31
+ completed?: boolean;
32
+ branchOutcomes?: unknown;
33
+ }): Promise<void>;
34
+ loadWorkflow(payload: { nameOrRef: unknown; callPath?: unknown; args?: unknown }): Promise<{ meta: unknown; body: string }>;
35
+ validateOutput(payload: { value: unknown; label?: unknown }): Promise<void>;
36
+ log(message: string): void;
37
+ phase(title: string): void;
38
+ }
39
+
40
+ export interface ScriptExecutorOptions {
41
+ cwd: string;
42
+ args?: unknown;
43
+ name: string;
44
+ signal: AbortSignal;
45
+ checkpointLimit?: number;
46
+ /** Internal test seam: how long the worker may go without a heartbeat. Not a public workflow option. */
47
+ stallTimeoutMs?: number;
48
+ /** Internal test seam for host-call fuel. */
49
+ hostCallLimit?: number;
50
+ /** Internal test seam for the Worker's V8 old-generation heap cap. */
51
+ workerMemoryLimitMb?: number;
52
+ }
53
+
54
+ type TerminationCause = "abort" | "stall" | "fatal" | "error" | "cleanup";
55
+
56
+ export async function executeWorkflowScript(
57
+ body: string,
58
+ host: ScriptExecutorHost,
59
+ options: ScriptExecutorOptions,
60
+ ): Promise<unknown> {
61
+ if (options.signal.aborted) throw new WorkflowAbortError();
62
+ const worker = new Worker(new URL("./script-worker.mjs", import.meta.url), {
63
+ workerData: {
64
+ body,
65
+ args: options.args,
66
+ cwd: options.cwd,
67
+ name: options.name,
68
+ checkpointLimit: options.checkpointLimit ?? DEFAULT_WORKFLOW_CHECKPOINT_LIMIT,
69
+ hostCallLimit: normalizeHostCallLimit(options.hostCallLimit),
70
+ },
71
+ resourceLimits: workerResourceLimits(options.workerMemoryLimitMb),
72
+ });
73
+ const stallTimeoutMs = options.stallTimeoutMs ?? 5_000;
74
+ let lastHeartbeat = Date.now();
75
+ let lastProgress = lastHeartbeat;
76
+ let lastCheckpointCount = 0;
77
+ let pendingRpcCount = 0;
78
+ let settled = false;
79
+ let terminationCause: TerminationCause | undefined;
80
+ let fatalError: Error | undefined;
81
+ let watchdog: NodeJS.Timeout | undefined;
82
+
83
+ const terminate = (cause: TerminationCause): void => {
84
+ terminationCause ??= cause;
85
+ void worker.terminate().catch(() => undefined);
86
+ };
87
+
88
+ const abortWorker = () => {
89
+ if (settled) return;
90
+ terminate("abort");
91
+ };
92
+ options.signal.addEventListener("abort", abortWorker, { once: true });
93
+
94
+ try {
95
+ const result = await new Promise<unknown>((resolve, reject) => {
96
+ const settle = (ok: boolean, value: unknown): void => {
97
+ if (settled) return;
98
+ settled = true;
99
+ if (watchdog) clearInterval(watchdog);
100
+ if (ok) resolve(value);
101
+ else reject(value);
102
+ };
103
+ const rejectAndTerminate = (error: unknown, cause: TerminationCause): void => {
104
+ if (cause === "fatal" && error instanceof Error) fatalError ??= error;
105
+ settle(false, error);
106
+ terminate(cause);
107
+ };
108
+
109
+ watchdog = setInterval(() => {
110
+ if (settled) return;
111
+ const now = Date.now();
112
+ if (now - lastHeartbeat > stallTimeoutMs) {
113
+ if (options.signal.aborted) {
114
+ rejectAndTerminate(new WorkflowAbortError(), "abort");
115
+ } else {
116
+ rejectAndTerminate(new WorkflowStallError(`workflow script worker became unresponsive for ${stallTimeoutMs}ms`), "stall");
117
+ }
118
+ return;
119
+ }
120
+ if (pendingRpcCount === 0 && now - lastProgress > stallTimeoutMs) {
121
+ rejectAndTerminate(
122
+ new WorkflowStallError(`workflow script made no script progress for ${stallTimeoutMs}ms`),
123
+ "stall",
124
+ );
125
+ }
126
+ }, Math.max(10, Math.min(250, Math.floor(stallTimeoutMs / 2))));
127
+ watchdog.unref?.();
128
+
129
+ if (options.signal.aborted) {
130
+ rejectAndTerminate(new WorkflowAbortError(), "abort");
131
+ return;
132
+ }
133
+
134
+ worker.on("message", (message: any) => {
135
+ if (settled || !message || typeof message !== "object") return;
136
+ if (message.type === "heartbeat") {
137
+ lastHeartbeat = Date.now();
138
+ const checkpoints = Number.isFinite(message.checkpoints) ? Number(message.checkpoints) : lastCheckpointCount;
139
+ if (checkpoints !== lastCheckpointCount) {
140
+ lastCheckpointCount = checkpoints;
141
+ lastProgress = lastHeartbeat;
142
+ }
143
+ return;
144
+ }
145
+ if (message.type === "fatal") {
146
+ const error = reviveError(message.error);
147
+ fatalError = error;
148
+ rejectAndTerminate(error, "fatal");
149
+ return;
150
+ }
151
+ if (message.type === "event") {
152
+ lastProgress = Date.now();
153
+ try {
154
+ if (message.op === "log") host.log(String(message.payload?.message ?? ""));
155
+ else if (message.op === "phase") host.phase(String(message.payload?.title ?? ""));
156
+ } catch (error) {
157
+ rejectAndTerminate(error, "error");
158
+ }
159
+ return;
160
+ }
161
+ if (message.type === "rpc") {
162
+ pendingRpcCount++;
163
+ lastProgress = Date.now();
164
+ void handleRpc(message).finally(() => {
165
+ pendingRpcCount = Math.max(0, pendingRpcCount - 1);
166
+ lastProgress = Date.now();
167
+ });
168
+ return;
169
+ }
170
+ if (message.type === "result") {
171
+ if (message.ok) settle(true, message.value);
172
+ else {
173
+ const error = reviveError(message.error);
174
+ if (error instanceof WorkflowPolicyError) fatalError ??= error;
175
+ settle(false, fatalError ?? error);
176
+ }
177
+ }
178
+ });
179
+
180
+ worker.on("error", (error: NodeJS.ErrnoException) => {
181
+ if (settled) return;
182
+ if (error.code === "ERR_WORKER_OUT_OF_MEMORY") {
183
+ settle(false, new WorkflowPolicyError("workflow script exceeded its worker memory limit"));
184
+ return;
185
+ }
186
+ settle(false, error);
187
+ });
188
+
189
+ worker.on("exit", (code) => {
190
+ if (settled) return;
191
+ if (fatalError) {
192
+ settle(false, fatalError);
193
+ } else if (terminationCause === "abort" || options.signal.aborted) {
194
+ settle(false, new WorkflowAbortError());
195
+ } else if (terminationCause === "stall") {
196
+ settle(false, new WorkflowStallError(`workflow script worker became unresponsive for ${stallTimeoutMs}ms`));
197
+ } else if (code === 0) {
198
+ settle(false, new Error("workflow script worker exited without returning a result"));
199
+ } else {
200
+ settle(false, new Error(`workflow script worker exited with code ${code}`));
201
+ }
202
+ });
203
+
204
+ async function handleRpc(message: any): Promise<void> {
205
+ const { id, op, payload } = message;
206
+ try {
207
+ let value: unknown;
208
+ switch (op) {
209
+ case "agent":
210
+ value = await host.agent(payload);
211
+ break;
212
+ case "reservePanel":
213
+ value = await host.reservePanel(payload);
214
+ break;
215
+ case "completePanelBranch":
216
+ await host.completePanelBranch(payload);
217
+ value = undefined;
218
+ break;
219
+ case "releasePanel":
220
+ await host.releasePanel(payload);
221
+ value = undefined;
222
+ break;
223
+ case "loadWorkflow":
224
+ value = await host.loadWorkflow(payload);
225
+ break;
226
+ case "validateOutput":
227
+ await host.validateOutput(payload);
228
+ value = undefined;
229
+ break;
230
+ default:
231
+ throw new WorkflowPolicyError(`unknown workflow host RPC: ${op}`);
232
+ }
233
+ safePost(worker, { type: "rpcResult", id, ok: true, value: cloneForPost(value) });
234
+ } catch (error) {
235
+ safePost(worker, { type: "rpcResult", id, ok: false, error: serializeError(error) });
236
+ }
237
+ }
238
+ });
239
+ return result;
240
+ } finally {
241
+ settled = true;
242
+ if (watchdog) clearInterval(watchdog);
243
+ options.signal.removeEventListener("abort", abortWorker);
244
+ terminate("cleanup");
245
+ await worker.terminate().catch(() => undefined);
246
+ }
247
+ }
248
+
249
+ function workerResourceLimits(value: number | undefined): {
250
+ maxOldGenerationSizeMb: number;
251
+ maxYoungGenerationSizeMb: number;
252
+ stackSizeMb: number;
253
+ } {
254
+ const oldGenerationMb = value ?? 128;
255
+ if (!Number.isInteger(oldGenerationMb) || oldGenerationMb < 16 || oldGenerationMb > 1024) {
256
+ throw new WorkflowPolicyError("workflow workerMemoryLimitMb must be an integer between 16 and 1024");
257
+ }
258
+ return {
259
+ maxOldGenerationSizeMb: oldGenerationMb,
260
+ maxYoungGenerationSizeMb: Math.max(8, Math.min(32, Math.floor(oldGenerationMb / 4))),
261
+ stackSizeMb: 8,
262
+ };
263
+ }
264
+
265
+ function normalizeHostCallLimit(value: number | undefined): number {
266
+ if (value === undefined) return 10_000;
267
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > 1_000_000) {
268
+ throw new WorkflowPolicyError("workflow hostCallLimit must be an integer between 1 and 1000000");
269
+ }
270
+ return value;
271
+ }
272
+
273
+ function cloneForPost(value: unknown): unknown {
274
+ try {
275
+ return structuredClone(value);
276
+ } catch (error) {
277
+ const detail = error instanceof Error ? ` ${error.message}` : "";
278
+ throw new Error(`workflow host RPC result must be structured-cloneable.${detail}`);
279
+ }
280
+ }
281
+
282
+ function safePost(worker: Worker, message: unknown): void {
283
+ try {
284
+ worker.postMessage(message);
285
+ } catch {
286
+ // Worker already exited/terminated; runtime drain will settle the host-side promise.
287
+ }
288
+ }
289
+
290
+ function reviveError(value: any): Error {
291
+ if (value?.code === WORKFLOW_POLICY_ERROR_CODE) return new WorkflowPolicyError(value.message ?? "workflow policy violation");
292
+ if (value?.code === WORKFLOW_ABORT_ERROR_CODE) return new WorkflowAbortError(value.message ?? "workflow aborted");
293
+ if (value?.code === WORKFLOW_STALL_ERROR_CODE) return new WorkflowStallError(value.message ?? "workflow stalled");
294
+ const error = new Error(value?.message ?? String(value ?? "unknown error"));
295
+ error.name = value?.name ?? "Error";
296
+ if (value?.code) (error as any).code = value.code;
297
+ if (value?.stack) error.stack = value.stack;
298
+ return error;
299
+ }
300
+
301
+ function serializeError(error: unknown): { name: string; message: string; code?: string; stack?: string } {
302
+ if (error instanceof WorkflowPolicyError) {
303
+ return { name: error.name, message: error.message, code: WORKFLOW_POLICY_ERROR_CODE, stack: error.stack };
304
+ }
305
+ if (error instanceof WorkflowAbortError) {
306
+ return { name: error.name, message: error.message, code: WORKFLOW_ABORT_ERROR_CODE, stack: error.stack };
307
+ }
308
+ if (error instanceof WorkflowStallError) {
309
+ return { name: error.name, message: error.message, code: WORKFLOW_STALL_ERROR_CODE, stack: error.stack };
310
+ }
311
+ return {
312
+ name: error instanceof Error ? error.name : "Error",
313
+ message: error instanceof Error ? error.message : String(error),
314
+ code: typeof error === "object" && error ? (error as any).code : undefined,
315
+ stack: error instanceof Error ? error.stack : undefined,
316
+ };
317
+ }