pi-ptc-subagents 0.1.0 → 0.1.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.1] - 2026-09-23
9
+
10
+ PTC runs inside one agent turn no longer pay a cold start each. Nothing changes in
11
+ what a program may call or return; the difference is latency and cancellation
12
+ behaviour.
13
+
14
+ ### Changed
15
+
16
+ - **Per-turn worker pool.** One warm `worker_threads` Worker per surface
17
+ (`run_code`, `workflow`) is kept for the duration of an agent turn and reused
18
+ across its PTC runs — ~0 ms against ~58 ms for a cold spawn (median of 5). The
19
+ pool is created lazily by the first PTC run of the turn, the two surfaces do not
20
+ share one, and the extension's `turn_end` hook retires it. Idle workers are
21
+ `unref()`-ed, so a warm pool never keeps `pi` alive. `runPtcProgram()` gains an
22
+ optional `pool` field; without it the original cold-start path runs unchanged.
23
+ ADR-0017.
24
+ - **The worker entry is a real `dist/worker.js`.** The `data:` URL built from
25
+ `Function.prototype.toString()` is retired, so V8's code cache and Node's module
26
+ cache survive warm reuse. TypeScript the _model_ submits at run time is still
27
+ type-stripped inside the worker — that is `compileProgram`'s path, not the
28
+ bootstrap's.
29
+
30
+ ### Fixed
31
+
32
+ - **Cancellation settles within a bound in every ordering.** A still-armed deadline
33
+ is the ceiling when a cancel arrives first; a worker that never answers a timeout
34
+ settles at `timeoutMs + graceMs`. A superseded run's frames can no longer settle
35
+ its successor, and an idle worker answers a cancel instead of making the host wait
36
+ out its grace window. ADR-0017 §10.
37
+ - **`serializedBytes` no longer bills containers as zero bytes.** `[null × 100k]`
38
+ was counted as nothing, so a frame could slip past `maxMessageBytes`; the helper
39
+ now matches `JSON.stringify` byte-for-byte and sums `ArrayBuffer` / typed-array
40
+ leaves.
41
+
8
42
  ## [0.1.0] - 2026-09-22
9
43
 
10
44
  First npm release. Ports DSH PTC mode — Programmable Tool Calling, formerly
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ExtensionAPI, Skill } from "@earendil-works/pi-coding-agent";
2
- import { MessagePort } from "node:worker_threads";
2
+ import { MessagePort, Worker } from "node:worker_threads";
3
3
  //#region src/mode/ptc-mode.d.ts
4
4
  /** The two surfaces this package exposes. `/ptc on` needs at least one of them active. */
5
5
  export declare const PTC_MODE_TOOL_NAMES: readonly string[];
