pi-ptc-subagents 0.1.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,466 @@
1
- import { ExtensionAPI, Skill } from "@earendil-works/pi-coding-agent";
1
+ import { ExtensionAPI, Skill, truncateTail } from "@earendil-works/pi-coding-agent";
2
+ import "@earendil-works/pi-ai";
2
3
  import { MessagePort, Worker } from "node:worker_threads";
4
+ //#region src/runtime/ulid.d.ts
5
+ /**
6
+ * Lexically-sortable identifier (26-char Crockford base32). Brand-only: the runtime value is a
7
+ * plain `string`, the brand is a compile-time distinction that keeps a taskId from being
8
+ * passed into a cursor slot.
9
+ */
10
+ type ULID = string & {
11
+ readonly __brand: "ULID";
12
+ };
13
+ //#endregion
14
+ //#region src/runtime/child-process-lifecycle.d.ts
15
+ /**
16
+ * One line of the child's stdout, parsed as JSON. The child emits the events described
17
+ * in examples/extensions/subagent/index.ts (message_end, tool_result_end, error).
18
+ * Anything unparseable is dropped: the line is one event the host cannot interpret,
19
+ * and the run continues.
20
+ */
21
+ interface ParsedAgentEvent {
22
+ type: string;
23
+ message?: {
24
+ role?: string;
25
+ content?: ReadonlyArray<{
26
+ type?: string;
27
+ text?: string;
28
+ }>;
29
+ usage?: {
30
+ input?: number;
31
+ output?: number;
32
+ cacheRead?: number;
33
+ cacheWrite?: number;
34
+ cost?: {
35
+ total?: number;
36
+ };
37
+ };
38
+ model?: string;
39
+ stopReason?: string;
40
+ errorMessage?: string;
41
+ };
42
+ message_text?: string;
43
+ }
44
+ /**
45
+ * Opaque per-handle state owned by the adapter that produced it. The dispatch code
46
+ * never reads `opaque` directly; it only hands the handle back to adapter methods
47
+ * (`kill`, `events`, `exit`, `stderr`).
48
+ */
49
+ interface ChildHandle {
50
+ readonly id: ULID;
51
+ readonly opaque: unknown;
52
+ }
53
+ /**
54
+ * Spawn options shared by both adapters. `promptFile` / `sessionDir` / `sessionId` /
55
+ * `sessionName` are the R1 fields the background dispatch consumes (ADR-0022 §1/R1). The
56
+ * foreground `pi.dispatch` path passes only `cwd` / `env` / `signal` / `promptFile`.
57
+ */
58
+ interface ChildSpawnOptions {
59
+ cwd: string;
60
+ env?: NodeJS.ProcessEnv;
61
+ signal?: AbortSignal;
62
+ promptFile: string;
63
+ sessionDir?: string;
64
+ sessionId?: string;
65
+ sessionName?: string;
66
+ }
67
+ /** Process-exit shape the `close` event on `node:child_process.ChildProcess` produces. */
68
+ interface ChildExitValue {
69
+ code: number | null;
70
+ signal: NodeJS.Signals | null;
71
+ }
72
+ /**
73
+ * The lifecycle contract. Implementations:
74
+ * - `RealChildProcessLifecycle` - wraps `node:child_process.spawn` (production).
75
+ * - `MockChildProcessLifecycle` - in-memory queue + exit signal (tests).
76
+ */
77
+ interface ChildProcessLifecycle {
78
+ /**
79
+ * Launch one child process. The first element of `argv` is the executable; the rest
80
+ * are forwarded as command-line arguments. Returns an opaque handle the caller uses
81
+ * to talk to the same child via the other methods.
82
+ *
83
+ * `opts.env` is the full environment for the child; the caller is responsible for
84
+ * merging anything the child needs (e.g. `PI_PTC_DEPTH`) on top of `process.env`.
85
+ */
86
+ spawn(argv: readonly string[], opts: ChildSpawnOptions): ChildHandle;
87
+ /**
88
+ * Send `signal` to the child iff it is still attached. Absorbs "process already gone"
89
+ * throws so callers can fire-and-forget on cancel. Mirrors `safeKill` below.
90
+ */
91
+ kill(handle: ChildHandle, signal: "SIGTERM" | "SIGKILL"): void;
92
+ /**
93
+ * Async iterator over the child's stdout events. Yields one `ParsedAgentEvent` per
94
+ * JSONL line. The iterator ends after the child closes; callers should `await` it
95
+ * fully before reading `exit()`.
96
+ */
97
+ events(handle: ChildHandle): AsyncIterable<ParsedAgentEvent>;
98
+ /**
99
+ * Resolve when the child closes, with `(code, signal)` matching `node:child_process`'s
100
+ * `close` event. A clean exit is `(0, null)`; a signal kill is `(null, <signal>)`;
101
+ * a spawn failure surfaces as `(null, null)` once the error event has propagated.
102
+ */
103
+ exit(handle: ChildHandle): Promise<ChildExitValue>;
104
+ /**
105
+ * Resolve with the accumulated stderr text once the child has closed. Returns `""`
106
+ * when the child wrote nothing. Spawn failures append a `[spawn-error] ...` marker
107
+ * (same convention as the pre-BG-03 dispatch code) so callers can distinguish a
108
+ * failed spawn from a clean non-zero exit.
109
+ */
110
+ stderr(handle: ChildHandle): Promise<string>;
111
+ }
112
+ //#endregion
113
+ //#region src/runtime/task-storage.d.ts
114
+ /**
115
+ * The 6-state TaskRecord status (ADR-0022 §2). `queued` is deliberately absent in v1
116
+ * (spawn-or-reject, no in-task queue; cap=8 rejects above the limit immediately).
117
+ */
118
+ type TaskStatus = "running" | "stopping" | "succeeded" | "failed" | "canceled" | "lost";
119
+ /** Origin of the spawn — `ptc-program` from a PTC run, `ptc-batch` from a future batch entry. */
120
+ interface TaskSpawnSource {
121
+ kind: "ptc-program" | "ptc-batch";
122
+ /** Caller id (PTC run id for `ptc-program`; batch id for `ptc-batch`). */
123
+ callerId: string;
124
+ }
125
+ /**
126
+ * Session-level row tracking the lifecycle of one background child. 21 fields, exact shape from
127
+ * ADR-0022 §3. Optional fields are absent on records still `running` (e.g. `finishedAt` /
128
+ * `exitCode` / `outputRef` are written at transition time, not at spawn).
129
+ */
130
+ interface TaskRecord {
131
+ id: ULID;
132
+ label: string;
133
+ agentName: string;
134
+ /** 0 = parent's direct, 1 = grandchild, etc. (ADR-0016 recursive section). */
135
+ depth: number;
136
+ status: TaskStatus;
137
+ /** ms epoch. */
138
+ createdAt: number;
139
+ startedAt: number;
140
+ finishedAt?: number;
141
+ durationMs?: number;
142
+ /** Last state transition ms epoch; used for cursor ordering of events. */
143
+ transitionAt: number;
144
+ /** `<sessionDir>/tasks/<id>/output.log` once the child has flushed any output. */
145
+ outputRef?: string;
146
+ outputBytes?: number;
147
+ /** ≤ 2 KB inline preview (Map+preview, ADR-0022 §3). */
148
+ outputPreview?: string;
149
+ stopReason?: string;
150
+ errorMessage?: string;
151
+ exitCode?: number;
152
+ spawnSource: TaskSpawnSource;
153
+ parentTaskId?: ULID;
154
+ /** `<sessionDir>/tasks/<id>.pi-*` session file (R1). */
155
+ sessionFile?: string;
156
+ }
157
+ /**
158
+ * Per-subscriber cursor for one TaskRecord (ADR-0022 §5). Cursor is per-subscriber (not per-task)
159
+ * because fork semantics differ from single-session semantics: a forked branch observes events
160
+ * from `max(parent, child)` cursor onwards, not from task creation.
161
+ */
162
+ interface Subscription {
163
+ subscriberId: ULID;
164
+ taskId: ULID;
165
+ /** Monotonic ULID; advances on every event delivered. */
166
+ cursor: ULID;
167
+ status: "active" | "closed";
168
+ /** ms epoch. */
169
+ createdAt: number;
170
+ }
171
+ /**
172
+ * Append-only event in a subscription buffer. The `eventId` IS the cursor: monotonic ULID, the
173
+ * `loadEvents(since)` filter slices `[strictly-after since, ...]`. Schema fields beyond the
174
+ * cursor carry enough state for the renderer to emit `<bg-task-notification>` (ADR-0022 §7)
175
+ * without re-reading the TaskRecord.
176
+ */
177
+ interface TaskEvent {
178
+ /** Cursor / event id; the loadEvents(since) filter. */
179
+ eventId: ULID;
180
+ subscriptionId: ULID;
181
+ taskId: ULID;
182
+ /**
183
+ * Emit key per ADR-0022 §2 + §8, e.g. `task:<id>:running`, `task:<id>:->canceled`,
184
+ * `task:<id>:->lost`. Storage layer treats this as an opaque string.
185
+ */
186
+ type: string;
187
+ status: TaskStatus;
188
+ /** ms epoch; matches `TaskRecord.transitionAt` at the moment of emission. */
189
+ transitionAtMs: number;
190
+ outputBytes?: number;
191
+ /** ≤ 2 KB inline preview (only present when `outputBytes <= 2048`). */
192
+ outputPreview?: string;
193
+ }
194
+ //#endregion
195
+ //#region src/runtime/task-registry.d.ts
196
+ /** Minimal structured logger seam (ADR-0022 implementation spec §12). */
197
+ interface RegistryLogger {
198
+ info(msg: string): void;
199
+ warn(msg: string): void;
200
+ }
201
+ /**
202
+ * Thin, frozen, spawn-time projection of a TaskRecord (ADR-0022 §4). The program carries
203
+ * this; the registry stores the TaskRecord. The handle is *not* updated on transition.
204
+ */
205
+ interface DispatchHandle {
206
+ taskId: ULID;
207
+ label: string;
208
+ status: "running";
209
+ }
210
+ /**
211
+ * Per-call dependencies for one registry command. `callerId` is also the subscriber id
212
+ * (ADR-0022 §5, "Subscriber == owner"); `clock` must be the injected time source.
213
+ */
214
+ interface TransitionContext {
215
+ clock: () => number;
216
+ logger?: RegistryLogger;
217
+ callerId: string;
218
+ }
219
+ /**
220
+ * The three `lost` reasons ADR-0022 §8 keeps distinct in the TaskRecord's
221
+ * `errorMessage` field for auditability.
222
+ */
223
+ type LostReason = "session_ended_while_running" | "user_killed_via_esc" | "lost_on_session_restart";
224
+ /** All state mutations the TaskRegistry accepts (single-terminal-writer invariant, this file). */
225
+ type TaskCommand = {
226
+ kind: "spawn";
227
+ parentTaskId?: ULID;
228
+ handle: DispatchHandle;
229
+ record: Omit<TaskRecord, "id" | "status" | "createdAt" | "transitionAt">;
230
+ } | {
231
+ kind: "transition";
232
+ taskId: ULID;
233
+ to: TaskStatus;
234
+ reason?: string;
235
+ stopReason?: string;
236
+ errorMessage?: string;
237
+ exitCode?: number;
238
+ /**
239
+ * ADR-0022 §3: the child's captured-output projection written on the terminal
240
+ * transition. The BG-04 pump drains stdout, persists it through `OutputStorage`, and
241
+ * passes these three so `ptc_task_output` can find it.
242
+ */
243
+ outputRef?: string;
244
+ outputBytes?: number;
245
+ outputPreview?: string;
246
+ } | {
247
+ kind: "stop";
248
+ taskId: ULID;
249
+ reason: string;
250
+ } | {
251
+ kind: "reconcile-lost";
252
+ taskId: ULID;
253
+ reason: LostReason;
254
+ } | {
255
+ kind: "resolve-exit";
256
+ taskId: ULID;
257
+ exitCode: number;
258
+ outputRef?: string;
259
+ outputBytes?: number;
260
+ outputPreview?: string;
261
+ };
262
+ /** Outcome of one successful command: the persisted record, emitted events, and cursor. */
263
+ interface TransitionResult {
264
+ record: TaskRecord;
265
+ events: TaskEvent[];
266
+ /** The event id of the last emitted event; equal to the subscription's new cursor. */
267
+ cursor: ULID;
268
+ /**
269
+ * The status the record held *before* this command applied. Absent on `spawn` (there is no
270
+ * prior state); present on every transition/stop/resolve so a caller can report the source
271
+ * state without a second, racing read (ADR-0022 §8 late-arrival stop).
272
+ */
273
+ fromStatus?: TaskStatus;
274
+ }
275
+ /**
276
+ * In-process transition observer (issue #68 §1). Fired synchronously by
277
+ * {@link TaskRegistry.transition} **after** the new record and its event have been persisted,
278
+ * with the post-transition record and the emitted event. Observers are best-effort: a throwing
279
+ * observer is warned about and never corrupts the transition. Registration returns an
280
+ * idempotent unsubscribe.
281
+ */
282
+ type TaskTransitionObserver = (record: TaskRecord, event: TaskEvent) => void;
283
+ /** Read-side filter (ADR-0022 §3). `limit` defaults to 100; `orderBy` defaults to desc. */
284
+ interface TaskQuery {
285
+ status?: TaskStatus[];
286
+ label?: string;
287
+ limit?: number;
288
+ orderBy?: "createdAt-asc" | "createdAt-desc";
289
+ }
290
+ /** The five-method lifecycle surface from the BG-02 brief. */
291
+ interface TaskRegistry {
292
+ transition(command: TaskCommand, ctx: TransitionContext): Promise<TransitionResult>;
293
+ query(view: TaskQuery): Promise<TaskRecord[]>;
294
+ /**
295
+ * Load one TaskRecord by id; `null` when the id is unknown. The O(1) complement of `query`,
296
+ * used by the background pump to see whether a stop was requested before it writes the
297
+ * terminal transition (ADR-0022 §8).
298
+ */
299
+ get(taskId: ULID): Promise<TaskRecord | null>;
300
+ /**
301
+ * Subscribe to every successfully persisted state write (spawn included). Returns an
302
+ * idempotent unsubscribe. Observers run synchronously, in-process; see
303
+ * {@link TaskTransitionObserver} for the best-effort contract and single-writer ordering.
304
+ */
305
+ onTransition(observer: TaskTransitionObserver): () => void;
306
+ reconcileLostTasks(): Promise<TaskRecord[]>;
307
+ }
308
+ //#endregion
309
+ //#region src/runtime/output-storage.d.ts
310
+ /**
311
+ * The persistence seam for one task's captured output.
312
+ *
313
+ * `readOutput` returning `null` and `writeOutput` accepting any string are the whole contract;
314
+ * ordering, framing and truncation are the caller's job.
315
+ */
316
+ interface OutputStorage {
317
+ /** Load the task's raw output; `null` when nothing has been written for that id. */
318
+ readOutput(taskId: ULID): Promise<string | null>;
319
+ /** Persist (insert-or-replace) the task's raw output. */
320
+ writeOutput(taskId: ULID, content: string): Promise<void>;
321
+ /**
322
+ * The stable path/ref the dispatcher records on `TaskRecord.outputRef`. It exists even before
323
+ * the first `writeOutput` (the ref is the record's address, not the file's existence).
324
+ */
325
+ outputRef(taskId: ULID): string;
326
+ }
327
+ //#endregion
328
+ //#region src/runtime/dispatch.d.ts
329
+ /**
330
+ * Per-run in-flight `pi.dispatch` counter (ADR-0016 §2 + ADR-0022 §9). The dispatcher's
331
+ * foreground gate owns the canonical counter today; this class is the small exported seam
332
+ * ADR-0022 §9 asks for so the background branch can hold a slot for a child's whole
333
+ * lifetime and release it only on the terminal transition — a background task keeps
334
+ * counting against `dispatchConcurrency` after `dispatch()` has already returned. The
335
+ * dispatcher can adopt this type without a behaviour change.
336
+ */
337
+ declare class DispatchSlotCounter {
338
+ #private;
339
+ readonly limit: number;
340
+ constructor(limit: number);
341
+ /** Number of slots currently held. */
342
+ get active(): number;
343
+ /** Reserve one in-flight slot (optionally keyed by a per-task holder token); `false` means the cap is reached (hard reject, no queue). */
344
+ tryAcquire(holder?: string): boolean;
345
+ /**
346
+ * Release one slot; per-token idempotent (a late release for the same task is a no-op) and
347
+ * never negative. An anonymous release only frees an anonymous reservation.
348
+ */
349
+ release(holder?: string): void;
350
+ }
351
+ /**
352
+ * Injected dependencies for the background branch. The foreground path ignores this object
353
+ * entirely (it uses the module-level {@link DISPATCH_LIFECYCLE}); every field is optional so
354
+ * existing zero-argument call sites keep working. Tests pass an in-memory registry and a
355
+ * mock lifecycle so no real `pi` process is ever spawned.
356
+ */
357
+ interface DispatchDeps {
358
+ /** Session-level TaskRegistry (ADR-0022 §3). Defaults to a lazy in-memory registry. */
359
+ taskRegistry?: TaskRegistry;
360
+ /** Child lifecycle for the background spawn. Defaults to the shared real adapter. */
361
+ lifecycle?: ChildProcessLifecycle;
362
+ /** Per-run in-flight counter shared with the dispatcher's foreground gate (ADR-0022 §9). */
363
+ slots?: DispatchSlotCounter;
364
+ /** Time source for TaskRecord stamps. Defaults to `Date.now`. */
365
+ clock?: () => number;
366
+ /** Logger for the background pump's failure path. Defaults to a `console.warn` logger. */
367
+ logger?: RegistryLogger;
368
+ /**
369
+ * ADR-0022 §3: where the pump persists a task's drained stdout. When present, the terminal
370
+ * transition records `outputRef` / `outputBytes` / `outputPreview` so `ptc_task_output`
371
+ * can dereference the bytes (BG-07).
372
+ */
373
+ outputStorage?: OutputStorage;
374
+ }
375
+ //#endregion
376
+ //#region src/runtime/notification-pipeline.d.ts
377
+ /** Idle-wake callback: one subscriber's pending events, delivered in cursor order. */
378
+ type IdleWakeHandler = (subscriberId: ULID, events: TaskEvent[]) => void;
379
+ /**
380
+ * The five-method pipeline surface (Ticket BG-05). The concrete class adds the
381
+ * dispatcher-facing `notifyIdle` wake trigger on top of this interface.
382
+ */
383
+ interface NotificationPipeline {
384
+ /** Create (or return the existing) subscription for one (subscriberId, taskId) pair. */
385
+ subscribe(subscriberId: ULID, taskId: ULID, since?: ULID): Promise<Subscription>;
386
+ /** Read pending events strictly newer than the cursor, WITHOUT advancing the cursor. */
387
+ drainPending(subscriberId: ULID, taskId: ULID): Promise<TaskEvent[]>;
388
+ /** Advance the cursor to `untilCursor`; never moves backwards (idempotent). */
389
+ acknowledgeEvents(subscriberId: ULID, taskId: ULID, untilCursor: ULID): Promise<void>;
390
+ /** Register an idle-wake handler; handlers fire in registration order. */
391
+ onIdleWake(handler: IdleWakeHandler): void;
392
+ /** Count events strictly newer than the cursor. */
393
+ pendingCount(subscriberId: ULID, taskId: ULID): Promise<number>;
394
+ }
395
+ //#endregion
396
+ //#region src/runtime/task-notification.d.ts
397
+ /**
398
+ * One drained event joined to the TaskRecord it describes. The join is required: the event
399
+ * lacks `label` / `agentName` / `depth` / `durationMs` / `outputRef` (see the module
400
+ * note above), all of which the ADR-0022 §7 child element must carry.
401
+ */
402
+ interface TaskNotificationItem {
403
+ event: TaskEvent;
404
+ record: TaskRecord;
405
+ }
406
+ //#endregion
407
+ //#region src/runtime/background-runtime.d.ts
408
+ /** Best-effort reporter for a bind failure; the index notifies the user from it. */
409
+ type BackgroundBindReporter = (message: string) => void;
410
+ /** One cursor to advance after a drained batch has actually been delivered. */
411
+ interface NotificationAck {
412
+ subscriberId: ULID;
413
+ taskId: ULID;
414
+ cursor: ULID;
415
+ /**
416
+ * The concrete session pipeline this drain READ from, carried so the ack lands on the same
417
+ * session even if a rebind happens between the drain and the send (P1). Without it the ack goes
418
+ * through the stable proxy's *current* delegate and targets a session that never knew the
419
+ * subscription, leaving the original cursor unadvanced and re-delivering the batch.
420
+ */
421
+ sink: NotificationPipeline;
422
+ }
423
+ /**
424
+ * One drain: the events to render plus the cursors the caller acknowledges after a successful
425
+ * send. The split is what lets a failed send leave the cursor unadvanced (ADR-0022 §5/§6).
426
+ */
427
+ interface NotificationDrain {
428
+ items: TaskNotificationItem[];
429
+ acks: readonly NotificationAck[];
430
+ }
431
+ /**
432
+ * The holder. `registry` / `outputStorage` / `pipeline` are stable objects safe to capture
433
+ * before any session exists; `bindSession` swaps the session delegate underneath them.
434
+ */
435
+ interface BackgroundTaskRuntime {
436
+ readonly registry: TaskRegistry;
437
+ readonly outputStorage: OutputStorage;
438
+ readonly pipeline: NotificationPipeline;
439
+ readonly slots: DispatchSlotCounter;
440
+ readonly lifecycle: ChildProcessLifecycle;
441
+ /** Stable deps the two PTC tools hand to `runPtcProgram` (ADR-0022 §9). */
442
+ readonly dispatchDeps: DispatchDeps;
443
+ /** Session clock (`Date.now` unless injected); passed to `ptc_task_stop`. */
444
+ readonly clock: () => number;
445
+ /**
446
+ * (Re)bind to a session: build the session storage/registry/pipeline, swap the delegates, then
447
+ * reconcile. Returns the records marked `lost`. A bind failure (storage construction or the
448
+ * reconcile sweep) is reported via `reporter` and the logger, and leaves the previous delegate
449
+ * usable; it never rejects and never partially swaps.
450
+ */
451
+ bindSession(sessionDir: string | undefined, reporter?: BackgroundBindReporter): Promise<TaskRecord[]>;
452
+ /** Drain undelivered events for a subscriber as `{event, record}` pairs ready for the renderer. */
453
+ drainNotifications(subscriberId: ULID): Promise<NotificationDrain>;
454
+ /**
455
+ * Advance the subscription cursor for a drained batch. The caller invokes this ONLY after the
456
+ * batch's message was actually delivered (ADR-0022 §5/§6); a failed send leaves the cursor
457
+ * where it was so the next drain re-delivers the event.
458
+ */
459
+ acknowledgeNotifications(acks: readonly NotificationAck[]): Promise<void>;
460
+ /** Mark every non-terminal task lost (given reason) and release resources. */
461
+ shutdown(reason: LostReason): Promise<TaskRecord[]>;
462
+ }
463
+ //#endregion
3
464
  //#region src/mode/ptc-mode.d.ts
