pi-ptc-subagents 0.1.2 → 0.1.3

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/dist/worker.js CHANGED
@@ -1,759 +1,4 @@
1
- import { a as describeValue, c as workerProtocolSpec } from "./protocol-DUnAP1Ro.js";
2
- import { stripTypeScriptTypes } from "node:module";
3
- import { parentPort, workerData } from "node:worker_threads";
4
- import { inspect } from "node:util";
5
- //#region src/runtime/worker-main.ts
6
- /**
7
- * Worker-side PTC runtime: installs one surface, runs one program, routes `tools.*`
8
- * calls back to the host.
9
- *
10
- * State machine (ADR-0017 §5):
11
- *
12
- * CREATED ── function entry ──────────────────────────► BOOTING
13
- * │ │
14
- * │ control port installed
15
- * │ │
16
- * │ ┌───────┴───────┐
17
- * │ │ │
18
- * │ ▼ ▼
19
- * │ (host sends init) READY ◄─── ready sent
20
- * │ │ │
21
- * │ ▼ │ init
22
- * │ RUNNING │
23
- * │ │ │
24
- * │ (program completes / throws / cancel) │
25
- * │ │ │
26
- * │ ▼ │
27
- * │ reset() + ready sent ───────────────► READY
28
- * │
29
- * └── (host closes control port) ─► worker terminates, no further transitions
30
- *
31
- * A later `connect` re-enters `READY` from wherever the worker is: a handover, not a wait — which is
32
- * how a worker the host abandoned is picked up by the next run (see the connect handler, ADR-0017
33
- * §10(h)).
34
- *
35
- * The `ready` frame has two meanings under ADR-0017 §5:
36
- * - cold start: "booted, send init".
37
- * - warm reuse: "cleared per-run state, ready for next init".
38
- *
39
- * Run identity: every accepted `init` starts a new run and claims the worker for it
40
- * (a run ordinal, captured by that run). A run only owns the worker until the next
41
- * `init` supersedes it — so the reset that ends a run must prove it still owns the
42
- * worker before it clears anything (see `reset`).
43
- *
44
- * Frame ownership: a run's outbound frames (`log`, `narration`, `phase`, `call`, `result`,
45
- * `error`) are posted through the control port that run captured when its `init` arrived —
46
- * never through "whichever port the worker is connected to now". Warm reuse replaces the
47
- * connection while a superseded run may still be unwinding, so routing is what keeps one run's
48
- * terminal frame from settling the next one (see `post` and `startRun`).
49
- *
50
- * The worker is loaded by file URL (`dist/worker.js`, see `worker-entry.ts`); V8's
51
- * code cache and Node's module cache survive across warm-reuse spawns. The
52
- * SELF-CONTAINMENT RULE that previously gated this module — `Function.prototype.toString()`
53
- * of `workerMain` into a `data:` URL — is retired.
54
- */
55
- function workerMain(deps) {
56
- const hostFrame = deps.protocol.hostFrame;
57
- const workerFrame = deps.protocol.workerFrame;
58
- const logLevel = deps.protocol.logLevel;
59
- const errorKind = deps.protocol.errorKind;
60
- /** Wrapper name used to compile a program body (`return …` is legal inside it). */
61
- const programName = "__ptcProgram";
62
- /** Active lifecycle state — see the doc comment at the top of the module. */
63
- let state = "CREATED";
64
- /**
65
- * Run ordinal: incremented once per accepted `init` frame, never decremented. The value
66
- * a run was born with is its identity for life — `reset()` checks it against this
67
- * counter before touching anything.
68
- */
69
- let runGeneration = 0;
70
- let control;
71
- let cancelled;
72
- let nextCallId = 1;
73
- /** Call frames posted and not yet answered; capped by `maxPendingCalls` (ADR-0004). */
74
- let inFlightCalls = 0;
75
- let maxPendingCalls = Number.POSITIVE_INFINITY;
76
- const admissionWaiters = [];
77
- const pending = /* @__PURE__ */ new Map();
78
- /**
79
- * Snapshot of `globalThis` taken once, at boot (end of `installWarningCapture`), i.e. the
80
- * "warm baseline" a reset restores to.
81
- *
82
- * What it covers: `globalThis`'s own enumerable string keys — the way a PTC program normally
83
- * touches the realm. A `globalThis.x = 1` left by run N is deleted before run N+1 starts, and any
84
- * `console` / `tools` / `log` / `phase` / `parallel` / `pipeline` installed by run N is put back
85
- * to its boot value for run N+1 to replace with its own install.
86
- *
87
- * What it does **not** cover — a superseded run's leftovers that survive into the next one, so
88
- * "warm reuse is hermetic" would be the wrong reading of this snapshot:
89
- * - a property added to `globalThis` that is non-enumerable or symbol-keyed (`Object.keys` is
90
- * what both the snapshot and the restore walk, so such a property is neither deleted nor put
91
- * back);
92
- * - anything reachable through another object: `Array.prototype.x = 1`, a mutated built-in, the
93
- * module-level state of a module the program imported (Node caches modules per worker, and a
94
- * program can `import()` one);
95
- * - `process` state the program wrote — `process.title`, `process.exitCode`, listeners it added;
96
- * - timers it started, which keep firing (see `startRun`).
97
- *
98
- * Those are known, accepted limits of warm reuse rather than oversights of this snapshot.
99
- *
100
- * The *boot* snapshot is the only clean starting point there is: a later `connect` must not
101
- * take a new one, or whatever the run it superseded left behind would be part of the baseline
102
- * and would be restored into every run after it.
103
- */
104
- let warmBaseline;
105
- const globals = () => globalThis;
106
- class ToolCallError extends Error {
107
- toolName;
108
- constructor(toolName, message) {
109
- super(message);
110
- this.name = "ToolCallError";
111
- this.toolName = toolName;
112
- }
113
- }
114
- const messageOf = (error) => error instanceof Error ? error.message : String(error);
115
- /**
116
- * Trim a stack down to what a reader can act on.
117
- *
118
- * Capped at a small depth so the model-facing stack stays inside the output budget.
119
- * Now that the worker is loaded by file URL, no `data:text/javascript,…` frames
120
- * appear at all — the rule kept the previous bootstrap's encoded payload from
121
- * filling the stack.
122
- */
123
- const stackOf = (error) => {
124
- if (!(error instanceof Error) || typeof error.stack !== "string") return void 0;
125
- const lines = error.stack.split("\n");
126
- const kept = [lines[0] ?? ""];
127
- for (const line of lines.slice(1)) {
128
- kept.push(line);
129
- if (kept.length >= 6) break;
130
- }
131
- return kept.join("\n");
132
- };
133
- /**
134
- * Post a frame through `port` — the control port the caller's run belongs to — defaulting to
135
- * the port the worker is currently connected to.
136
- *
137
- * Invariant (ADR-0017 §10(b)): *a run's outbound frames travel only through the port that run
138
- * captured at `init` time*. Warm reuse installs a new port while a superseded run may still be
139
- * unwinding, and that run's terminal `result` must not land on the *next* run's port — the host
140
- * reads any `result` there as "the current run settled" (`handleFrame` → `finish()`), which
141
- * would end a run whose program never finished. The port of a run the host has abandoned has
142
- * been closed on the host's side, so its late frames fail to send and are dropped here; that is
143
- * the right outcome for a frame nobody owns any more.
144
- *
145
- * Only realm-level frames — the `connect` handshake's `ready`, warning `log`s, which belong to
146
- * the worker rather than to a program — use the default.
147
- */
148
- const post = (frame, port = control) => {
149
- if (!port) return false;
150
- try {
151
- port.postMessage(frame);
152
- return true;
153
- } catch {
154
- return false;
155
- }
156
- };
157
- const postLog = (level, text, port) => {
158
- post({
159
- kind: workerFrame.log,
160
- level,
161
- text
162
- }, port);
163
- };
164
- const postError = (kind, message, stack, port) => {
165
- post(stack === void 0 ? {
166
- kind: workerFrame.error,
167
- error: {
168
- kind,
169
- message
170
- }
171
- } : {
172
- kind: workerFrame.error,
173
- error: {
174
- kind,
175
- message,
176
- stack
177
- }
178
- }, port);
179
- };
180
- const abortError = (reason) => {
181
- const error = /* @__PURE__ */ new Error(reason === "timeout" ? "PTC run timed out" : "PTC run was cancelled");
182
- error.name = "AbortError";
183
- return error;
184
- };
185
- const closedError = () => {
186
- const error = /* @__PURE__ */ new Error("the PTC control channel closed before the call completed");
187
- error.name = "AbortError";
188
- return error;
189
- };
190
- /**
191
- * Admission control for host binding calls.
192
- *
193
- * "Maximum simultaneous host binding calls" is the worker's invariant to keep: a call
194
- * frame is only posted once fewer than `maxPendingCalls` are outstanding, so a wide
195
- * `Promise.all` / `parallel()` burst queues here rather than arriving at the host in one
196
- * tick. The host still counts arrivals as a backstop, but it must never see this budget
197
- * exceeded by a well-behaved program.
198
- *
199
- * Waiting for a slot is cancel-aware: `cancel` and a closed control port reject waiters
200
- * immediately, so an oversized burst unwinds at once instead of waiting out the grace
201
- * window before the host terminates the worker.
202
- */
203
- const acquireCallSlot = async () => {
204
- if (inFlightCalls < maxPendingCalls) {
205
- inFlightCalls += 1;
206
- return;
207
- }
208
- await new Promise((resolve, reject) => {
209
- admissionWaiters.push({
210
- resolve,
211
- reject
212
- });
213
- });
214
- };
215
- const releaseCallSlot = () => {
216
- const next = admissionWaiters.shift();
217
- if (next) {
218
- next.resolve();
219
- return;
220
- }
221
- inFlightCalls -= 1;
222
- };
223
- const rejectAdmissionWaiters = (error) => {
224
- while (admissionWaiters.length > 0) {
225
- const waiter = admissionWaiters.shift();
226
- if (waiter) waiter.reject(error);
227
- }
228
- };
229
- /** Settle every outstanding call and every admission wait, so nothing can hang. */
230
- const flushPendingCalls = (error) => {
231
- rejectAdmissionWaiters(error);
232
- for (const [callId, entry] of pending) {
233
- pending.delete(callId);
234
- entry.reject(error);
235
- }
236
- };
237
- /**
238
- * F3 — reinstall the frozen per-run environment carried in `workerData`.
239
- *
240
- * The spawn already passes the same snapshot as the worker's `env` option; rebuilding
241
- * `process.env` from `workerData` here is what makes the snapshot the authority: the
242
- * program's environment is the frozen record, never whatever `process.env` happened
243
- * to hold when a binding call crossed the wire.
244
- */
245
- const installFrozenEnv = () => {
246
- const data = deps.workerData;
247
- if (typeof data !== "object" || data === null) return;
248
- const snapshot = data.env;
249
- if (typeof snapshot !== "object" || snapshot === null || Array.isArray(snapshot)) return;
250
- const env = snapshot;
251
- for (const key of Object.keys(process.env)) if (!(key in env)) delete process.env[key];
252
- for (const key of Object.keys(env)) {
253
- const value = env[key];
254
- if (typeof value === "string") process.env[key] = value;
255
- }
256
- };
257
- /**
258
- * Lossless-JSON materialization of the program's return value (R1 §3 `invalid-output`).
259
- *
260
- * Plain JSON only: class instances, `Date`/`Map`/`Set`, functions, symbols, bigints,
261
- * non-finite numbers and cycles are rejected with a path-qualified reason instead of
262
- * being silently lossy through the transport. `undefined` follows `JSON.stringify`:
263
- * object properties are dropped, array slots become `null`.
264
- */
265
- const toJsonValue = (value) => {
266
- const seen = /* @__PURE__ */ new Set();
267
- const walk = (candidate, path) => {
268
- if (candidate === null) return {
269
- ok: true,
270
- value: null
271
- };
272
- if (typeof candidate === "string" || typeof candidate === "boolean") return {
273
- ok: true,
274
- value: candidate
275
- };
276
- if (typeof candidate === "undefined") return {
277
- ok: true,
278
- value: void 0
279
- };
280
- if (typeof candidate === "number") {
281
- if (!Number.isFinite(candidate)) return {
282
- ok: false,
283
- reason: `${path} is ${String(candidate)}, which is not representable as JSON`
284
- };
285
- return {
286
- ok: true,
287
- value: candidate
288
- };
289
- }
290
- if (typeof candidate !== "object") return {
291
- ok: false,
292
- reason: `${path} is ${describeValue(candidate)}; results must be lossless JSON`
293
- };
294
- if (Array.isArray(candidate)) {
295
- if (seen.has(candidate)) return {
296
- ok: false,
297
- reason: `${path} is a circular reference`
298
- };
299
- seen.add(candidate);
300
- const items = [];
301
- for (let index = 0; index < candidate.length; index += 1) {
302
- const item = walk(candidate[index], `${path}[${index}]`);
303
- if (!item.ok) return item;
304
- items.push(item.value === void 0 ? null : item.value);
305
- }
306
- seen.delete(candidate);
307
- return {
308
- ok: true,
309
- value: items
310
- };
311
- }
312
- const prototype = Object.getPrototypeOf(candidate);
313
- if (prototype !== Object.prototype && prototype !== null) {
314
- const ctor = candidate.constructor;
315
- return {
316
- ok: false,
317
- reason: `${path} is a ${ctor && typeof ctor.name === "string" && ctor.name.length > 0 ? ctor.name : "an object"}; results must be plain JSON objects`
318
- };
319
- }
320
- if (seen.has(candidate)) return {
321
- ok: false,
322
- reason: `${path} is a circular reference`
323
- };
324
- seen.add(candidate);
325
- const entries = {};
326
- for (const key of Object.keys(candidate)) {
327
- const item = walk(candidate[key], `${path}.${key}`);
328
- if (!item.ok) return item;
329
- if (item.value !== void 0) entries[key] = item.value;
330
- }
331
- seen.delete(candidate);
332
- return {
333
- ok: true,
334
- value: entries
335
- };
336
- };
337
- return walk(value, "result");
338
- };
339
- const formatArgs = (args) => args.map((arg) => typeof arg === "string" ? arg : deps.inspect(arg, {
340
- depth: 4,
341
- breakLength: Infinity,
342
- colors: false
343
- })).join(" ");
344
- /**
345
- * Capture `console` instead of letting worker stdio reach the host terminal.
346
- *
347
- * Only the printing methods exist on this surface; everything routes to `log` frames
348
- * so the model (not the terminal) sees the program's output. Log frames belong to the run
349
- * whose console this is, hence the captured `port`.
350
- */
351
- const installConsole = (port) => {
352
- const write = (level) => {
353
- return (...args) => {
354
- postLog(level, formatArgs(args), port);
355
- };
356
- };
357
- globals().console = {
358
- log: write(logLevel.log),
359
- info: write(logLevel.info),
360
- warn: write(logLevel.warn),
361
- error: write(logLevel.error),
362
- debug: write(logLevel.debug)
363
- };
364
- };
365
- /**
366
- * Route `process.emitWarning` output through `log` frames.
367
- *
368
- * Node's default warning printer writes to stderr, which is piped to the host terminal.
369
- * `ExperimentalWarning`s are dropped rather than forwarded: the PTC runtime itself uses
370
- * experimental Node APIs (type stripping, for one) and those warnings are about the
371
- * harness, not about the program — forwarding them would put Node-internals noise into
372
- * every run's logs.
373
- */
374
- const installWarningCapture = () => {
375
- if (typeof process.removeAllListeners !== "function" || typeof process.on !== "function") return;
376
- process.removeAllListeners("warning");
377
- process.on("warning", (warning) => {
378
- const record = typeof warning === "object" && warning !== null ? warning : void 0;
379
- const name = record && typeof record.name === "string" ? record.name : "Warning";
380
- if (name === "ExperimentalWarning") return;
381
- const detail = record && typeof record.message === "string" ? record.message : String(warning);
382
- postLog(logLevel.warn, `${name}: ${detail}`);
383
- });
384
- };
385
- /**
386
- * Compile a program body into a callable.
387
- *
388
- * The body is wrapped in an async function so `return` and `await` work at the top
389
- * level, then run through `stripTypeScriptTypes` so the same body may carry type
390
- * annotations (DSH's "type annotations are advisory, the code runs type-stripped").
391
- * JavaScript the TypeScript parser rejects falls back to the unstripped source; a
392
- * genuine syntax error then surfaces from the `Function` constructor instead.
393
- */
394
- const compileProgram = (code) => {
395
- const wrapped = `async function ${programName}() {\n${code}\n}`;
396
- let source = wrapped;
397
- try {
398
- source = deps.stripTypes(wrapped, { mode: "strip" });
399
- } catch {
400
- source = wrapped;
401
- }
402
- return new Function(`${source}\nreturn ${programName}();`);
403
- };
404
- const runProgram = async (code, generation, port) => {
405
- try {
406
- let program;
407
- try {
408
- program = compileProgram(code);
409
- } catch (error) {
410
- postError(errorKind.exception, `program failed to compile: ${messageOf(error)}`, stackOf(error), port);
411
- return;
412
- }
413
- try {
414
- const value = await program();
415
- const normalized = toJsonValue(value);
416
- if (!normalized.ok) {
417
- postError(errorKind.invalidOutput, normalized.reason, void 0, port);
418
- return;
419
- }
420
- post(normalized.value === void 0 ? { kind: workerFrame.result } : {
421
- kind: workerFrame.result,
422
- value: normalized.value
423
- }, port);
424
- } catch (error) {
425
- postError(cancelled ?? errorKind.exception, messageOf(error), stackOf(error), port);
426
- }
427
- } finally {
428
- await reset(generation, port);
429
- }
430
- };
431
- /**
432
- * `tools.<name>(args)` — acquire an admission slot, post a call frame, and await the
433
- * matching `call-result`.
434
- *
435
- * The slot is released on every exit path (settled, admission refused by `post`, cancel),
436
- * which is what keeps a burst draining instead of deadlocking. Call frames carry the
437
- * binding's own run port: a superseded run's late call is dropped at its closed port instead
438
- * of arriving at the host as a call for a run it never made.
439
- */
440
- const makeBinding = (name, port) => {
441
- return async (args) => {
442
- if (cancelled) throw abortError(cancelled);
443
- await acquireCallSlot();
444
- try {
445
- if (cancelled) throw abortError(cancelled);
446
- const callId = nextCallId;
447
- nextCallId += 1;
448
- const promise = new Promise((resolve, reject) => {
449
- pending.set(callId, {
450
- resolve,
451
- reject
452
- });
453
- });
454
- if (!post({
455
- kind: workerFrame.call,
456
- callId,
457
- tool: name,
458
- args
459
- }, port)) {
460
- pending.delete(callId);
461
- throw new ToolCallError(name, `${name}() could not be called: the arguments are not transferable or the run has ended`);
462
- }
463
- return await promise;
464
- } finally {
465
- releaseCallSlot();
466
- }
467
- };
468
- };
469
- const settleCall = (frame) => {
470
- const callId = frame.callId;
471
- if (typeof callId !== "number") return;
472
- const entry = pending.get(callId);
473
- if (!entry) return;
474
- pending.delete(callId);
475
- if (frame.ok === true) {
476
- entry.resolve(frame.value);
477
- return;
478
- }
479
- entry.reject(new ToolCallError(typeof frame.tool === "string" ? frame.tool : "unknown", typeof frame.message === "string" ? frame.message : "binding call failed"));
480
- };
481
- const cancelPending = (reason) => {
482
- cancelled = reason;
483
- flushPendingCalls(abortError(reason));
484
- };
485
- /**
486
- * Confirm a cancel that has nothing to flush, and forget it.
487
- *
488
- * A cancel can arrive while the worker is idle — booting, or freshly reset and waiting for the
489
- * next `init`. There is no program to interrupt and no pending call to reject, so the honest
490
- * answer is to report the cancel: the host asked to stop, and it should settle now rather than
491
- * wait out its grace window for a run that will never start (and the `init` it might send next
492
- * would clear the verdict and run the program anyway).
493
- */
494
- const reportIdleCancel = (reason, port) => {
495
- cancelled = void 0;
496
- const error = abortError(reason);
497
- postError(reason, error.message, void 0, port);
498
- };
499
- const assertItemCount = (helper, count, maxItemsPerCall) => {
500
- if (count > maxItemsPerCall) throw new RangeError(`${helper}() received ${count} items, exceeding maxItemsPerCall (${maxItemsPerCall})`);
501
- };
502
- const installWorkflowHelpers = (args, maxItemsPerCall, port) => {
503
- const target = globals();
504
- target.args = args === void 0 ? null : args;
505
- target.log = (message) => {
506
- if (typeof message !== "string") throw new TypeError(`log(message) expects a string, received ${describeValue(message)}`);
507
- post({
508
- kind: workerFrame.narration,
509
- message
510
- }, port);
511
- };
512
- target.phase = (title) => {
513
- if (typeof title !== "string") throw new TypeError(`phase(title) expects a string, received ${describeValue(title)}`);
514
- post({
515
- kind: workerFrame.phase,
516
- title
517
- }, port);
518
- };
519
- target.parallel = async (thunks) => {
520
- if (!Array.isArray(thunks)) throw new TypeError(`parallel(thunks) expects an array of functions, received ${describeValue(thunks)}`);
521
- assertItemCount("parallel", thunks.length, maxItemsPerCall);
522
- for (let index = 0; index < thunks.length; index += 1) if (typeof thunks[index] !== "function") throw new TypeError(`parallel(thunks) expects functions; item ${index} is ${describeValue(thunks[index])}`);
523
- return await Promise.all(thunks.map(async (thunk) => {
524
- try {
525
- return await thunk();
526
- } catch {
527
- return null;
528
- }
529
- }));
530
- };
531
- target.pipeline = async (items, ...stages) => {
532
- if (!Array.isArray(items)) throw new TypeError(`pipeline(items, ...stages) expects an array of items, received ${describeValue(items)}`);
533
- assertItemCount("pipeline", items.length, maxItemsPerCall);
534
- if (stages.length === 0) throw new TypeError("pipeline(items, ...stages) requires at least one stage function");
535
- for (let index = 0; index < stages.length; index += 1) if (typeof stages[index] !== "function") throw new TypeError(`pipeline(items, ...stages) expects stage functions; stage ${index} is ${describeValue(stages[index])}`);
536
- const stageFunctions = stages;
537
- return await Promise.all(items.map(async (item, index) => {
538
- try {
539
- let current = item;
540
- for (const stage of stageFunctions) current = await stage(current, item, index);
541
- return current;
542
- } catch {
543
- return null;
544
- }
545
- }));
546
- };
547
- };
548
- /**
549
- * Restore `globalThis` to the warm baseline taken at the end of the worker's
550
- * boot-time setup (`installWarningCapture`).
551
- *
552
- * This is stronger than deleting the known `RUN_GLOBAL_KEYS`: it also wipes any
553
- * stray property the previous run's program left behind (`globalThis.x = 1` and
554
- * similar). Without it, warm reuse would leak state from one run to the next.
555
- * Enumerating own *enumerable string* keys is also its limit — see `warmBaseline`
556
- * for the leftovers that walk past it.
557
- *
558
- * Properties in the baseline are restored even if the program overwrote them —
559
- * that is what keeps `console` honest for the next run, since the program's
560
- * console override does not survive the reset.
561
- */
562
- const restoreWarmBaseline = () => {
563
- if (!warmBaseline) return;
564
- const target = globals();
565
- for (const key of Object.keys(target)) if (!warmBaseline.has(key)) delete target[key];
566
- for (const [key, value] of warmBaseline) {
567
- const descriptor = Object.getOwnPropertyDescriptor(target, key);
568
- if (descriptor !== void 0 && descriptor.writable === false) continue;
569
- try {
570
- target[key] = value;
571
- } catch {}
572
- }
573
- };
574
- /**
575
- * RUNNING → READY: clear the per-run state this worker owns so it is safe to accept
576
- * another `init` frame on warm reuse (ADR-0017 §5).
577
- *
578
- * Order matters: flush first so no settled call leaks into the next run, then
579
- * restore the warm baseline (the program may have left values behind that would
580
- * shadow the next install), then rehydrate the frozen env so the next run starts
581
- * from the same `process.env` snapshot the host captured. State transitions to
582
- * `READY` last — only when the worker is actually eligible for the next `init`.
583
- *
584
- * What it clears is exactly: the outstanding calls and admission waiters, the run
585
- * bookkeeping (`inFlightCalls`, `cancelled`, `nextCallId`, `pending`), `globalThis`'s own
586
- * enumerable keys (via the warm baseline — see `warmBaseline` for what that misses) and
587
- * `process.env`. It does **not** stop timers the program left running; nothing can, see
588
- * `startRun`. And when the run that produced this reset was superseded, none of it happens at
589
- * all: the clearing then falls to the *next* run's `startRun` (see the invariant below).
590
- *
591
- * Invariant — a reset is scoped to the run that produced it: `generation` is the run
592
- * ordinal the finishing run was born with. If the worker has since been handed to a
593
- * newer run (`generation !== runGeneration`), this reset is a no-op: it must not clear
594
- * the newer run's calls, globals or bookkeeping, and it must not post `ready` (the
595
- * host reads `ready` as "the current run settled, send the next init" — posting a
596
- * stranger's `ready` is what makes a program execute twice). Ownership is captured at
597
- * `init` time rather than read from the shared `state`, because `state` belongs to
598
- * whoever owns the worker now. `port` is the run's own control port for the same reason:
599
- * a `ready` that survives the ownership check still has no business on another run's port.
600
- */
601
- const reset = async (generation, port) => {
602
- if (state !== "RUNNING" || generation !== runGeneration) return;
603
- flushPendingCalls(closedError());
604
- inFlightCalls = 0;
605
- cancelled = void 0;
606
- nextCallId = 1;
607
- pending.clear();
608
- restoreWarmBaseline();
609
- installFrozenEnv();
610
- state = "READY";
611
- post({ kind: workerFrame.ready }, port);
612
- };
613
- /**
614
- * Claim the worker for one run and install that run's surface.
615
- *
616
- * `port` is the channel the run's `init` arrived on; it *is* the run's control port for life.
617
- * Every frame the run produces afterwards — `log`, `narration`, `phase`, `call`, `result`,
618
- * `error`, its closing `ready` — is posted through it, even if the worker has since been handed
619
- * to another run (see `post`).
620
- *
621
- * The run starts from a clean realm. That cannot be left to the previous run's `reset()`: a run
622
- * the host abandoned is superseded, and its reset is a generation-guarded no-op — so its strays on
623
- * `globalThis` (`tools`, the workflow helpers, whatever its program assigned) and its bookkeeping
624
- * would otherwise live on into this run and break the isolation warm reuse promises (ADR-0017
625
- * §10(d) — and note §5 is explicit that the reset handshake is *not* where isolation comes
626
- * from). Clearing here is unconditional: one run starting is the end of everything the previous
627
- * round was doing, whatever the previous round did or did not do.
628
- *
629
- * "Clean" is what `restoreWarmBaseline` can restore — `globalThis`'s own enumerable keys, then the
630
- * frozen env and this run's fresh installs — plus this worker's run bookkeeping. It is honestly
631
- * not every trace a program can leave: a timer it started keeps firing, a prototype it patched
632
- * stays patched, and `process` state it wrote stays written (see `warmBaseline` for the list).
633
- * Timers are the leftover this function used to claim to clear, and the claim was false: Node
634
- * offers no way to enumerate a worker's pending timers — inside a worker thread
635
- * `process._getActiveHandles()` returns `[]` and `process.getActiveResourcesInfo()` reports names
636
- * (`"Timeout"`), not handles (both checked on Node 24.21.0). A leftover interval is therefore a
637
- * known, accepted limitation of warm reuse, not something this function clears.
638
- */
639
- const startRun = (frame, generation, port) => {
640
- restoreWarmBaseline();
641
- cancelled = void 0;
642
- inFlightCalls = 0;
643
- pending.clear();
644
- installFrozenEnv();
645
- installConsole(port);
646
- maxPendingCalls = typeof frame.maxPendingCalls === "number" && Number.isFinite(frame.maxPendingCalls) && frame.maxPendingCalls > 0 ? frame.maxPendingCalls : Number.POSITIVE_INFINITY;
647
- const tools = {};
648
- const bindingNames = Array.isArray(frame.bindings) ? frame.bindings.filter((name) => typeof name === "string") : [];
649
- for (const name of bindingNames) tools[name] = makeBinding(name, port);
650
- const available = bindingNames.join(", ") || "(none)";
651
- const candidates = Array.isArray(frame.bindingCandidates) ? frame.bindingCandidates : [];
652
- for (const name of candidates) {
653
- if (typeof name !== "string" || tools[name]) continue;
654
- tools[name] = async () => {
655
- throw new ToolCallError(name, `no binding named "${name}" in this run; available bindings: ${available}`);
656
- };
657
- }
658
- globals().tools = tools;
659
- if (frame.surface === "workflow") {
660
- const maxItemsPerCall = typeof frame.maxItemsPerCall === "number" ? frame.maxItemsPerCall : 0;
661
- installWorkflowHelpers(frame.args, maxItemsPerCall, port);
662
- }
663
- runProgram(typeof frame.code === "string" ? frame.code : "", generation, port);
664
- };
665
- /**
666
- * Handle one host frame, tagging it with the port it arrived on: that port *is* the run's
667
- * control port if the frame is an `init` (see `startRun`), so the run's frames stay on the
668
- * channel its own host is listening to.
669
- */
670
- const handleHostFrame = (raw, port) => {
671
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return;
672
- const frame = raw;
673
- if (frame.kind === hostFrame.init) {
674
- if (state !== "READY") return;
675
- runGeneration += 1;
676
- const generation = runGeneration;
677
- state = "RUNNING";
678
- startRun(frame, generation, port);
679
- return;
680
- }
681
- if (frame.kind === hostFrame.callResult) {
682
- settleCall(frame);
683
- return;
684
- }
685
- if (frame.kind === hostFrame.cancel) {
686
- const reason = frame.reason === "timeout" ? "timeout" : "abort";
687
- if (state === "READY") {
688
- reportIdleCancel(reason, port);
689
- return;
690
- }
691
- cancelPending(reason);
692
- }
693
- };
694
- state = "BOOTING";
695
- deps.parentPort.on("message", (value) => {
696
- if (typeof value !== "object" || value === null) return;
697
- const frame = value;
698
- if (frame.kind !== hostFrame.connect) return;
699
- const port = frame.port;
700
- if (typeof port !== "object" || port === null || typeof port.on !== "function") return;
701
- const connected = port;
702
- control = connected;
703
- connected.on("message", (raw) => {
704
- handleHostFrame(raw, connected);
705
- });
706
- connected.on("close", () => {
707
- if (control === connected) flushPendingCalls(closedError());
708
- });
709
- installWarningCapture();
710
- const isFirstBoot = warmBaseline === void 0;
711
- if (isFirstBoot) warmBaseline = new Map(Object.entries(globals()));
712
- state = "READY";
713
- if (cancelled !== void 0) {
714
- if (isFirstBoot) {
715
- reportIdleCancel(cancelled, connected);
716
- return;
717
- }
718
- cancelled = void 0;
719
- }
720
- post({ kind: workerFrame.ready }, connected);
721
- });
722
- }
723
- //#endregion
724
- //#region src/runtime/worker-entry.ts
725
- /**
726
- * Worker entry — loaded by file URL (ADR-0017 §7).
727
- *
728
- * The previous bootstrap composed a `data:text/javascript,…` URL from
729
- * `Function.prototype.toString()` of `workerMain`, which kept the package as a
730
- * single self-contained file but paid a fresh V8 spin-up + module-graph parse
731
- * for every worker spawn (no code-cache reuse). After the rolldown dual-entry
732
- * build emits `dist/worker.js` as a sibling of `dist/index.js`, this file is
733
- * the target the host loads; V8 and Node's module cache survive across
734
- * warm-reuse spawns.
735
- *
736
- * The protocol tables travel as static imports in both bundles (host imports
737
- * from `dist/index.js`, worker imports from `dist/worker.js`) — both files
738
- * bundle `./protocol.ts`, so each side constructs `workerProtocolSpec()`
739
- * independently and the wire strings stay aligned. This removes the build-time
740
- * protocol literal the data: URL used to carry.
741
- *
742
- * `workerData` is the spawn-time payload `{ env }` — the frozen per-run env
743
- * snapshot (F3, ADR-0005) — and nothing else. Run identity is *not* spawn-time:
744
- * a warm worker outlives the run that spawned it, so the host sends each run's
745
- * `runId` in its own `init` frame instead. `workerMain` reads neither (see its
746
- * `WorkerMainDeps.workerData` note); `runId` is host-side bookkeeping for
747
- * attributing frames to a run.
748
- */
749
- workerMain({
750
- parentPort,
751
- workerData,
752
- inspect,
753
- stripTypes: stripTypeScriptTypes,
754
- protocol: workerProtocolSpec()
755
- });
756
- //#endregion
757
- export {};
758
-
1
+ import{a as e,c as t}from"./protocol-CfOgxz3u.js";import{stripTypeScriptTypes as n}from"node:module";import{parentPort as r,workerData as i}from"node:worker_threads";import{inspect as a}from"node:util";function o(t){let n=t.protocol.hostFrame,r=t.protocol.workerFrame,i=t.protocol.logLevel,a=t.protocol.errorKind,o=`__ptcProgram`,s=`CREATED`,c=0,l,u,d=1,f=0,p=1/0,m=[],h=/* @__PURE__ */ new Map,g,_=()=>globalThis;class v extends Error{toolName;constructor(e,t){super(t),this.name=`ToolCallError`,this.toolName=e}}let y=e=>e instanceof Error?e.message:String(e),b=e=>{if(!(e instanceof Error)||typeof e.stack!=`string`)return;let t=e.stack.split(`
2
+ `),n=[t[0]??``];for(let e of t.slice(1))if(n.push(e),n.length>=6)break;return n.join(`
3
+ `)},x=(e,t=l)=>{if(!t)return!1;try{return t.postMessage(e),!0}catch{return!1}},S=(e,t,n)=>{x({kind:r.log,level:e,text:t},n)},C=(e,t,n,i)=>{x(n===void 0?{kind:r.error,error:{kind:e,message:t}}:{kind:r.error,error:{kind:e,message:t,stack:n}},i)},w=e=>{let t=/* @__PURE__ */ Error(e===`timeout`?`PTC run timed out`:`PTC run was cancelled`);return t.name=`AbortError`,t},T=()=>{let e=/* @__PURE__ */ Error(`the PTC control channel closed before the call completed`);return e.name=`AbortError`,e},E=async()=>{if(f<p){f+=1;return}await new Promise((e,t)=>{m.push({resolve:e,reject:t})})},D=()=>{let e=m.shift();if(e){e.resolve();return}--f},O=e=>{for(;m.length>0;){let t=m.shift();t&&t.reject(e)}},k=e=>{O(e);for(let[t,n]of h)h.delete(t),n.reject(e)},A=()=>{let e=t.workerData;if(typeof e!=`object`||!e)return;let n=e.env;if(typeof n!=`object`||!n||Array.isArray(n))return;let r=n;for(let e of Object.keys(process.env))e in r||delete process.env[e];for(let e of Object.keys(r)){let t=r[e];typeof t==`string`&&(process.env[e]=t)}},j=t=>{let n=/* @__PURE__ */ new Set,r=(t,i)=>{if(t===null)return{ok:!0,value:null};if(typeof t==`string`||typeof t==`boolean`)return{ok:!0,value:t};if(t===void 0)return{ok:!0,value:void 0};if(typeof t==`number`)return Number.isFinite(t)?{ok:!0,value:t}:{ok:!1,reason:`${i} is ${String(t)}, which is not representable as JSON`};if(typeof t!=`object`)return{ok:!1,reason:`${i} is ${e(t)}; results must be lossless JSON`};if(Array.isArray(t)){if(n.has(t))return{ok:!1,reason:`${i} is a circular reference`};n.add(t);let e=[];for(let n=0;n<t.length;n+=1){let a=r(t[n],`${i}[${n}]`);if(!a.ok)return a;e.push(a.value===void 0?null:a.value)}return n.delete(t),{ok:!0,value:e}}let a=Object.getPrototypeOf(t);if(a!==Object.prototype&&a!==null){let e=t.constructor;return{ok:!1,reason:`${i} is a ${e&&typeof e.name==`string`&&e.name.length>0?e.name:`an object`}; results must be plain JSON objects`}}if(n.has(t))return{ok:!1,reason:`${i} is a circular reference`};n.add(t);let o={};for(let e of Object.keys(t)){let n=r(t[e],`${i}.${e}`);if(!n.ok)return n;n.value!==void 0&&(o[e]=n.value)}return n.delete(t),{ok:!0,value:o}};return r(t,`result`)},M=e=>e.map(e=>typeof e==`string`?e:t.inspect(e,{depth:4,breakLength:1/0,colors:!1})).join(` `),N=e=>{let t=t=>(...n)=>{S(t,M(n),e)};_().console={log:t(i.log),info:t(i.info),warn:t(i.warn),error:t(i.error),debug:t(i.debug)}},P=()=>{typeof process.removeAllListeners==`function`&&typeof process.on==`function`&&(process.removeAllListeners(`warning`),process.on(`warning`,e=>{let t=typeof e==`object`&&e?e:void 0,n=t&&typeof t.name==`string`?t.name:`Warning`;if(n===`ExperimentalWarning`)return;let r=t&&typeof t.message==`string`?t.message:String(e);S(i.warn,`${n}: ${r}`)}))},F=e=>{let n=`async function ${o}() {\n${e}\n}`,r=n;try{r=t.stripTypes(n,{mode:`strip`})}catch{r=n}return Function(`${r}\nreturn ${o}();`)},I=async(e,t,n)=>{try{let t;try{t=F(e)}catch(e){C(a.exception,`program failed to compile: ${y(e)}`,b(e),n);return}try{let e=await t(),i=j(e);if(!i.ok){C(a.invalidOutput,i.reason,void 0,n);return}x(i.value===void 0?{kind:r.result}:{kind:r.result,value:i.value},n)}catch(e){C(u??a.exception,y(e),b(e),n)}}finally{await W(t,n)}},L=(e,t)=>async n=>{if(u)throw w(u);await E();try{if(u)throw w(u);let i=d;d+=1;let a=new Promise((e,t)=>{h.set(i,{resolve:e,reject:t})});if(!x({kind:r.call,callId:i,tool:e,args:n},t))throw h.delete(i),new v(e,`${e}() could not be called: the arguments are not transferable or the run has ended`);return await a}finally{D()}},R=e=>{let t=e.callId;if(typeof t!=`number`)return;let n=h.get(t);if(n){if(h.delete(t),e.ok===!0){n.resolve(e.value);return}n.reject(new v(typeof e.tool==`string`?e.tool:`unknown`,typeof e.message==`string`?e.message:`binding call failed`))}},z=e=>{u=e,k(w(e))},B=(e,t)=>{u=void 0;let n=w(e);C(e,n.message,void 0,t)},V=(e,t,n)=>{if(t>n)throw RangeError(`${e}() received ${t} items, exceeding maxItemsPerCall (${n})`)},H=(t,n,i)=>{let a=_();a.args=t===void 0?null:t,a.log=t=>{if(typeof t!=`string`)throw TypeError(`log(message) expects a string, received ${e(t)}`);x({kind:r.narration,message:t},i)},a.phase=t=>{if(typeof t!=`string`)throw TypeError(`phase(title) expects a string, received ${e(t)}`);x({kind:r.phase,title:t},i)},a.parallel=async t=>{if(!Array.isArray(t))throw TypeError(`parallel(thunks) expects an array of functions, received ${e(t)}`);V(`parallel`,t.length,n);for(let n=0;n<t.length;n+=1)if(typeof t[n]!=`function`)throw TypeError(`parallel(thunks) expects functions; item ${n} is ${e(t[n])}`);return await Promise.all(t.map(async e=>{try{return await e()}catch{return null}}))},a.pipeline=async(t,...r)=>{if(!Array.isArray(t))throw TypeError(`pipeline(items, ...stages) expects an array of items, received ${e(t)}`);if(V(`pipeline`,t.length,n),r.length===0)throw TypeError(`pipeline(items, ...stages) requires at least one stage function`);for(let t=0;t<r.length;t+=1)if(typeof r[t]!=`function`)throw TypeError(`pipeline(items, ...stages) expects stage functions; stage ${t} is ${e(r[t])}`);let i=r;return await Promise.all(t.map(async(e,t)=>{try{let n=e;for(let r of i)n=await r(n,e,t);return n}catch{return null}}))}},U=()=>{if(!g)return;let e=_();for(let t of Object.keys(e))g.has(t)||delete e[t];for(let[t,n]of g){let r=Object.getOwnPropertyDescriptor(e,t);if(r===void 0||r.writable!==!1)try{e[t]=n}catch{}}},W=async(e,t)=>{s===`RUNNING`&&e===c&&(k(T()),f=0,u=void 0,d=1,h.clear(),U(),A(),s=`READY`,x({kind:r.ready},t))},G=(e,t,n)=>{U(),u=void 0,f=0,h.clear(),A(),N(n),p=typeof e.maxPendingCalls==`number`&&Number.isFinite(e.maxPendingCalls)&&e.maxPendingCalls>0?e.maxPendingCalls:1/0;let r={},i=Array.isArray(e.bindings)?e.bindings.filter(e=>typeof e==`string`):[];for(let e of i)r[e]=L(e,n);let a=i.join(`, `)||`(none)`,o=Array.isArray(e.bindingCandidates)?e.bindingCandidates:[];for(let e of o)typeof e!=`string`||r[e]||(r[e]=async()=>{throw new v(e,`no binding named "${e}" in this run; available bindings: ${a}`)});if(_().tools=r,e.surface===`workflow`){let t=typeof e.maxItemsPerCall==`number`?e.maxItemsPerCall:0;H(e.args,t,n)}I(typeof e.code==`string`?e.code:``,t,n)},K=(e,t)=>{if(typeof e!=`object`||!e||Array.isArray(e))return;let r=e;if(r.kind===n.init){if(s!==`READY`)return;c+=1;let e=c;s=`RUNNING`,G(r,e,t);return}if(r.kind===n.callResult){R(r);return}if(r.kind===n.cancel){let e=r.reason===`timeout`?`timeout`:`abort`;if(s===`READY`){B(e,t);return}z(e)}};s=`BOOTING`,t.parentPort.on(`message`,e=>{if(typeof e!=`object`||!e)return;let t=e;if(t.kind!==n.connect)return;let i=t.port;if(typeof i!=`object`||!i||typeof i.on!=`function`)return;let a=i;l=a,a.on(`message`,e=>{K(e,a)}),a.on(`close`,()=>{l===a&&k(T())}),P();let o=g===void 0;if(o&&(g=new Map(Object.entries(_()))),s=`READY`,u!==void 0){if(o){B(u,a);return}u=void 0}x({kind:r.ready},a)})}o({parentPort:r,workerData:i,inspect:a,stripTypes:n,protocol:t()});export{};
759
4
  //# sourceMappingURL=worker.js.map