@@ -188,7 +188,7 @@ export declare function buildPtcSkillsSection(skills: readonly Skill[], format?:
188
188
  * realm (no VM) and ship no `agent()` (G1 #13 → B), so there is nothing to cap.
189
189
  * - `sandbox-unavailable` is not an error kind either: ADR-0007 ships no OS sandbox.
190
190
  */
191
- /** The two worker surfaces. Each run gets a fresh worker with exactly one of them. */
191
+ /** The two worker surfaces. A worker is spawned with exactly one of them and keeps it. */
192
192
  type PtcSurface = "run_code" | "workflow";
193
193
  interface PtcConfig {
194
194
  /** Default elapsed deadline for a run, including nested binding waits (R1 §1). */
@@ -226,6 +226,28 @@ interface PtcConfig {
226
226
  maxOldGenerationSizeMb: number;
227
227
  /** V8 young-generation cap handed to `new Worker({ resourceLimits })` (F2). */
228
228
  maxYoungGenerationSizeMb: number;
229
+ /**
230
+ * Per-turn worker pool capacity (ADR-0017 §3). Decoupled from `maxParallelSubCalls`
231
+ * because pool capacity is "resident workers" while `maxParallelSubCalls` is
232
+ * "in-flight calls" — two different ceilings. Default 4.
233
+ */
234
+ poolSize: number;
235
+ /**
236
+ * How long `runPtcProgram({ pool })` will wait for an available worker before
237
+ * failing the run with `kind: workerExit`. Decoupled from `timeoutMs` because
238
+ * acquire-wait is bounded by the pool's responsiveness, not the run's overall
239
+ * deadline. Default 30 000 ms.
240
+ */
241
+ poolAcquireTimeoutMs: number;
242
+ /**
243
+ * Ceiling on how long a pool's `drain()` waits for in-flight workers to release
244
+ * themselves before terminating them outright: a worker whose `release()` never
245
+ * arrives (a stuck dispatcher promise, an unobserved crash) must bound the turn-end
246
+ * hook rather than hang it. Not `graceMs` above, which is a *run's* cooperative-cancel
247
+ * window; this one is the *pool's* window at retirement, and drain resolves either way.
248
+ * Default 5 000 ms.
249
+ */
250
+ drainGraceMs: number;
229
251
  }
230
252
  export declare const DEFAULT_CONFIG: Readonly<PtcConfig>;
231
253
  /**
@@ -370,6 +392,80 @@ interface CreateBuiltinBindingsOptions {
370
392
  */
371
393
  export declare function createBuiltinBindings(options: CreateBuiltinBindingsOptions): BindingTable;
372
394
  //#endregion
395
+ //#region src/runtime/worker-pool.d.ts
396
+ /** Options the dispatcher hands to `new Worker(...)`. */
397
+ type WorkerPoolWorkerOptions = ConstructorParameters<typeof Worker>[1];
398
+ interface WorkerPoolOptions {
399
+ /**
400
+ * Build a fresh worker URL for each spawn. The pool calls this only when it actually
401
+ * needs to grow the resident count, so a heavy `buildWorkerUrl` is amortised across
402
+ * warm reuses.
403
+ */
404
+ buildWorkerUrl: () => URL;
405
+ /** Maximum resident workers; acquire when full queues the caller. Default 4. */
406
+ size?: number;
407
+ /** Acquire-wait bound; default 30 000 ms. */
408
+ acquireTimeoutMs?: number;
409
+ /**
410
+ * Bound on how long `drain()` waits for in-flight workers to be released before it
411
+ * terminates them anyway. Default 5 000 ms. Drain resolves either way — expiry is not
412
+ * an error, it just means a worker was retired without a clean release.
413
+ */
414
+ drainGraceMs?: number;
415
+ /** Options passed verbatim to `new Worker(url, options)` when spawning. */
416
+ workerOptions: WorkerPoolWorkerOptions;
417
+ }
418
+ interface WorkerPoolStats {
419
+ resident: number;
420
+ inFlight: number;
421
+ waiters: number;
422
+ totalAcquires: number;
423
+ poolExhaustions: number;
424
+ }
425
+ /**
426
+ * A fixed-capacity FIFO-served worker pool. Not safe for concurrent `acquire()` calls
427
+ * beyond Node's microtask interleaving guarantees — callers (`runPtcProgram`) await
428
+ * `acquire` before scheduling anything that touches the pool, so the only concurrent
429
+ * surface is `release()` against an in-flight worker.
430
+ */
431
+ export declare class WorkerPool {
432
+ #private;
433
+ constructor(options: WorkerPoolOptions);
434
+ /**
435
+ * Acquire an idle worker, spawn a new one if below capacity, or queue until capacity
436
+ * opens up. Throws `Error("pool acquire timed out after ${acquireTimeoutMs} ms")` on
437
+ * timeout; the dispatcher wraps that message with a `pool acquire failed: ` prefix (ADR-0017 §4).
438
+ */
439
+ acquire(): Promise<Worker>;
440
+ /**
441
+ * Hand a worker back to the pool. If a waiter is queued, hand the worker directly to
442
+ * them (no idle round-trip). Otherwise park the worker in `idle` — unless it has
443
+ * already exited, in which case retire it.
444
+ */
445
+ release(worker: Worker): void;
446
+ /**
447
+ * Last time `release()` was called for `worker`. The dispatcher reads this to
448
+ * publish `ptc:worker:reset-time` on a warm `ready` frame: `now - lastSettledAt`
449
+ * is the worker's per-run reset cost (ADR-0017 Addendum). `undefined` for a worker
450
+ * the pool never released (cold-start).
451
+ */
452
+ lastSettledAt(worker: Worker): number | undefined;
453
+ /**
454
+ * Wait for every in-flight worker to be released — bounded by `drainGraceMs` — then
455
+ * terminate every resident worker and reject every queued waiter. After `drain()`
456
+ * returns the pool is unusable.
457
+ *
458
+ * The bound matters: `drain()` is awaited from the pi `turn_end` hook, so a worker
459
+ * whose `release()` never arrives (a stuck dispatcher promise, an unobserved crash)
460
+ * must not hold the turn open. Workers still in flight when the grace expires are
461
+ * terminated with the idle ones, and `drain()` resolves normally — a single bad worker
462
+ * is not a reason to fail the turn boundary.
463
+ */
464
+ drain(): Promise<void>;
465
+ /** Read-only snapshot of the pool's counters; useful for tests and diagnostics. */
466
+ stats(): WorkerPoolStats;
467
+ }
468
+ //#endregion
373
469
  //#region src/runtime/dispatcher.d.ts
374
470
  interface RunPtcProgramOptions {
375
471
  /** Program body: an async function body (`return`/`await` at the top level). */
@@ -389,12 +485,26 @@ interface RunPtcProgramOptions {
389
485
  signal?: AbortSignal;
390
486
  /** Identifier carried to the worker; generated when omitted. */
391
487
  runId?: string;
488
+ /**
489
+ * Optional worker pool (ADR-0017). Absent = spawn a fresh worker and terminate it
490
+ * at run end (the original cold-start path). Present = the dispatcher acquires a
491
+ * worker from the pool at run start and releases it back at run end. Acquire waits
492
+ * are bounded by `config.poolAcquireTimeoutMs`; on timeout the run fails with
493
+ * `kind: workerExit`.
494
+ */
495
+ pool?: WorkerPool;
392
496
  }
393
497
  /**
394
- * One image hoisted out of a successful binding result (DSH parity — see ADR-0014).
498
+ * One image hoisted out of a successful binding result (DSH parity — see ADR-0014,
499
+ * ADR-0017 §8).
395
500
  *
396
- * `data` is base64 exactly as pi's own `read` tool returns it, so the tool layer can forward it as
397
- * an `ImageContent` block without re-encoding.
501
+ * `data` is base64 exactly as pi's own `read` tool returns it, so the tool layer can
502
+ * forward it as an `ImageContent` block without re-encoding. The host↔worker wire carries
503
+ * base64 too: the worker's channel is lossless JSON (a program may return part of a
504
+ * binding result), and a `MessagePort` transferList of raw bytes could not survive that
505
+ * contract — a transferred `ArrayBuffer` would be unusable as a program's return value.
506
+ * Keeping one representation end to end means zero conversions between the binding and
507
+ * pi's image adapter.
398
508
  */
399
509
  interface PtcImage {
400
510
  data: string;
@@ -413,9 +523,11 @@ interface PtcRunOutcome {
413
523
  * Images hoisted out of successful binding results, in call order.
414
524
  *
415
525
  * Every image a program's tool calls produced, with no cap and no dedupe: how many images a run
416
- * attaches is the program's business, exactly as it is in DSH. Present only when at least one was
417
- * hoisted — a failed or cancelled run attaches nothing, because the tool layer throws for it
418
- * (`codeRunFailedError`) and its image would never reach the model.
526
+ * attaches is the program's business, exactly as it is in DSH. Each entry carries base64 `data`,
527
+ * the representation pi's image adapter consumes, so the tool layer forwards it untouched.
528
+ * Present only when at least one was hoisted — a failed or cancelled run attaches nothing
529
+ * (ADR-0014 §2), because the tool layer throws for it (`codeRunFailedError`) and its image would
530
+ * never reach the model.
419
531
  */
420
532
  images?: PtcImage[];
421
533
  /** Failure details; absent on success. */
@@ -430,8 +542,29 @@ interface PtcRunOutcome {
430
542
  */
431
543
  export declare function runPtcProgram(options: RunPtcProgramOptions): Promise<PtcRunOutcome>;
432
544
  //#endregion
545
+ //#region src/runtime/turn-pools.d.ts
546
+ interface TurnPoolsOptions {
547
+ /** Per-turn limit overrides on top of `DEFAULT_CONFIG` (pool size, acquire bound, V8 caps). */
548
+ config?: Partial<PtcConfig>;
549
+ }
550
+ export declare class TurnPools {
551
+ #private;
552
+ constructor(options?: TurnPoolsOptions);
553
+ /**
554
+ * The pool for one surface, created on first request. Warm workers from an earlier
555
+ * run in this turn are served by the same pool (that is the point); a fresh
556
+ * `TurnPools` is how a turn gets a fresh set.
557
+ */
558
+ get(surface: PtcSurface): WorkerPool;
559
+ /**
560
+ * Terminate every pool this holder created. Idempotent, and safe to call from a turn
561
+ * boundary hook: a second call finds nothing to drain.
562
+ */
563
+ drain(): Promise<void>;
564
+ }
565
+ //#endregion
433
566
  //#region src/index.d.ts
434
567
  export default function ptcSubagents(pi: ExtensionAPI): void;
435
568
  //#endregion
436
- export type { Binding, BindingContext, BindingTable, CreateBuiltinBindingsOptions, DefaultModeConfig, ModeBlockReason, ModeEntryDecision, ModeEntryInput, ModeHideStrategy, PersistedModeState, PtcConfig, PtcErrorKind, PtcErrorShape, PtcJsonValue, PtcModeState, PtcRunOutcome, PtcSurface, RunPtcProgramOptions };
569
+ export type { Binding, BindingContext, BindingTable, CreateBuiltinBindingsOptions, DefaultModeConfig, ModeBlockReason, ModeEntryDecision, ModeEntryInput, ModeHideStrategy, PersistedModeState, PtcConfig, PtcErrorKind, PtcErrorShape, PtcImage, PtcJsonValue, PtcModeState, PtcRunOutcome, PtcSurface, RunPtcProgramOptions, TurnPoolsOptions, WorkerPoolOptions, WorkerPoolStats, WorkerPoolWorkerOptions };
437
570
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/mode/ptc-mode.ts","../src/mode/skills-section.ts","../src/runtime/limits.ts","../src/runtime/protocol.ts","../src/tools/text.ts","../src/runtime/bindings.ts","../src/runtime/dispatcher.ts","../src/index.ts"],"mappings":";;;;qBA2Ca;;;;;;;;;;qBAWA;;qBAGA;;qBAGA;;qBAGA;;KAGD;;;;;;;;;;;;;qBAcC,uBAAuB;;UAGnB;EACf;;EAEA;;EAEA;;;wBAIc,oBAAoB;;;;;UAQnB;EACf;EACA;;;UAQe;EACf;EACA;EACA;;;;;;;;;wBAUc,sBAAsB,mBAAmB;;KAwC7C;;KAGA;EACN;EAAa;EAAyB;;EACtC;EAAc,QAAQ;;;UAGX;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA,OAAO;;;wBASO,YAAY,sBAAsB;;;;;;;;wBAclC,YAAY,2BAA2B,MAAM;;;;;;;;;;;;;;;wBAqB7C,gBAAgB,OAAO,iBAAiB;;;;;;;wBA4BxC,cAAc,OAAO,cAAc;;;;;wBAQnC,4BACd,OAAO,cACP;;;;;;;;;;;wBAgBc,mBACd,WAAW,gCACX;EACG;EAAyB;;;;;;;;;wBAoBd,qBAAqB,6BAA6B,MAAM;;;;qBCvR3D;;qBAOA;;;;;;;wBASG,qBAAqB;;;;;;;;;;wBAarB,sBACd,iBAAiB,SACjB,UAAS,QAAQ,SAAS;;;;;;;;;;;;;;;;;;KCtChB;UAEK;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;EAOA;;;;EAIA;;;;;EAKA;;;EAGA;;EAEA;;EAEA;;EAEA;;qBAGW,gBAAgB,SAAS;;;;;;;qBAqBzB;;;;;;;wBAeG,gBAAgB,SAAQ,OAAO,aAA2B;;;;;;;wBAe1D,cAAc,YAAW,QAAQ,aAAkB;;;;;;;wBAmBnD,mBACd,+BACA,SAAQ;;;;cC9GJ;WACJ;WACA;WACA;WACA;;qBAEW,wBAAwB;;cAG/B;WACJ;WACA;WACA;WACA;WACA;WACA;WACA;;qBAEW,0BAA0B;cAGjC;WACJ;WACA;WACA;WACA;WACA;;qBAEW,sBAAsB;cAG7B;;WAEJ;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;qBAEW,uBAAuB;KACxB,uBAAuB,6BAA6B;;UAK/C;GACA,cAAA;;KAEL,kDAAkD,iBAAiB;UAE9D;EACf,MAAM;EACN;EACA;;;;;qBCvDW;;wBAwBG,UAAU;;;;;;;;;wBAgBV,aAAa;;;;;;wBA6Ib,iBAAiB,OAAO;;;;qBC7K3B;KASD,6BAA6B;qBAiB5B,gCAAgC;UAE5B;;EAEf,SAAS;;EAET;;EAEA;;EAEA;;UAGe;WACN;EACT,QAAQ,eAAe,SAAS,iBAAiB;;KAGvC,eAAe,oBAAoB;UAkC9B;;EAEf;;EAEA;;;;;;;;;wBAUc,sBAAsB,SAAS,+BAA+B;;;UCnF7D;;EAEf;EACA,SAAS;;EAET;;EAEA,UAAU;;EAEV;;EAEA;;EAEA,SAAS,QAAQ;;EAEjB,SAAS;;EAET;;;;;;;;UASe;EACf;EACA;;UAGe;;EAEf;;EAEA;;EAEA;;EAEA,QAAQ;;;;;;;;;EASR,SAAS;;EAET,QAAQ;;;;;;;;;wBAwBY,cAAc,SAAS,uBAAuB,QAAQ;;;wBCUpD,aAAa,IAAI"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/mode/ptc-mode.ts","../src/mode/skills-section.ts","../src/runtime/limits.ts","../src/runtime/protocol.ts","../src/tools/text.ts","../src/runtime/bindings.ts","../src/runtime/worker-pool.ts","../src/runtime/dispatcher.ts","../src/runtime/turn-pools.ts","../src/index.ts"],"mappings":";;;;qBA2Ca;;;;;;;;;;qBAWA;;qBAGA;;qBAGA;;qBAGA;;KAGD;;;;;;;;;;;;;qBAcC,uBAAuB;;UAGnB;EACf;;EAEA;;EAEA;;;wBAIc,oBAAoB;;;;;UAQnB;EACf;EACA;;;UAQe;EACf;EACA;EACA;;;;;;;;;wBAUc,sBAAsB,mBAAmB;;KAwC7C;;KAGA;EACN;EAAa;EAAyB;;EACtC;EAAc,QAAQ;;;UAGX;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA,OAAO;;;wBASO,YAAY,sBAAsB;;;;;;;;wBAclC,YAAY,2BAA2B,MAAM;;;;;;;;;;;;;;;wBAqB7C,gBAAgB,OAAO,iBAAiB;;;;;;;wBA4BxC,cAAc,OAAO,cAAc;;;;;wBAQnC,4BACd,OAAO,cACP;;;;;;;;;;;wBAgBc,mBACd,WAAW,gCACX;EACG;EAAyB;;;;;;;;;wBAoBd,qBAAqB,6BAA6B,MAAM;;;;qBCvR3D;;qBAOA;;;;;;;wBASG,qBAAqB;;;;;;;;;;wBAarB,sBACd,iBAAiB,SACjB,UAAS,QAAQ,SAAS;;;;;;;;;;;;;;;;;;KCtChB;UAEK;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;EAOA;;;;EAIA;;;;;EAKA;;;EAGA;;EAEA;;EAEA;;EAEA;;;;;;EAMA;;;;;;;EAOA;;;;;;;;;EASA;;qBAGW,gBAAgB,SAAS;;;;;;;qBAwBzB;;;;;;;wBAeG,gBAAgB,SAAQ,OAAO,aAA2B;;;;;;;wBAe1D,cAAc,YAAW,QAAQ,aAAkB;;;;;;;wBAmBnD,mBACd,+BACA,SAAQ;;;;cCvIJ;WACJ;WACA;WACA;WACA;;qBAEW,wBAAwB;;cAG/B;WACJ;WACA;WACA;WACA;WACA;WACA;WACA;;qBAEW,0BAA0B;cAGjC;WACJ;WACA;WACA;WACA;WACA;;qBAEW,sBAAsB;cAG7B;;WAEJ;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;qBAEW,uBAAuB;KACxB,uBAAuB,6BAA6B;;UAK/C;GACA,cAAA;;KAEL,kDAAkD,iBAAiB;UAE9D;EACf,MAAM;EACN;EACA;;;;;qBCvDW;;wBAwBG,UAAU;;;;;;;;;wBAgBV,aAAa;;;;;;wBA6Ib,iBAAiB,OAAO;;;;qBC7K3B;KASD,6BAA6B;qBAiB5B,gCAAgC;UAE5B;;EAEf,SAAS;;EAET;;EAEA;;EAEA;;UAGe;WACN;EACT,QAAQ,eAAe,SAAS,iBAAiB;;KAGvC,eAAe,oBAAoB;UAkC9B;;EAEf;;EAEA;;;;;;;;;wBAUc,sBAAsB,SAAS,+BAA+B;;;;KCjFlE,0BAA0B,6BAA6B;UAiClD;;;;;;EAMf,sBAAsB;;EAEtB;;EAEA;;;;;;EAMA;;EAEA,eAAe;;UAqCA;EACf;EACA;EACA;EACA;EACA;;;;;;;;qBASW;;EAyBX,YAAY,SAAS;;;;;;EA4CrB,WAAiB,QAAQ;;;;;;EA4DzB,QAAQ,QAAQ;;;;;;;EAuChB,cAAc,QAAQ;;;;;;;;;;;;EAetB,SAAe;;EA4Bf,SAAS;;;;UC7SM;;EAEf;EACA,SAAS;;EAET;;EAEA,UAAU;;EAEV;;EAEA;;EAEA,SAAS,QAAQ;;EAEjB,SAAS;;EAET;;;;;;;;EAQA,OAAO;;;;;;;;;;;;;;UAeQ;EACf;EACA;;UAGe;;EAEf;;EAEA;;EAEA;;EAEA,QAAQ;;;;;;;;;;;EAWR,SAAS;;EAET,QAAQ;;;;;;;;;wBAgGY,cAAc,SAAS,uBAAuB,QAAQ;;;UCtM3D;;EAEf,SAAS,QAAQ;;qBAGN;;EAIX,YAAY,UAAS;;;;;;EASrB,IAAI,SAAS,aAAa;;;;;EAuC1B,SAAe;;;;wBCyDO,aAAa,IAAI"}