4
465
  /** The two surfaces this package exposes. `/ptc on` needs at least one of them active. */
5
466
  export declare const PTC_MODE_TOOL_NAMES: readonly string[];
@@ -344,6 +805,27 @@ interface PtcErrorShape {
344
805
  message: string;
345
806
  stack?: string;
346
807
  }
808
+ /**
809
+ * One binding call the program made, as the dispatcher recorded it.
810
+ *
811
+ * Host-side only: this shape never crosses the wire — the existing `call` /
812
+ * `call-result` frames already carry every fact, and the host reconstructs the
813
+ * record at the seam where the facts are known (`SubCallTracker`,
814
+ * `src/runtime/sub-call-tracker.ts`). A snapshot reaches the renderer
815
+ * via `PtcToolDetails.subCalls`.
816
+ */
817
+ type SubCallStatus = "running" | "ok" | "error" | "cancelled" | "rejected";
818
+ interface SubCallRecord {
819
+ callId: number;
820
+ name: string;
821
+ args: unknown;
822
+ status: SubCallStatus;
823
+ startMs: number;
824
+ endMs?: number;
825
+ durationMs?: number;
826
+ resultSummary?: string;
827
+ errorMessage?: string;
828
+ }
347
829
  //#endregion
348
830
  //#region src/tools/text.d.ts
