pi-ptc-subagents 0.1.1 → 0.1.2
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 +27 -0
- package/dist/index.d.ts +48 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -45
- package/dist/index.js.map +1 -1
- package/dist/{protocol-DLR50XxW.js → protocol-DUnAP1Ro.js} +12 -2
- package/dist/protocol-DUnAP1Ro.js.map +1 -0
- package/dist/worker.js +50 -6
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/protocol-DLR50XxW.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ 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.2] - 2026-09-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`pi.dispatch` is registered in production again.** The injection check compared the
|
|
13
|
+
binding-name array by reference against `DEFAULT_BINDING_NAMES`; production resolves
|
|
14
|
+
names through a `.filter()` that always returns a fresh array, so the comparison never
|
|
15
|
+
held and shipped sessions had no `pi.dispatch` at all. Injection now keys off a new
|
|
16
|
+
`includeDispatch` option (defaulting to "the caller passed no explicit `names`"), and
|
|
17
|
+
both shipped tools pass it explicitly. ADR-0016.
|
|
18
|
+
- **The dispatch concurrency cap honours `dispatchConcurrency` (default 8) and rejects
|
|
19
|
+
immediately instead of queueing.** The dispatcher used to read `maxParallelSubCalls`
|
|
20
|
+
(default 10) and FIFO-queue the overflow; per ADR-0016 §2 the N+1th concurrent
|
|
21
|
+
`pi.dispatch` now resolves at once with `{ status: "rejected", errorMessage:
|
|
22
|
+
"dispatch concurrency limit reached" }`. The cap applies to `pi.dispatch` only and
|
|
23
|
+
has its own counter; builtin binding fan-out keeps DSH's `maxParallelSubCalls` (10)
|
|
24
|
+
FIFO-queueing semantics (ADR-0004), so in-flight builtin calls never consume
|
|
25
|
+
dispatch slots.
|
|
26
|
+
- **The depth-limit rejection message is verbatim again.** The program receives
|
|
27
|
+
exactly `dispatch depth limit reached`, matching what the child's
|
|
28
|
+
`<pi-ptc-context>` hint promises — the diagnostic suffix is gone.
|
|
29
|
+
- **`maxDispatchDepth` bounds recursion again.** Every run reported depth 0 and
|
|
30
|
+
children never inherited it, so the depth check could never fire. `dispatch()` now
|
|
31
|
+
stamps `PI_PTC_DEPTH` on the child subprocess's environment, the extension
|
|
32
|
+
entrypoint reads it back, and `runPtcProgram()` accepts a `depth` baseline that
|
|
33
|
+
reaches the binding context. ADR-0016 Recursive section.
|
|
34
|
+
|
|
8
35
|
## [0.1.1] - 2026-09-23
|
|
9
36
|
|
|
10
37
|
PTC runs inside one agent turn no longer pay a cold start each. Nothing changes in
|
package/dist/index.d.ts
CHANGED
|
@@ -201,16 +201,24 @@ interface PtcConfig {
|
|
|
201
201
|
maxMessageBytes: number;
|
|
202
202
|
/** Admission control for simultaneously in-flight worker→host binding calls (ADR-0004). */
|
|
203
203
|
maxPendingCalls: number;
|
|
204
|
-
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
204
|
+
/**
|
|
205
|
+
* Concurrent builtin binding dispatches; DSH's `maxParallelSubCalls`
|
|
206
|
+
* (ADR-0004 consequence), mirrored verbatim (10). The overflow
|
|
207
|
+
* FIFO-queues for a slot instead of failing. This is the authoritative
|
|
208
|
+
* cap for the builtin fan-out path — independent of
|
|
209
|
+
* `dispatchConcurrency`, with its own counter: neither cap throttles
|
|
210
|
+
* the other.
|
|
211
|
+
*/
|
|
210
212
|
maxParallelSubCalls: number;
|
|
211
|
-
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
213
|
+
/**
|
|
214
|
+
* Per-run hard cap on concurrently in-flight `pi.dispatch(...)` calls,
|
|
215
|
+
* enforced by the dispatcher: the next concurrent call resolves
|
|
216
|
+
* immediately with `{ status: "rejected", errorMessage: "dispatch
|
|
217
|
+
* concurrency limit reached" }` — never queued, never spawned.
|
|
218
|
+
* Default 8, matches pi's `subagent` extension `MAX_PARALLEL_TASKS`.
|
|
219
|
+
* ADR-0016 section 2. Independent of `maxParallelSubCalls`: builtin
|
|
220
|
+
* calls never consume a dispatch slot and vice versa.
|
|
221
|
+
*/
|
|
214
222
|
dispatchConcurrency: number;
|
|
215
223
|
/** Maximum recursion depth for `pi.dispatch`. The child PTC run spawned by
|
|
216
224
|
* the (depth+1)-th dispatch is allowed only when childDepth <= maxDispatchDepth.
|
|
@@ -227,9 +235,10 @@ interface PtcConfig {
|
|
|
227
235
|
/** V8 young-generation cap handed to `new Worker({ resourceLimits })` (F2). */
|
|
228
236
|
maxYoungGenerationSizeMb: number;
|
|
229
237
|
/**
|
|
230
|
-
* Per-turn worker pool capacity (ADR-0017 §3). Decoupled from
|
|
231
|
-
* because pool capacity is "resident workers" while
|
|
232
|
-
* "in-flight calls" — two different ceilings.
|
|
238
|
+
* Per-turn worker pool capacity (ADR-0017 §3). Decoupled from
|
|
239
|
+
* `dispatchConcurrency` because pool capacity is "resident workers" while
|
|
240
|
+
* `dispatchConcurrency` is "in-flight calls" — two different ceilings.
|
|
241
|
+
* Default 4.
|
|
233
242
|
*/
|
|
234
243
|
poolSize: number;
|
|
235
244
|
/**
|
|
@@ -361,6 +370,15 @@ export declare function renderModelValue(value: PtcJsonValue): string;
|
|
|
361
370
|
/** pi's built-in tools that can be exposed as bindings, in native order. */
|
|
362
371
|
export declare const BUILTIN_BINDING_NAMES: readonly ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
363
372
|
type BuiltinBindingName = (typeof BUILTIN_BINDING_NAMES)[number];
|
|
373
|
+
/**
|
|
374
|
+
* Bindings exposed when the caller does not pass an explicit name list.
|
|
375
|
+
*
|
|
376
|
+
* `bash` is included on purpose: DSH's PTC preset still mounts `tool-bash`/`tool-pwsh`
|
|
377
|
+
* (R1 §2 — PTC mode hides the tools from the wire list and re-exposes the same registry
|
|
378
|
+
* as bindings), so a PTC program written against DSH may run shell commands. Leaving it
|
|
379
|
+
* out would silently shrink the surface relative to DSH. Callers that want a read-only
|
|
380
|
+
* PTC surface pass an explicit subset.
|
|
381
|
+
*/
|
|
364
382
|
export declare const DEFAULT_BINDING_NAMES: readonly BuiltinBindingName[];
|
|
365
383
|
interface BindingContext {
|
|
366
384
|
/** Aborted when the run is cancelled, times out, or settles. */
|
|
@@ -382,6 +400,16 @@ interface CreateBuiltinBindingsOptions {
|
|
|
382
400
|
cwd: string;
|
|
383
401
|
/** Subset of {@link BUILTIN_BINDING_NAMES}; defaults to all of them (bash included). */
|
|
384
402
|
names?: readonly string[];
|
|
403
|
+
/**
|
|
404
|
+
* Whether the parallel binding `pi.dispatch` (ADR-0016) joins the table. Defaults to
|
|
405
|
+
* "did the caller curate the surface": `true` when `names` is omitted, `false` when an
|
|
406
|
+
* explicit list is passed (R3's read-only PTC surface pattern). Deciding on whether
|
|
407
|
+
* `names` was provided — not on array identity with {@link DEFAULT_BINDING_NAMES} —
|
|
408
|
+
* matters because production callers resolve names through a `.filter()` that always
|
|
409
|
+
* returns a fresh array. The two shipped tools pass `true` explicitly: the dispatch
|
|
410
|
+
* binding is part of every production surface.
|
|
411
|
+
*/
|
|
412
|
+
includeDispatch?: boolean;
|
|
385
413
|
}
|
|
386
414
|
/**
|
|
387
415
|
* Build the binding table for one run.
|
|
@@ -483,6 +511,14 @@ interface RunPtcProgramOptions {
|
|
|
483
511
|
config?: Partial<PtcConfig>;
|
|
484
512
|
/** Cancels the run; the worker gets a cooperative cancel window before termination. */
|
|
485
513
|
signal?: AbortSignal;
|
|
514
|
+
/**
|
|
515
|
+
* Depth of this run in the `pi.dispatch` recursion chain (ADR-0016 Recursive section):
|
|
516
|
+
* 0 for the parent turn's run, 1+ for a run inside a child spawned by `pi.dispatch`.
|
|
517
|
+
* Handed to the binding context so the dispatch binding can bound recursion. The
|
|
518
|
+
* extension entrypoint derives it from `PI_PTC_DEPTH`; direct library use defaults
|
|
519
|
+
* to 0.
|
|
520
|
+
*/
|
|
521
|
+
depth?: number;
|
|
486
522
|
/** Identifier carried to the worker; generated when omitted. */
|
|
487
523
|
runId?: string;
|
|
488
524
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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/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
|
|
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":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as describeValue, i as WORKER_FRAME_KIND, n as PTC_ERROR_KIND, o as isPtcHostFrame, r as PTC_LOG_LEVEL, s as isPtcWorkerFrame, t as HOST_FRAME_KIND } from "./protocol-DUnAP1Ro.js";
|
|
2
2
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, defineTool, formatSize, formatSkillsForPrompt, getAgentDir, truncateTail } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { validateToolArguments } from "@earendil-works/pi-ai";
|
|
@@ -16,14 +16,15 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
16
16
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
17
17
|
//#region src/runtime/dispatch.ts
|
|
18
18
|
/**
|
|
19
|
-
* pi.dispatch:
|
|
19
|
+
* pi.dispatch: the parallel binding that spawns a fresh pi subprocess per call.
|
|
20
20
|
*
|
|
21
|
-
* ADR-0016 (2026-09-23) is the contract
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
* ADR-0016 (2026-09-23) is the contract, and this file is the whole implementation:
|
|
22
|
+
* agent-markdown discovery (`discoverAgent`, with the agentScope user/project split),
|
|
23
|
+
* the recursion-depth hint appended to the child's system prompt (`appendDepthHint`),
|
|
24
|
+
* the argv the child pi is launched with (`buildArgv`), and `dispatch()` itself —
|
|
25
|
+
* spawn, JSON-line event parsing, usage accumulation, the close-outcome decision
|
|
26
|
+
* (`decideCloseOutcome`), and SIGTERM → SIGKILL signal propagation when the run that
|
|
27
|
+
* issued the dispatch is cancelled.
|
|
27
28
|
*
|
|
28
29
|
* Behaviourally compatible with pi's examples/extensions/subagent/index.ts reference
|
|
29
30
|
* (--mode json, -p, --no-session, --append-system-prompt <tmpfile>), but not cooperative:
|
|
@@ -63,15 +64,32 @@ function appendDepthHint(systemPrompt, depth, maxDepth) {
|
|
|
63
64
|
].join("\n");
|
|
64
65
|
return systemPrompt.length === 0 ? hint : systemPrompt + "\n\n" + hint;
|
|
65
66
|
}
|
|
66
|
-
/**
|
|
67
|
-
function dispatchDepthLimitReached(
|
|
67
|
+
/** Result returned when the depth limit is exceeded. */
|
|
68
|
+
function dispatchDepthLimitReached() {
|
|
68
69
|
return {
|
|
69
70
|
text: "",
|
|
70
71
|
status: "rejected",
|
|
71
72
|
agentName: "unknown",
|
|
72
73
|
durationMs: 0,
|
|
73
74
|
exitCode: -1,
|
|
74
|
-
errorMessage: "dispatch depth limit reached
|
|
75
|
+
errorMessage: "dispatch depth limit reached"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Verbatim ADR-0016 §2 message for the per-run dispatch concurrency cap. Exported as a
|
|
80
|
+
* named constant so the contract string has one definition and cannot drift (tests pin
|
|
81
|
+
* it character for character).
|
|
82
|
+
*/
|
|
83
|
+
const DISPATCH_CONCURRENCY_LIMIT_MESSAGE = "dispatch concurrency limit reached";
|
|
84
|
+
/** Result returned when the per-run dispatch concurrency cap is exceeded (ADR-0016 §2). */
|
|
85
|
+
function dispatchConcurrencyLimitReached() {
|
|
86
|
+
return {
|
|
87
|
+
text: "",
|
|
88
|
+
status: "rejected",
|
|
89
|
+
agentName: "unknown",
|
|
90
|
+
durationMs: 0,
|
|
91
|
+
exitCode: -1,
|
|
92
|
+
errorMessage: DISPATCH_CONCURRENCY_LIMIT_MESSAGE
|
|
75
93
|
};
|
|
76
94
|
}
|
|
77
95
|
/**
|
|
@@ -281,7 +299,7 @@ function cleanupTmp(tmp) {
|
|
|
281
299
|
*/
|
|
282
300
|
async function dispatch(input, ctx) {
|
|
283
301
|
const childDepth = ctx.depth + 1;
|
|
284
|
-
if (childDepth > ctx.
|
|
302
|
+
if (childDepth > ctx.maxDispatchDepth) return dispatchDepthLimitReached();
|
|
285
303
|
const cwd = input.cwd ?? ctx.cwd;
|
|
286
304
|
const agentScope = input.agentScope ?? "user";
|
|
287
305
|
const start = Date.now();
|
|
@@ -294,7 +312,7 @@ async function dispatch(input, ctx) {
|
|
|
294
312
|
exitCode: 1,
|
|
295
313
|
errorMessage: "unknown agent: " + input.agent + " (agentScope=" + agentScope + ", cwd=" + cwd + ")"
|
|
296
314
|
};
|
|
297
|
-
const fullPrompt = appendDepthHint(agent.systemPrompt, childDepth, ctx.
|
|
315
|
+
const fullPrompt = appendDepthHint(agent.systemPrompt, childDepth, ctx.maxDispatchDepth);
|
|
298
316
|
const tmp = await writePromptToTempFile(agent.name, fullPrompt);
|
|
299
317
|
const argv = buildArgv(input, agent, tmp.filePath);
|
|
300
318
|
return await new Promise((resolve) => {
|
|
@@ -341,7 +359,11 @@ async function dispatch(input, ctx) {
|
|
|
341
359
|
"ignore",
|
|
342
360
|
"pipe",
|
|
343
361
|
"pipe"
|
|
344
|
-
]
|
|
362
|
+
],
|
|
363
|
+
env: {
|
|
364
|
+
...process.env,
|
|
365
|
+
PI_PTC_DEPTH: String(childDepth)
|
|
366
|
+
}
|
|
345
367
|
});
|
|
346
368
|
} catch (err) {
|
|
347
369
|
finalize("rejected", "failed to spawn pi: " + (err instanceof Error ? err.message : String(err)));
|
|
@@ -439,6 +461,8 @@ const BUILTIN_BINDING_NAMES = [
|
|
|
439
461
|
"find",
|
|
440
462
|
"ls"
|
|
441
463
|
];
|
|
464
|
+
/** Parallel binding name (ADR-0016). */
|
|
465
|
+
const DISPATCH_BINDING_NAME = "pi.dispatch";
|
|
442
466
|
/**
|
|
443
467
|
* Bindings exposed when the caller does not pass an explicit name list.
|
|
444
468
|
*
|
|
@@ -448,11 +472,6 @@ const BUILTIN_BINDING_NAMES = [
|
|
|
448
472
|
* out would silently shrink the surface relative to DSH. Callers that want a read-only
|
|
449
473
|
* PTC surface pass an explicit subset.
|
|
450
474
|
*/
|
|
451
|
-
/** Parallel binding name (ADR-0016). Always bound alongside the builtin set;
|
|
452
|
-
* opt-out is the callers responsibility via an explicit subset to
|
|
453
|
-
* createBuiltinBindings (today the subset is restricted to builtin names,
|
|
454
|
-
* so opt-out is effectively use a future flag). */
|
|
455
|
-
const DISPATCH_BINDING_NAME = "pi.dispatch";
|
|
456
475
|
const DEFAULT_BINDING_NAMES = BUILTIN_BINDING_NAMES;
|
|
457
476
|
const BUILTIN_TOOL_FACTORIES = {
|
|
458
477
|
read: createReadTool,
|
|
@@ -472,6 +491,7 @@ const BUILTIN_TOOL_FACTORIES = {
|
|
|
472
491
|
*/
|
|
473
492
|
function createBuiltinBindings(options) {
|
|
474
493
|
const names = options.names ?? DEFAULT_BINDING_NAMES;
|
|
494
|
+
const includeDispatch = options.includeDispatch ?? options.names === void 0;
|
|
475
495
|
const table = /* @__PURE__ */ new Map();
|
|
476
496
|
for (const name of names) {
|
|
477
497
|
const factory = BUILTIN_TOOL_FACTORIES[name];
|
|
@@ -496,7 +516,7 @@ function createBuiltinBindings(options) {
|
|
|
496
516
|
}
|
|
497
517
|
});
|
|
498
518
|
}
|
|
499
|
-
if (
|
|
519
|
+
if (includeDispatch) table.set(DISPATCH_BINDING_NAME, {
|
|
500
520
|
name: DISPATCH_BINDING_NAME,
|
|
501
521
|
execute: async (args, context) => {
|
|
502
522
|
return dispatch(args, {
|
|
@@ -504,7 +524,7 @@ function createBuiltinBindings(options) {
|
|
|
504
524
|
callId: context.callId,
|
|
505
525
|
cwd: options.cwd,
|
|
506
526
|
depth: context.depth,
|
|
507
|
-
|
|
527
|
+
maxDispatchDepth: context.maxDispatchDepth
|
|
508
528
|
});
|
|
509
529
|
}
|
|
510
530
|
});
|
|
@@ -912,7 +932,12 @@ function buildWorkerUrl() {
|
|
|
912
932
|
* hardening itself is defined once, in `workerSpawnOptions` (`worker-pool.ts`),
|
|
913
933
|
* - complete the `MessageChannel` handshake and send the `init` frame,
|
|
914
934
|
* - route `tools.*` calls to the binding table — concurrently, with DSH's
|
|
915
|
-
* `
|
|
935
|
+
* `maxPendingCalls` admission control, the `maxParallelSubCalls` builtin
|
|
936
|
+
* forwarding cap (ADR-0004 consequence: the overflow FIFO-queues for a
|
|
937
|
+
* slot), and the per-run `dispatchConcurrency` hard cap on in-flight
|
|
938
|
+
* `pi.dispatch` calls (ADR-0016 §2: the overflow resolves immediately as
|
|
939
|
+
* rejected, it is never queued). The two caps have independent counters:
|
|
940
|
+
* neither throttles the other.
|
|
916
941
|
* - collect logs / narration / phases and enforce the joint output budget,
|
|
917
942
|
* - enforce the deadline, honour caller cancellation, and settle the run: hand the worker
|
|
918
943
|
* back (pooled) or tear it down (cold).
|
|
@@ -1054,9 +1079,22 @@ async function runPtcProgram(options) {
|
|
|
1054
1079
|
let workerReady = false;
|
|
1055
1080
|
let pendingCalls = 0;
|
|
1056
1081
|
let outputBytes = 0;
|
|
1082
|
+
/**
|
|
1083
|
+
* Concurrently in-flight `pi.dispatch` calls, counted so the per-run hard cap
|
|
1084
|
+
* (ADR-0016 §2) can reject the overflow immediately. There is deliberately no
|
|
1085
|
+
* waiter queue behind this counter: the N+1th concurrent call resolves as
|
|
1086
|
+
* rejected instead of waiting for a slot. Independent of `activeBuiltinCalls`.
|
|
1087
|
+
*/
|
|
1057
1088
|
let activeDispatches = 0;
|
|
1058
|
-
|
|
1059
|
-
|
|
1089
|
+
/**
|
|
1090
|
+
* Concurrently in-flight builtin binding calls, counted against
|
|
1091
|
+
* `maxParallelSubCalls` (ADR-0004 consequence). Unlike the dispatch cap,
|
|
1092
|
+
* the overflow FIFO-queues for a slot — DSH's semantics for builtin fan-out.
|
|
1093
|
+
* Independent of `activeDispatches`.
|
|
1094
|
+
*/
|
|
1095
|
+
let activeBuiltinCalls = 0;
|
|
1096
|
+
const builtinWaiters = [];
|
|
1097
|
+
const runDepth = options.depth ?? 0;
|
|
1060
1098
|
/** Deadline timer: fires `timeoutMs` after the run started. A cancel does not disarm it. */
|
|
1061
1099
|
let runTimer;
|
|
1062
1100
|
/** The cancel's cooperative window: armed and re-armed by `armGraceTimer` only. */
|
|
@@ -1159,22 +1197,22 @@ async function runPtcProgram(options) {
|
|
|
1159
1197
|
fail(PTC_ERROR_KIND.outputLimit, `${source} exceeded the output budget: logs + result reached ${outputBytes} bytes (maxOutputBytes=${config.maxOutputBytes}); ${logs.length} log line(s) retained`);
|
|
1160
1198
|
return false;
|
|
1161
1199
|
};
|
|
1162
|
-
const
|
|
1163
|
-
if (
|
|
1164
|
-
|
|
1200
|
+
const acquireBuiltinSlot = async () => {
|
|
1201
|
+
if (activeBuiltinCalls < config.maxParallelSubCalls) {
|
|
1202
|
+
activeBuiltinCalls += 1;
|
|
1165
1203
|
return;
|
|
1166
1204
|
}
|
|
1167
1205
|
await new Promise((slot) => {
|
|
1168
|
-
|
|
1206
|
+
builtinWaiters.push(slot);
|
|
1169
1207
|
});
|
|
1170
1208
|
};
|
|
1171
|
-
const
|
|
1172
|
-
const next =
|
|
1209
|
+
const releaseBuiltinSlot = () => {
|
|
1210
|
+
const next = builtinWaiters.shift();
|
|
1173
1211
|
if (next) {
|
|
1174
1212
|
next();
|
|
1175
1213
|
return;
|
|
1176
1214
|
}
|
|
1177
|
-
|
|
1215
|
+
activeBuiltinCalls -= 1;
|
|
1178
1216
|
};
|
|
1179
1217
|
const handleCall = (frame) => {
|
|
1180
1218
|
pendingCalls += 1;
|
|
@@ -1198,10 +1236,25 @@ async function runPtcProgram(options) {
|
|
|
1198
1236
|
});
|
|
1199
1237
|
return;
|
|
1200
1238
|
}
|
|
1201
|
-
|
|
1202
|
-
if (
|
|
1203
|
-
|
|
1204
|
-
|
|
1239
|
+
const isDispatch = frame.tool === DISPATCH_BINDING_NAME;
|
|
1240
|
+
if (isDispatch) {
|
|
1241
|
+
if (activeDispatches >= config.dispatchConcurrency) {
|
|
1242
|
+
postCallResult({
|
|
1243
|
+
kind: HOST_FRAME_KIND.callResult,
|
|
1244
|
+
callId: frame.callId,
|
|
1245
|
+
tool: frame.tool,
|
|
1246
|
+
ok: true,
|
|
1247
|
+
value: dispatchConcurrencyLimitReached()
|
|
1248
|
+
});
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
activeDispatches += 1;
|
|
1252
|
+
} else {
|
|
1253
|
+
await acquireBuiltinSlot();
|
|
1254
|
+
if (settled) {
|
|
1255
|
+
releaseBuiltinSlot();
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1205
1258
|
}
|
|
1206
1259
|
try {
|
|
1207
1260
|
const value = await binding.execute(frame.args, {
|
|
@@ -1236,7 +1289,8 @@ async function runPtcProgram(options) {
|
|
|
1236
1289
|
message: messageOf(error)
|
|
1237
1290
|
});
|
|
1238
1291
|
} finally {
|
|
1239
|
-
|
|
1292
|
+
if (isDispatch) activeDispatches -= 1;
|
|
1293
|
+
else releaseBuiltinSlot();
|
|
1240
1294
|
}
|
|
1241
1295
|
};
|
|
1242
1296
|
/** `true` when the worker actually received the result (see the hoist at the call site). */
|
|
@@ -1569,6 +1623,20 @@ function renderModelValue(value) {
|
|
|
1569
1623
|
* `details` stays raw for the TUI. See ADR-0012.
|
|
1570
1624
|
*/
|
|
1571
1625
|
/**
|
|
1626
|
+
* Read the depth baseline pi-ptc was started with inside a child pi process.
|
|
1627
|
+
*
|
|
1628
|
+
* `dispatch()` stamps `PI_PTC_DEPTH` (the child's own depth) onto the spawned subprocess's
|
|
1629
|
+
* environment, and the extension entrypoint reads it back here so a child PTC run's binding
|
|
1630
|
+
* context starts at the dispatched depth instead of at 0 — otherwise the recursion bound in
|
|
1631
|
+
* `dispatch()` could never bite below the first level. Only a pure non-negative integer is
|
|
1632
|
+
* accepted; a missing or malformed value means "not a dispatched child" and yields 0.
|
|
1633
|
+
*/
|
|
1634
|
+
function resolveDepthFromEnv(source = process.env) {
|
|
1635
|
+
const raw = source.PI_PTC_DEPTH;
|
|
1636
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) return 0;
|
|
1637
|
+
return Number.parseInt(raw, 10);
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1572
1640
|
* Binding names for one run: the built-ins this session actually has enabled (T7, #21).
|
|
1573
1641
|
*
|
|
1574
1642
|
* A PTC program must never reach further than the session it runs in — a session started
|
|
@@ -2262,8 +2330,10 @@ function createPtcRunCodeTool(options = {}) {
|
|
|
2262
2330
|
cwd,
|
|
2263
2331
|
bindings: createBuiltinBindings({
|
|
2264
2332
|
cwd,
|
|
2265
|
-
names
|
|
2333
|
+
names,
|
|
2334
|
+
includeDispatch: true
|
|
2266
2335
|
}),
|
|
2336
|
+
...options.depth === void 0 ? {} : { depth: options.depth },
|
|
2267
2337
|
...params.timeoutMs === void 0 ? {} : { timeoutMs: params.timeoutMs },
|
|
2268
2338
|
...signal === void 0 ? {} : { signal },
|
|
2269
2339
|
...options.config === void 0 ? {} : { config: options.config },
|
|
@@ -2337,11 +2407,6 @@ const PARAMETERS = Type.Object({
|
|
|
2337
2407
|
script: Type.String({ description: "The program: the body of an async TypeScript function." }),
|
|
2338
2408
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Plain-JSON input bound to the program's `args` global. No functions, symbols, undefined values or cycles." }))
|
|
2339
2409
|
});
|
|
2340
|
-
function describeValue(value) {
|
|
2341
|
-
if (value === null) return "null";
|
|
2342
|
-
if (Array.isArray(value)) return "an array";
|
|
2343
|
-
return `a ${typeof value}`;
|
|
2344
|
-
}
|
|
2345
2410
|
/**
|
|
2346
2411
|
* Reject anything that is not plain JSON, with a path-qualified reason.
|
|
2347
2412
|
*
|
|
@@ -2434,9 +2499,11 @@ function createPtcWorkflowTool(options = {}) {
|
|
|
2434
2499
|
cwd,
|
|
2435
2500
|
bindings: createBuiltinBindings({
|
|
2436
2501
|
cwd,
|
|
2437
|
-
names
|
|
2502
|
+
names,
|
|
2503
|
+
includeDispatch: true
|
|
2438
2504
|
}),
|
|
2439
2505
|
...params.args === void 0 ? {} : { args: params.args },
|
|
2506
|
+
...options.depth === void 0 ? {} : { depth: options.depth },
|
|
2440
2507
|
...signal === void 0 ? {} : { signal },
|
|
2441
2508
|
...options.config === void 0 ? {} : { config: options.config },
|
|
2442
2509
|
...pool === void 0 ? {} : { pool }
|
|
@@ -2880,13 +2947,21 @@ function ptcSubagents(pi) {
|
|
|
2880
2947
|
* `tools.<name>(...)` call fail.
|
|
2881
2948
|
*/
|
|
2882
2949
|
const getBindingSourceNames = () => bindingSource(mode, pi.getActiveTools());
|
|
2950
|
+
/**
|
|
2951
|
+
* Depth baseline for PTC runs this pi process starts (ADR-0016 Recursive section).
|
|
2952
|
+
* Inside a pi process spawned by `pi.dispatch`, the env carries `PI_PTC_DEPTH`; a
|
|
2953
|
+
* normal pi session has none, and the parent turn's runs stay at depth 0.
|
|
2954
|
+
*/
|
|
2955
|
+
const ptcDepth = resolveDepthFromEnv();
|
|
2883
2956
|
pi.registerTool(createPtcRunCodeTool({
|
|
2884
2957
|
getBindingSourceNames,
|
|
2885
|
-
getPool: () => turnPools.get("run_code")
|
|
2958
|
+
getPool: () => turnPools.get("run_code"),
|
|
2959
|
+
depth: ptcDepth
|
|
2886
2960
|
}));
|
|
2887
2961
|
pi.registerTool(createPtcWorkflowTool({
|
|
2888
2962
|
getBindingSourceNames,
|
|
2889
|
-
getPool: () => turnPools.get("workflow")
|
|
2963
|
+
getPool: () => turnPools.get("workflow"),
|
|
2964
|
+
depth: ptcDepth
|
|
2890
2965
|
}));
|
|
2891
2966
|
/**
|
|
2892
2967
|
* Retire the turn's pools. `drain()` terminates the warm workers, so nothing outlives the
|