349
831
  /** Hard cap for one rendered line; longer lines are truncated with `…`. */
@@ -389,6 +871,29 @@ interface BindingContext {
389
871
  depth: number;
390
872
  /** Maximum allowed depth; passed through to `pi.dispatch` for the depth check. */
391
873
  maxDispatchDepth: number;
874
+ /**
875
+ * ADR-0022 §5: the TaskRecord owner / subscription subscriber. The dispatcher threads the
876
+ * run id here so a background task's events are addressed to the spawning run.
877
+ */
878
+ callerId?: string;
879
+ /**
880
+ * ADR-0022 R1: the session dir a background child persists into. Threaded from the
881
+ * dispatcher (see `RunPtcProgramOptions.sessionDir`); when no dir is available the spawn
882
+ * keeps the foreground no-session shape.
883
+ */
884
+ sessionDir?: string;
885
+ /**
886
+ * ADR-0022 §3/reopen R-m12: this process's own background task id, when this run is inside a
887
+ * background child. Threaded from the dispatcher (see `RunPtcProgramOptions.parentTaskId`)
888
+ * and forwarded to `pi.dispatch` so a nested spawn records `TaskRecord.parentTaskId`.
889
+ */
890
+ parentTaskId?: ULID;
891
+ /**
892
+ * ADR-0022 §9: per-run dispatch dependencies. The dispatcher supplies the run's shared
893
+ * `DispatchSlotCounter` here; a host may also pass session-level deps (registry / lifecycle /
894
+ * output storage) so the binding's `dispatch()` call shares them.
895
+ */
896
+ dispatchDeps?: DispatchDeps;
392
897
  }
393
898
  interface Binding {
394
899
  readonly name: string;
@@ -519,8 +1024,28 @@ interface RunPtcProgramOptions {
519
1024
  * to 0.
520
1025
  */
521
1026
  depth?: number;
1027
+ /**
1028
+ * ADR-0022 §3/reopen R-m12: this process's own background task id, when the run is inside a
1029
+ * child spawned by `pi.dispatch({ background: true })`. The entrypoint reads it from
1030
+ * `PI_PTC_TASK_ID` and threads it to the binding context so a nested spawn records
1031
+ * `TaskRecord.parentTaskId`. Absent for a top-level session.
1032
+ */
1033
+ parentTaskId?: ULID;
522
1034
  /** Identifier carried to the worker; generated when omitted. */
523
1035
  runId?: string;
1036
+ /**
1037
+ * ADR-0022 R1: the session dir background children persist into, when the host has one.
1038
+ * Threaded to the binding context so `pi.dispatch({ background: true })` can stamp the R1
1039
+ * session flags; absent means the background spawn keeps the foreground no-session shape.
1040
+ */
1041
+ sessionDir?: string;
1042
+ /**
1043
+ * ADR-0022 §3/§9: session-level dispatch dependencies (TaskRegistry / OutputStorage /
1044
+ * lifecycle / clock / logger). The dispatcher merges its per-run `DispatchSlotCounter`
1045
+ * into this bag before handing it to the `pi.dispatch` binding, so background tasks count
1046
+ * against this run's `dispatchConcurrency` for their whole lifetime.
1047
+ */
1048
+ dispatchDeps?: DispatchDeps;
524
1049
  /**
525
1050
  * Optional worker pool (ADR-0017). Absent = spawn a fresh worker and terminate it
526
1051
  * at run end (the original cold-start path). Present = the dispatcher acquires a
@@ -529,6 +1054,18 @@ interface RunPtcProgramOptions {
529
1054
  * `kind: workerExit`.
530
1055
  */
531
1056
  pool?: WorkerPool;
1057
+ /**
1058
+ * Called every time a sub-call starts or ends, so the caller can push a live partial result
1059
+ * and have the tree visible while the run is in flight (ADR-0021 §4). Never called after the
1060
+ * run settles — `finish` owns the terminal snapshot. Absent means "no live updates wanted"
1061
+ * (direct library use, tests that only assert the terminal outcome).
1062
+ *
1063
+ * The argument is a **thunk**, not the snapshot itself: a wide `Promise.all` produces an
1064
+ * event per call, and `snapshot()` copies every record. Building it eagerly would make N
1065
+ * sequential calls cost O(N²) copies for pushes that the caller's throttle mostly drops.
1066
+ * Call it only when a push is actually due.
1067
+ */
1068
+ onSubCallChange?: (snapshot: () => readonly SubCallRecord[]) => void;
532
1069
  }
533
1070
  /**
534
1071
  * One image hoisted out of a successful binding result (DSH parity — see ADR-0014,
@@ -568,6 +1105,15 @@ interface PtcRunOutcome {
568
1105
  images?: PtcImage[];
569
1106
  /** Failure details; absent on success. */
570
1107
  error?: PtcErrorShape;
1108
+ /**
1109
+ * One record per binding call the program made (ADR-0021).
1110
+ *
1111
+ * Present only when the dispatcher tracked sub-calls for the surface in question (always,
1112
+ * post-ADR-0021 — the dispatcher wires `SubCallTracker` for every run). Order is host-side
1113
+ * dispatch order; the renderer reads it as a point-in-time snapshot. Absent for runs that settled
1114
+ * before the tracker was constructed (the pre-`Promise` abort and pool-acquire-failed paths).
1115
+ */
1116
+ subCalls?: readonly SubCallRecord[];
571
1117
  }
572
1118
  /**
573
1119
  * Run one PTC program in a fresh worker and resolve with its outcome.
@@ -600,7 +1146,15 @@ export declare class TurnPools {
600
1146
  }
601
1147
  //#endregion
602
1148
  //#region src/index.d.ts
603
- export default function ptcSubagents(pi: ExtensionAPI): void;
1149
+ /**
1150
+ * Optional seams for the extension factory. Production calls `ptcSubagents(pi)`; tests may inject
1151
+ * a pre-built background runtime (with a mock child lifecycle) so no real `pi` process is spawned.
1152
+ */
1153
+ export interface PtcSubagentsOptions {
1154
+ /** Use this session-scoped background runtime instead of constructing one. */
1155
+ backgroundRuntime?: BackgroundTaskRuntime;
1156
+ }
1157
+ export default function ptcSubagents(pi: ExtensionAPI, options?: PtcSubagentsOptions): void;
604
1158
  //#endregion
605
1159
  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 };
606
1160
  //# sourceMappingURL=index.d.ts.map